diff --git a/pkg/gen/internalapi/embedded_spec.go b/pkg/gen/internalapi/embedded_spec.go index 794c7a791f2..bbf9c7eb293 100644 --- a/pkg/gen/internalapi/embedded_spec.go +++ b/pkg/gen/internalapi/embedded_spec.go @@ -3061,6 +3061,13 @@ func init() { "description": "ID of the order that the upload belongs to", "name": "orderId", "in": "query" + }, + { + "type": "string", + "format": "uuid", + "description": "Optional PPM shipment ID related to the upload", + "name": "ppmId", + "in": "query" } ], "responses": { @@ -10940,6 +10947,13 @@ func init() { "description": "ID of the order that the upload belongs to", "name": "orderId", "in": "query" + }, + { + "type": "string", + "format": "uuid", + "description": "Optional PPM shipment ID related to the upload", + "name": "ppmId", + "in": "query" } ], "responses": { diff --git a/pkg/gen/internalapi/internaloperations/uploads/delete_upload_parameters.go b/pkg/gen/internalapi/internaloperations/uploads/delete_upload_parameters.go index 91cad5f24f6..371f160ef2e 100644 --- a/pkg/gen/internalapi/internaloperations/uploads/delete_upload_parameters.go +++ b/pkg/gen/internalapi/internaloperations/uploads/delete_upload_parameters.go @@ -36,6 +36,10 @@ type DeleteUploadParams struct { In: query */ OrderID *strfmt.UUID + /*Optional PPM shipment ID related to the upload + In: query + */ + PpmID *strfmt.UUID /*UUID of the upload to be deleted Required: true In: path @@ -59,6 +63,11 @@ func (o *DeleteUploadParams) BindRequest(r *http.Request, route *middleware.Matc res = append(res, err) } + qPpmID, qhkPpmID, _ := qs.GetOK("ppmId") + if err := o.bindPpmID(qPpmID, qhkPpmID, route.Formats); err != nil { + res = append(res, err) + } + rUploadID, rhkUploadID, _ := route.Params.GetOK("uploadId") if err := o.bindUploadID(rUploadID, rhkUploadID, route.Formats); err != nil { res = append(res, err) @@ -106,6 +115,43 @@ func (o *DeleteUploadParams) validateOrderID(formats strfmt.Registry) error { return nil } +// bindPpmID binds and validates parameter PpmID from query. +func (o *DeleteUploadParams) bindPpmID(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: false + // AllowEmptyValue: false + + if raw == "" { // empty values pass all other validations + return nil + } + + // Format: uuid + value, err := formats.Parse("uuid", raw) + if err != nil { + return errors.InvalidType("ppmId", "query", "strfmt.UUID", raw) + } + o.PpmID = (value.(*strfmt.UUID)) + + if err := o.validatePpmID(formats); err != nil { + return err + } + + return nil +} + +// validatePpmID carries on validations for parameter PpmID +func (o *DeleteUploadParams) validatePpmID(formats strfmt.Registry) error { + + if err := validate.FormatOf("ppmId", "query", "uuid", o.PpmID.String(), formats); err != nil { + return err + } + return nil +} + // bindUploadID binds and validates parameter UploadID from path. func (o *DeleteUploadParams) bindUploadID(rawData []string, hasKey bool, formats strfmt.Registry) error { var raw string diff --git a/pkg/gen/internalapi/internaloperations/uploads/delete_upload_urlbuilder.go b/pkg/gen/internalapi/internaloperations/uploads/delete_upload_urlbuilder.go index 6d0850522b1..ac38bd47b6f 100644 --- a/pkg/gen/internalapi/internaloperations/uploads/delete_upload_urlbuilder.go +++ b/pkg/gen/internalapi/internaloperations/uploads/delete_upload_urlbuilder.go @@ -19,6 +19,7 @@ type DeleteUploadURL struct { UploadID strfmt.UUID OrderID *strfmt.UUID + PpmID *strfmt.UUID _basePath string // avoid unkeyed usage @@ -69,6 +70,14 @@ func (o *DeleteUploadURL) Build() (*url.URL, error) { qs.Set("orderId", orderIDQ) } + var ppmIDQ string + if o.PpmID != nil { + ppmIDQ = o.PpmID.String() + } + if ppmIDQ != "" { + qs.Set("ppmId", ppmIDQ) + } + _result.RawQuery = qs.Encode() return &_result, nil diff --git a/pkg/handlers/ghcapi/move.go b/pkg/handlers/ghcapi/move.go index 7cb8caa553b..2c75a267277 100644 --- a/pkg/handlers/ghcapi/move.go +++ b/pkg/handlers/ghcapi/move.go @@ -15,6 +15,7 @@ import ( "github.com/transcom/mymove/pkg/gen/ghcmessages" "github.com/transcom/mymove/pkg/handlers" "github.com/transcom/mymove/pkg/handlers/ghcapi/internal/payloads" + "github.com/transcom/mymove/pkg/models" "github.com/transcom/mymove/pkg/services" ) @@ -46,6 +47,11 @@ func (h GetMoveHandler) Handle(params moveop.GetMoveParams) middleware.Responder } } + privileges, err := models.FetchPrivilegesForUser(appCtx.DB(), appCtx.Session().UserID) + if err != nil { + appCtx.Logger().Error("Error retreiving user privileges", zap.Error(err)) + } + // if this user is accessing the move record, we need to lock it so others can't edit it // to allow for locking a move, we need to look at these things // 1. Is the user an office user? @@ -66,8 +72,18 @@ func (h GetMoveHandler) Handle(params moveop.GetMoveParams) middleware.Responder } } - payload := payloads.Move(move) - return moveop.NewGetMoveOK().WithPayload(payload), nil + moveOrders, err := models.FetchOrder(appCtx.DB(), move.OrdersID) + if err != nil { + appCtx.Logger().Error("Error retreiving user privileges", zap.Error(err)) + } + + if moveOrders.OrdersType == "SAFETY" && !privileges.HasPrivilege(models.PrivilegeTypeSafety) { + appCtx.Logger().Error("Invalid permissions") + return moveop.NewGetMoveNotFound(), nil + } else { + payload := payloads.Move(move) + return moveop.NewGetMoveOK().WithPayload(payload), nil + } }) } diff --git a/pkg/handlers/ghcapi/move_test.go b/pkg/handlers/ghcapi/move_test.go index 89c17207769..aef8ba17471 100644 --- a/pkg/handlers/ghcapi/move_test.go +++ b/pkg/handlers/ghcapi/move_test.go @@ -111,7 +111,7 @@ func (suite *HandlerSuite) TestGetMoveHandler() { Type: &factory.TransportationOffices.CloseoutOffice, }, }, nil) - moveFetcher := moveservice.NewMoveFetcher() + mockFetcher := mocks.MoveFetcher{} mockLocker := movelocker.NewMoveLocker() requestOfficeUser := factory.BuildOfficeUserWithRoles(suite.DB(), nil, []roles.RoleType{roles.RoleTypeServicesCounselor}) @@ -126,10 +126,16 @@ func (suite *HandlerSuite) TestGetMoveHandler() { handler := GetMoveHandler{ HandlerConfig: suite.HandlerConfig(), - MoveFetcher: moveFetcher, + MoveFetcher: &mockFetcher, MoveLocker: mockLocker, } + mockFetcher.On("FetchMove", + mock.AnythingOfType("*appcontext.appContext"), + move.Locator, + mock.Anything, + ).Return(&move, nil) + response := handler.Handle(params) suite.IsType(&moveops.GetMoveOK{}, response) payload := response.(*moveops.GetMoveOK).Payload @@ -228,6 +234,40 @@ func (suite *HandlerSuite) TestGetMoveHandler() { // Validate outgoing payload: nil payload suite.Nil(payload) }) + + suite.Run("Unsuccessful move fetch - invalid privileges", func() { + setupTestData() + mockFetcher := mocks.MoveFetcher{} + mockLocker := movelocker.NewMoveLocker() + + handler := GetMoveHandler{ + HandlerConfig: suite.HandlerConfig(), + MoveFetcher: &mockFetcher, + MoveLocker: mockLocker, + } + + mockFetcher.On("FetchMove", + mock.AnythingOfType("*appcontext.appContext"), + move.Locator, + mock.Anything, + ).Return(&models.Move{}, apperror.NotFoundError{}) + + req := httptest.NewRequest("GET", "/move/#{move.locator}", nil) + req = suite.AuthenticateUserRequest(req, requestUser.User) + params := moveops.GetMoveParams{ + HTTPRequest: req, + Locator: move.Locator, + } + + // Validate incoming payload: no body to validate + + response := handler.Handle(params) + suite.IsType(&moveops.GetMoveNotFound{}, response) + payload := response.(*moveops.GetMoveNotFound).Payload + + // Validate outgoing payload: nil payload + suite.Nil(payload) + }) } func (suite *HandlerSuite) TestSearchMovesHandler() { diff --git a/pkg/handlers/internalapi/uploads.go b/pkg/handlers/internalapi/uploads.go index e9e67d155a3..503061f05bb 100644 --- a/pkg/handlers/internalapi/uploads.go +++ b/pkg/handlers/internalapi/uploads.go @@ -122,6 +122,17 @@ func (h DeleteUploadHandler) Handle(params uploadop.DeleteUploadParams) middlewa return handlers.ResponseForError(appCtx.Logger(), err), err } + var ppmShipmentStatus models.PPMShipmentStatus + + if params.PpmID != nil { + ppmShipmentId, _ := uuid.FromString(params.PpmID.String()) + ppmShipment, err := models.FetchPPMShipmentByPPMShipmentID(appCtx.DB(), ppmShipmentId) + if err != nil { + return handlers.ResponseForError(appCtx.Logger(), err), err + } + ppmShipmentStatus = ppmShipment.Status + } + if params.OrderID != nil { orderID, _ := uuid.FromString(params.OrderID.String()) move, e := models.FetchMoveByOrderID(appCtx.DB(), orderID) @@ -157,8 +168,8 @@ func (h DeleteUploadHandler) Handle(params uploadop.DeleteUploadParams) middlewa appCtx.Logger().Error("error retrieving move associated with this upload", zap.Error(err)) } - //If move status is not DRAFT, upload cannot be deleted - if *uploadInformation.MoveStatus != models.MoveStatusDRAFT { + //If move status is not DRAFT and customer is not uploading ppm docs, upload cannot be deleted + if (*uploadInformation.MoveStatus != models.MoveStatusDRAFT) && (ppmShipmentStatus != models.PPMShipmentStatusWaitingOnCustomer) { return uploadop.NewDeleteUploadForbidden(), fmt.Errorf("deletion not permitted Move is not in 'DRAFT' status") } diff --git a/src/pages/MyMove/PPM/Closeout/Expenses/Expenses.jsx b/src/pages/MyMove/PPM/Closeout/Expenses/Expenses.jsx index d0d7ae7ca0c..3ed1d1c9d58 100644 --- a/src/pages/MyMove/PPM/Closeout/Expenses/Expenses.jsx +++ b/src/pages/MyMove/PPM/Closeout/Expenses/Expenses.jsx @@ -87,7 +87,7 @@ const Expenses = () => { }; const handleUploadDelete = (uploadId, fieldName, setFieldTouched, setFieldValue) => { - deleteUpload(uploadId) + deleteUpload(uploadId, null, mtoShipment?.ppmShipment?.id) .then(() => { const filteredUploads = mtoShipment.ppmShipment.movingExpenses[currentIndex][fieldName].uploads.filter( (upload) => upload.id !== uploadId, diff --git a/src/pages/MyMove/PPM/Closeout/ProGear/ProGear.jsx b/src/pages/MyMove/PPM/Closeout/ProGear/ProGear.jsx index 0f7f90e3d1c..a2e40b1459f 100644 --- a/src/pages/MyMove/PPM/Closeout/ProGear/ProGear.jsx +++ b/src/pages/MyMove/PPM/Closeout/ProGear/ProGear.jsx @@ -96,7 +96,7 @@ const ProGear = () => { }; const handleUploadDelete = (uploadId, fieldName, setFieldTouched, setFieldValue) => { - deleteUpload(uploadId) + deleteUpload(uploadId, null, mtoShipment?.ppmShipment?.id) .then(() => { const filteredUploads = mtoShipment.ppmShipment.proGearWeightTickets[currentIndex][fieldName].uploads.filter( (upload) => upload.id !== uploadId, diff --git a/src/pages/MyMove/PPM/Closeout/WeightTickets/WeightTickets.jsx b/src/pages/MyMove/PPM/Closeout/WeightTickets/WeightTickets.jsx index 5cb319edec5..5dcd4a3a38a 100644 --- a/src/pages/MyMove/PPM/Closeout/WeightTickets/WeightTickets.jsx +++ b/src/pages/MyMove/PPM/Closeout/WeightTickets/WeightTickets.jsx @@ -93,7 +93,7 @@ const WeightTickets = () => { }; const handleUploadDelete = (uploadId, fieldName, setFieldTouched, setFieldValue) => { - deleteUpload(uploadId) + deleteUpload(uploadId, null, mtoShipment?.ppmShipment?.id) .then(() => { const filteredUploads = mtoShipment.ppmShipment.weightTickets[currentIndex][fieldName].uploads.filter( (upload) => upload.id !== uploadId, diff --git a/src/services/internalApi.js b/src/services/internalApi.js index 78c1df3fb00..b578ec9706f 100644 --- a/src/services/internalApi.js +++ b/src/services/internalApi.js @@ -248,12 +248,13 @@ export async function createUploadForPPMDocument(ppmShipmentId, documentId, file ); } -export async function deleteUpload(uploadId, orderId) { +export async function deleteUpload(uploadId, orderId, ppmId) { return makeInternalRequest( 'uploads.deleteUpload', { uploadId, orderId, + ppmId, }, { normalize: false, diff --git a/swagger-def/internal.yaml b/swagger-def/internal.yaml index 3d6381b7e84..ad91810a37b 100644 --- a/swagger-def/internal.yaml +++ b/swagger-def/internal.yaml @@ -3075,6 +3075,11 @@ paths: type: string format: uuid description: ID of the order that the upload belongs to + - in: query + name: ppmId + type: string + format: uuid + description: Optional PPM shipment ID related to the upload responses: "204": description: deleted diff --git a/swagger/internal.yaml b/swagger/internal.yaml index 9e5d793734d..f65548fc5a6 100644 --- a/swagger/internal.yaml +++ b/swagger/internal.yaml @@ -4371,6 +4371,11 @@ paths: type: string format: uuid description: ID of the order that the upload belongs to + - in: query + name: ppmId + type: string + format: uuid + description: Optional PPM shipment ID related to the upload responses: '204': description: deleted