From afabbefb90cc1082fe64a1e2cb7adedfacc78783 Mon Sep 17 00:00:00 2001 From: Cory Kleinjan Date: Thu, 27 Feb 2025 15:39:50 +0000 Subject: [PATCH] Adding ability to service counselor to upload weight tickets --- pkg/gen/ghcapi/embedded_spec.go | 14 ++ .../uploads/create_upload_parameters.go | 37 +++++ .../uploads/create_upload_urlbuilder.go | 9 +- pkg/handlers/ghcapi/api.go | 15 +- pkg/handlers/ghcapi/uploads.go | 136 +++++++++++++++--- pkg/handlers/ghcapi/uploads_test.go | 15 +- swagger-def/ghc.yaml | 5 + swagger/ghc.yaml | 5 + 8 files changed, 213 insertions(+), 23 deletions(-) diff --git a/pkg/gen/ghcapi/embedded_spec.go b/pkg/gen/ghcapi/embedded_spec.go index bb69f484f51..67544360d08 100644 --- a/pkg/gen/ghcapi/embedded_spec.go +++ b/pkg/gen/ghcapi/embedded_spec.go @@ -6580,6 +6580,13 @@ func init() { "name": "file", "in": "formData", "required": true + }, + { + "type": "boolean", + "description": "If the upload is a Weight Receipt", + "name": "weightReceipt", + "in": "query", + "required": true } ], "responses": { @@ -24027,6 +24034,13 @@ func init() { "name": "file", "in": "formData", "required": true + }, + { + "type": "boolean", + "description": "If the upload is a Weight Receipt", + "name": "weightReceipt", + "in": "query", + "required": true } ], "responses": { diff --git a/pkg/gen/ghcapi/ghcoperations/uploads/create_upload_parameters.go b/pkg/gen/ghcapi/ghcoperations/uploads/create_upload_parameters.go index c3c00400764..20a2b723f6a 100644 --- a/pkg/gen/ghcapi/ghcoperations/uploads/create_upload_parameters.go +++ b/pkg/gen/ghcapi/ghcoperations/uploads/create_upload_parameters.go @@ -14,6 +14,7 @@ import ( "github.com/go-openapi/runtime" "github.com/go-openapi/runtime/middleware" "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" "github.com/go-openapi/validate" ) @@ -50,6 +51,11 @@ type CreateUploadParams struct { In: formData */ File io.ReadCloser + /*If the upload is a Weight Receipt + Required: true + In: query + */ + WeightReceipt bool } // BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface @@ -85,6 +91,11 @@ func (o *CreateUploadParams) BindRequest(r *http.Request, route *middleware.Matc } else { o.File = &runtime.File{Data: file, Header: fileHeader} } + + qWeightReceipt, qhkWeightReceipt, _ := qs.GetOK("weightReceipt") + if err := o.bindWeightReceipt(qWeightReceipt, qhkWeightReceipt, route.Formats); err != nil { + res = append(res, err) + } if len(res) > 0 { return errors.CompositeValidationError(res...) } @@ -134,3 +145,29 @@ func (o *CreateUploadParams) validateDocumentID(formats strfmt.Registry) error { func (o *CreateUploadParams) bindFile(file multipart.File, header *multipart.FileHeader) error { return nil } + +// bindWeightReceipt binds and validates parameter WeightReceipt from query. +func (o *CreateUploadParams) bindWeightReceipt(rawData []string, hasKey bool, formats strfmt.Registry) error { + if !hasKey { + return errors.Required("weightReceipt", "query", rawData) + } + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: true + // AllowEmptyValue: false + + if err := validate.RequiredString("weightReceipt", "query", raw); err != nil { + return err + } + + value, err := swag.ConvertBool(raw) + if err != nil { + return errors.InvalidType("weightReceipt", "query", "bool", raw) + } + o.WeightReceipt = value + + return nil +} diff --git a/pkg/gen/ghcapi/ghcoperations/uploads/create_upload_urlbuilder.go b/pkg/gen/ghcapi/ghcoperations/uploads/create_upload_urlbuilder.go index 526f68188d1..f0fcd48c9be 100644 --- a/pkg/gen/ghcapi/ghcoperations/uploads/create_upload_urlbuilder.go +++ b/pkg/gen/ghcapi/ghcoperations/uploads/create_upload_urlbuilder.go @@ -11,11 +11,13 @@ import ( golangswaggerpaths "path" "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" ) // CreateUploadURL generates an URL for the create upload operation type CreateUploadURL struct { - DocumentID *strfmt.UUID + DocumentID *strfmt.UUID + WeightReceipt bool _basePath string // avoid unkeyed usage @@ -59,6 +61,11 @@ func (o *CreateUploadURL) Build() (*url.URL, error) { qs.Set("documentId", documentIDQ) } + weightReceiptQ := swag.FormatBool(o.WeightReceipt) + if weightReceiptQ != "" { + qs.Set("weightReceipt", weightReceiptQ) + } + _result.RawQuery = qs.Encode() return &_result, nil diff --git a/pkg/handlers/ghcapi/api.go b/pkg/handlers/ghcapi/api.go index 4119b8b3564..723832515a2 100644 --- a/pkg/handlers/ghcapi/api.go +++ b/pkg/handlers/ghcapi/api.go @@ -9,6 +9,7 @@ import ( ghcops "github.com/transcom/mymove/pkg/gen/ghcapi/ghcoperations" "github.com/transcom/mymove/pkg/handlers" paperwork "github.com/transcom/mymove/pkg/paperwork" + paperworkgenerator "github.com/transcom/mymove/pkg/paperwork" paymentrequesthelper "github.com/transcom/mymove/pkg/payment_request" "github.com/transcom/mymove/pkg/services/address" boatshipment "github.com/transcom/mymove/pkg/services/boat_shipment" @@ -55,6 +56,7 @@ import ( "github.com/transcom/mymove/pkg/services/upload" usersroles "github.com/transcom/mymove/pkg/services/users_roles" weightticket "github.com/transcom/mymove/pkg/services/weight_ticket" + weightticketparser "github.com/transcom/mymove/pkg/services/weight_ticket_parser" "github.com/transcom/mymove/pkg/uploader" ) @@ -722,7 +724,18 @@ func NewGhcAPIHandler(handlerConfig handlers.HandlerConfig) *ghcops.MymoveAPI { paymentPacketCreator := ppmshipment.NewPaymentPacketCreator(ppmShipmentFetcher, pdfGenerator, AOAPacketCreator) ghcAPI.PpmShowPaymentPacketHandler = ShowPaymentPacketHandler{handlerConfig, paymentPacketCreator} - ghcAPI.UploadsCreateUploadHandler = CreateUploadHandler{handlerConfig} + pdfGenerator, err = paperworkgenerator.NewGenerator(userUploader.Uploader()) + if err != nil { + log.Fatalln(err) + } + + parserComputer := weightticketparser.NewWeightTicketComputer() + weightGenerator, err := weightticketparser.NewWeightTicketParserGenerator(pdfGenerator) + if err != nil { + log.Fatalln(err) + } + + ghcAPI.UploadsCreateUploadHandler = CreateUploadHandler{handlerConfig, parserComputer, weightGenerator} ghcAPI.UploadsUpdateUploadHandler = UpdateUploadHandler{handlerConfig, upload.NewUploadInformationFetcher()} ghcAPI.UploadsDeleteUploadHandler = DeleteUploadHandler{handlerConfig, upload.NewUploadInformationFetcher()} diff --git a/pkg/handlers/ghcapi/uploads.go b/pkg/handlers/ghcapi/uploads.go index a74e5d48498..afa7b95b3c7 100644 --- a/pkg/handlers/ghcapi/uploads.go +++ b/pkg/handlers/ghcapi/uploads.go @@ -1,8 +1,14 @@ package ghcapi import ( + "io" + "path/filepath" + "regexp" + "strings" + "github.com/go-openapi/runtime" "github.com/go-openapi/runtime/middleware" + "github.com/gobuffalo/validate/v3" "github.com/gofrs/uuid" "go.uber.org/zap" @@ -14,11 +20,16 @@ import ( "github.com/transcom/mymove/pkg/models" "github.com/transcom/mymove/pkg/services" "github.com/transcom/mymove/pkg/services/upload" + weightticketparser "github.com/transcom/mymove/pkg/services/weight_ticket_parser" uploaderpkg "github.com/transcom/mymove/pkg/uploader" ) +const weightEstimatePages = 11 + type CreateUploadHandler struct { handlers.HandlerConfig + services.WeightTicketComputer + services.WeightTicketGenerator } func (h CreateUploadHandler) Handle(params uploadop.CreateUploadParams) middleware.Responder { @@ -57,29 +68,114 @@ func (h CreateUploadHandler) Handle(params uploadop.CreateUploadParams) middlewa docID = &document.ID } - newUserUpload, url, verrs, createErr := uploaderpkg.CreateUserUploadForDocumentWrapper( - appCtx, - appCtx.Session().UserID, - h.FileStorer(), - file, - file.Header.Filename, - uploaderpkg.MaxCustomerUserUploadFileSizeLimit, - uploaderpkg.AllowedTypesServiceMember, - docID, - models.UploadTypeOFFICE, - ) + var newUserUpload *models.UserUpload + var verrs *validate.Errors + var url string + var createErr error + isWeightEstimatorFile := false + + // extract extension from filename + filename := file.Header.Filename + timestampPattern := regexp.MustCompile(`-(\d{14})$`) + + timestamp := "" + filenameWithoutTimestamp := "" + if matches := timestampPattern.FindStringSubmatch(filename); len(matches) > 1 { + timestamp = matches[1] + filenameWithoutTimestamp = strings.TrimSuffix(filename, "-"+timestamp) + } else { + filenameWithoutTimestamp = filename + } + + extension := filepath.Ext(filenameWithoutTimestamp) + extensionLower := strings.ToLower(extension) + + // check if file is an excel file + if extensionLower == ".xlsx" { + var err error - if verrs.HasAny() || createErr != nil { - appCtx.Logger().Error("failed to create new user upload", zap.Error(createErr), zap.String("verrs", verrs.Error())) - switch createErr.(type) { - case uploaderpkg.ErrTooLarge: - return uploadop.NewCreateUploadRequestEntityTooLarge(), rollbackErr - case uploaderpkg.ErrFile: + isWeightEstimatorFile, err = weightticketparser.IsWeightEstimatorFile(appCtx, file) + + if err != nil { return uploadop.NewCreateUploadInternalServerError(), rollbackErr - case uploaderpkg.ErrFailedToInitUploader: + } + + _, err = file.Data.Seek(0, io.SeekStart) + + if err != nil { return uploadop.NewCreateUploadInternalServerError(), rollbackErr - default: - return handlers.ResponseForVErrors(appCtx.Logger(), verrs, createErr), rollbackErr + } + } + + if params.WeightReceipt && isWeightEstimatorFile { + pageValues, err := h.WeightTicketComputer.ParseWeightEstimatorExcelFile(appCtx, file) + + if err != nil { + return uploadop.NewCreateUploadInternalServerError(), rollbackErr + } + + pdfFileName := strings.TrimSuffix(filenameWithoutTimestamp, filepath.Ext(filenameWithoutTimestamp)) + ".pdf" + "-" + timestamp + aFile, pdfInfo, err := h.WeightTicketGenerator.FillWeightEstimatorPDFForm(*pageValues, pdfFileName) + + // Ensure weight receipt PDF is not corrupted + if err != nil || pdfInfo.PageCount != weightEstimatePages { + return uploadop.NewCreateUploadInternalServerError(), rollbackErr + } + + newUserUpload, url, verrs, createErr = uploaderpkg.CreateUserUploadForDocumentWrapper( + appCtx, + appCtx.Session().UserID, + h.FileStorer(), + uploaderpkg.File{File: aFile}, + file.Header.Filename, + uploaderpkg.MaxCustomerUserUploadFileSizeLimit, + uploaderpkg.AllowedTypesPPMDocuments, + docID, + models.UploadTypeOFFICE, + ) + + if verrs.HasAny() || createErr != nil { + appCtx.Logger().Error("failed to create new user upload", zap.Error(createErr), zap.String("verrs", verrs.Error())) + switch createErr.(type) { + case uploaderpkg.ErrTooLarge: + return uploadop.NewCreateUploadRequestEntityTooLarge(), rollbackErr + case uploaderpkg.ErrFile: + return uploadop.NewCreateUploadInternalServerError(), rollbackErr + case uploaderpkg.ErrFailedToInitUploader: + return uploadop.NewCreateUploadInternalServerError(), rollbackErr + default: + return handlers.ResponseForVErrors(appCtx.Logger(), verrs, createErr), rollbackErr + } + } + + if err != nil { + return uploadop.NewCreateUploadInternalServerError(), rollbackErr + } + } else { + newUserUpload, url, verrs, createErr = uploaderpkg.CreateUserUploadForDocumentWrapper( + appCtx, + appCtx.Session().UserID, + h.FileStorer(), + file, + file.Header.Filename, + uploaderpkg.MaxCustomerUserUploadFileSizeLimit, + uploaderpkg.AllowedTypesServiceMember, + docID, + models.UploadTypeOFFICE, + ) + + if verrs.HasAny() || createErr != nil { + appCtx.Logger().Error("failed to create new user upload", zap.Error(createErr), zap.String("verrs", verrs.Error())) + switch createErr.(type) { + case uploaderpkg.ErrTooLarge: + return uploadop.NewCreateUploadRequestEntityTooLarge(), rollbackErr + case uploaderpkg.ErrFile: + return uploadop.NewCreateUploadInternalServerError(), rollbackErr + case uploaderpkg.ErrFailedToInitUploader: + return uploadop.NewCreateUploadInternalServerError(), rollbackErr + default: + return handlers.ResponseForVErrors(appCtx.Logger(), verrs, createErr), rollbackErr + } } } diff --git a/pkg/handlers/ghcapi/uploads_test.go b/pkg/handlers/ghcapi/uploads_test.go index 94830bdb5bf..c45eb2c961c 100644 --- a/pkg/handlers/ghcapi/uploads_test.go +++ b/pkg/handlers/ghcapi/uploads_test.go @@ -10,7 +10,10 @@ import ( uploadop "github.com/transcom/mymove/pkg/gen/ghcapi/ghcoperations/uploads" "github.com/transcom/mymove/pkg/handlers" "github.com/transcom/mymove/pkg/models" + paperworkgenerator "github.com/transcom/mymove/pkg/paperwork" + weightticketparser "github.com/transcom/mymove/pkg/services/weight_ticket_parser" storageTest "github.com/transcom/mymove/pkg/storage/test" + "github.com/transcom/mymove/pkg/uploader" ) const FixturePDF = "test.pdf" @@ -33,7 +36,17 @@ func makeRequest(suite *HandlerSuite, params uploadop.CreateUploadParams, servic handlerConfig := suite.HandlerConfig() handlerConfig.SetFileStorer(fakeS3) - handler := CreateUploadHandler{handlerConfig} + userUploader, err := uploader.NewUserUploader(handlerConfig.FileStorer(), uploader.MaxCustomerUserUploadFileSizeLimit) + suite.FatalNoError(err) + + pdfGenerator, err := paperworkgenerator.NewGenerator(userUploader.Uploader()) + suite.FatalNoError(err) + + parserComputer := weightticketparser.NewWeightTicketComputer() + weightGenerator, err := weightticketparser.NewWeightTicketParserGenerator(pdfGenerator) + suite.FatalNoError(err) + + handler := CreateUploadHandler{handlerConfig, parserComputer, weightGenerator} response := handler.Handle(params) return response diff --git a/swagger-def/ghc.yaml b/swagger-def/ghc.yaml index faf1e7b44af..40bf4969fa0 100644 --- a/swagger-def/ghc.yaml +++ b/swagger-def/ghc.yaml @@ -4358,6 +4358,11 @@ paths: type: file description: The file to upload. required: true + - in: query + name: weightReceipt + type: boolean + description: If the upload is a Weight Receipt + required: true responses: "201": description: created upload diff --git a/swagger/ghc.yaml b/swagger/ghc.yaml index e6b1d27b36e..5f7e5c5fc6b 100644 --- a/swagger/ghc.yaml +++ b/swagger/ghc.yaml @@ -4558,6 +4558,11 @@ paths: type: file description: The file to upload. required: true + - in: query + name: weightReceipt + type: boolean + description: If the upload is a Weight Receipt + required: true responses: '201': description: created upload