diff --git a/.circleci/config.yml b/.circleci/config.yml index dc5a2796aa0..fc1cd3e6401 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1554,7 +1554,7 @@ jobs: # Use the BuildNum to update the cache key so that the # coverage cache is always updated - save_cache: - key: v7-server-tests-coverage-{{ .BuildNum }} + key: v8-server-tests-coverage-{{ .BuildNum }} paths: - ~/transcom/mymove/tmp/baseline-go-coverage when: always @@ -1684,7 +1684,7 @@ jobs: # Use the BuildNum to update the cache key so that the # coverage cache is always updated - save_cache: - key: v5-client-tests-coverage-{{ .BuildNum }} + key: v6-client-tests-coverage-{{ .BuildNum }} paths: - ~/transcom/mymove/tmp/baseline-jest-coverage when: always diff --git a/Dockerfile.e2e b/Dockerfile.e2e index d161f38e33d..c88d243eb48 100644 --- a/Dockerfile.e2e +++ b/Dockerfile.e2e @@ -1,4 +1,4 @@ -FROM alpine:3.20.2 +FROM alpine:3.20.3 # hadolint ignore=DL3017 RUN apk upgrade --no-cache busybox diff --git a/Dockerfile.migrations b/Dockerfile.migrations index c3852e777b4..b4187431cd2 100644 --- a/Dockerfile.migrations +++ b/Dockerfile.migrations @@ -1,4 +1,4 @@ -FROM alpine:3.20.2 +FROM alpine:3.20.3 # hadolint ignore=DL3017 RUN apk upgrade --no-cache busybox diff --git a/Dockerfile.migrations_local b/Dockerfile.migrations_local index 004eb905476..18cf05fdc22 100644 --- a/Dockerfile.migrations_local +++ b/Dockerfile.migrations_local @@ -18,7 +18,7 @@ RUN rm -f bin/milmove && make bin/milmove # FINAL # ######### -FROM alpine:3.20.2 +FROM alpine:3.20.3 # hadolint ignore=DL3017 RUN apk upgrade --no-cache busybox diff --git a/Dockerfile.reviewapp b/Dockerfile.reviewapp index 011bdc6d4dd..99fbf45409f 100644 --- a/Dockerfile.reviewapp +++ b/Dockerfile.reviewapp @@ -45,7 +45,7 @@ RUN set -x \ && make bin/generate-test-data # define migrations before client build since it doesn't need client -FROM alpine:3.20.2 as migrate +FROM alpine:3.20.3 as migrate COPY --from=server_builder /build/bin/rds-ca-2019-root.pem /bin/rds-ca-2019-root.pem COPY --from=server_builder /build/bin/milmove /bin/milmove diff --git a/Dockerfile.tools b/Dockerfile.tools index c3fd78134b6..97fe065b9d3 100644 --- a/Dockerfile.tools +++ b/Dockerfile.tools @@ -1,4 +1,4 @@ -FROM alpine:3.20.2 +FROM alpine:3.20.3 # hadolint ignore=DL3017 RUN apk upgrade --no-cache busybox diff --git a/Dockerfile.tools_local b/Dockerfile.tools_local index 57edc1744da..8b724400d9e 100644 --- a/Dockerfile.tools_local +++ b/Dockerfile.tools_local @@ -18,7 +18,7 @@ RUN rm -f bin/prime-api-client && make bin/prime-api-client # FINAL # ######### -FROM alpine:3.20.2 +FROM alpine:3.20.3 # hadolint ignore=DL3017 RUN apk upgrade --no-cache busybox diff --git a/README.md b/README.md index b80b7b38a2c..78e795bb938 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,8 @@ [![GoDoc](https://godoc.org/github.com/transcom/mymove?status.svg)](https://godoc.org/github.com/transcom/mymove) -This repository contains the application source code for the Personal Property Prototype, a possible next generation version of the Defense Personal Property System (DPS). DPS is an online system managed by the U.S. [Department of Defense](https://www.defense.gov/) (DoD) [Transportation Command](http://www.ustranscom.mil/) (USTRANSCOM) and is used by service members and their families to manage household goods moves. +This repository contains the application source code for the MilMove application, the next generation version of the Defense Personal Property System (DPS). DPS is an online system managed by the U.S. [Department of Defense](https://www.defense.gov/) (DoD) [Transportation Command](http://www.ustranscom.mil/) (USTRANSCOM) and is used by service members and their families to manage household goods moves. -This prototype was built by a [Defense Digital Service](https://www.dds.mil/) team in support of USTRANSCOM's mission. ## License Information diff --git a/migrations/app/migrations_manifest.txt b/migrations/app/migrations_manifest.txt index 14b898bc6e1..b3215527673 100644 --- a/migrations/app/migrations_manifest.txt +++ b/migrations/app/migrations_manifest.txt @@ -994,3 +994,7 @@ 20240819164156_update_pws_violations_pt3.up.sql 20240820125856_allow_pptas_migration.up.sql 20240820151043_add_gsr_role.up.sql +20240822180409_adding_locked_price_cents_service_param.up.sql +20240909194514_pricing_unpriced_ms_cs_service_items.up.sql +20240910021542_populating_locked_price_cents_from_pricing_estimate_for_ms_and_cs_service_items.up.sql +20240917132411_update_provides_ppm_closeout_transportation_offices.up.sql diff --git a/migrations/app/schema/20240822180409_adding_locked_price_cents_service_param.up.sql b/migrations/app/schema/20240822180409_adding_locked_price_cents_service_param.up.sql new file mode 100644 index 00000000000..baa1337fac5 --- /dev/null +++ b/migrations/app/schema/20240822180409_adding_locked_price_cents_service_param.up.sql @@ -0,0 +1,26 @@ +INSERT INTO service_item_param_keys +(id,key,description,type,origin,created_at,updated_at) +SELECT '7ec5cf87-a446-4dd6-89d3-50bbc0d2c206','LockedPriceCents', 'Locked price when move was made available to prime', 'INTEGER', 'SYSTEM', now(), now() +WHERE NOT EXISTS + (SELECT 1 + FROM service_item_param_keys s + WHERE s.id = '7ec5cf87-a446-4dd6-89d3-50bbc0d2c206' + ); + +INSERT INTO service_params +(id,service_id,service_item_param_key_id,created_at,updated_at,is_optional) +SELECT '22056106-bbde-4ae7-b5bd-e7d2f103ab7d',(SELECT id FROM re_services WHERE code='MS'),(SELECT id FROM service_item_param_keys where key='LockedPriceCents'), now(), now(), 'false' +WHERE NOT EXISTS + ( SELECT 1 + FROM service_params s + WHERE s.id = '22056106-bbde-4ae7-b5bd-e7d2f103ab7d' + ); + +INSERT INTO service_params +(id,service_id,service_item_param_key_id,created_at,updated_at,is_optional) +SELECT '86f8c20c-071e-4715-b0c1-608f540b3be3',(SELECT id FROM re_services WHERE code='CS'),(SELECT id FROM service_item_param_keys where key='LockedPriceCents'), now(), now(), 'false' +WHERE NOT EXISTS + ( SELECT 1 + FROM service_params s + WHERE s.id = '86f8c20c-071e-4715-b0c1-608f540b3be3' + ); \ No newline at end of file diff --git a/migrations/app/schema/20240909194514_pricing_unpriced_ms_cs_service_items.up.sql b/migrations/app/schema/20240909194514_pricing_unpriced_ms_cs_service_items.up.sql new file mode 100644 index 00000000000..de31bcc645c --- /dev/null +++ b/migrations/app/schema/20240909194514_pricing_unpriced_ms_cs_service_items.up.sql @@ -0,0 +1,29 @@ +-- Filling in pricing_estimates for unprices services code MS and CS service items. Service items should not be able to reach this state +-- but some older data exists where unpriced MS and CS items exist +SET statement_timeout = 300000; +SET lock_timeout = 300000; +SET idle_in_transaction_session_timeout = 300000; + +UPDATE mto_service_items AS ms +SET locked_price_cents = + CASE + when price_cents > 0 AND (s.code = 'MS' OR s.code = 'CS') AND ms.re_service_id = s.id then price_cents + when price_cents = 0 AND (s.code = 'MS' OR s.code = 'CS') AND ms.re_service_id = s.id then 0 + END, + pricing_estimate = + CASE + when price_cents > 0 AND (s.code = 'MS' OR s.code = 'CS') AND ms.re_service_id = s.id then price_cents + when price_cents = 0 AND (s.code = 'MS' OR s.code = 'CS') AND ms.re_service_id = s.id then 0 + END +FROM re_task_order_fees AS tf +JOIN re_services AS s +ON tf.service_id = s.id +JOIN re_contract_years AS cy +ON tf.contract_year_id = cy.id +JOIN re_contracts AS ct +ON cy.contract_id = ct.id +JOIN mto_service_items AS msi +ON s.id = msi.re_service_id +JOIN moves AS mo +ON mo.id = msi.move_id +WHERE (s.code = 'MS' OR s.code = 'CS') AND (mo.available_to_prime_at BETWEEN cy.start_date AND cy.end_date) AND ms.re_service_id = s.id AND ms.locked_price_cents is null AND ms.pricing_estimate is null; \ No newline at end of file diff --git a/migrations/app/schema/20240910021542_populating_locked_price_cents_from_pricing_estimate_for_ms_and_cs_service_items.up.sql b/migrations/app/schema/20240910021542_populating_locked_price_cents_from_pricing_estimate_for_ms_and_cs_service_items.up.sql new file mode 100644 index 00000000000..bf154feebe5 --- /dev/null +++ b/migrations/app/schema/20240910021542_populating_locked_price_cents_from_pricing_estimate_for_ms_and_cs_service_items.up.sql @@ -0,0 +1,9 @@ +-- Customer directed that the current pricing_estimate should be saved as the locked_price for MS and CS +SET statement_timeout = 300000; +SET lock_timeout = 300000; +SET idle_in_transaction_session_timeout = 300000; + +UPDATE mto_service_items AS ms +SET locked_price_cents = pricing_estimate +FROM re_services AS r +WHERE ms.re_service_id = r.id AND (r.code = 'MS' OR r.code = 'CS') \ No newline at end of file diff --git a/migrations/app/schema/20240917132411_update_provides_ppm_closeout_transportation_offices.up.sql b/migrations/app/schema/20240917132411_update_provides_ppm_closeout_transportation_offices.up.sql new file mode 100644 index 00000000000..07cc0505623 --- /dev/null +++ b/migrations/app/schema/20240917132411_update_provides_ppm_closeout_transportation_offices.up.sql @@ -0,0 +1,20 @@ +-- PPPO Fort Belvoir - USA +UPDATE transportation_offices SET provides_ppm_closeout = true WHERE id = 'a877a317-be5f-482b-a126-c91a34be9290'; + +-- Personal Property Activity HQ (PPA HQ) - USAF +UPDATE transportation_offices SET provides_ppm_closeout = false WHERE id = 'ebdacf64-353a-4014-91db-0d04d88320f0'; + +-- PPPO Base Miami - USCG +UPDATE transportation_offices SET provides_ppm_closeout = true WHERE id = '1b3e7496-efa7-48aa-ba22-b630d6fea98b'; + +-- JPPSO - North West (JEAT) - USA +UPDATE transportation_offices SET provides_ppm_closeout = false WHERE id = '5a3388e1-6d46-4639-ac8f-a8937dc26938'; + +-- PPPO NSWC Panama City Division - USN +UPDATE transportation_offices SET provides_ppm_closeout = true WHERE id = '57cf1e81-8113-4a52-bc50-3cb8902c2efd'; + +-- JPPSO - Mid Atlantic (BGAC) - USA +UPDATE transportation_offices SET provides_ppm_closeout = false WHERE id = '8e25ccc1-7891-4146-a9d0-cd0d48b59a50'; + +-- JPPSO - South West (LKNQ) - USN +UPDATE transportation_offices SET provides_ppm_closeout = false WHERE id = '27002d34-e9ea-4ef5-a086-f23d07c4088c'; \ No newline at end of file diff --git a/package.json b/package.json index 8670451fad2..117476ac088 100644 --- a/package.json +++ b/package.json @@ -112,7 +112,7 @@ "test": "react-app-rewired test --env=jsdom", "test:debug": "react-app-rewired --inspect-brk test --runInBand --env=jsdom", "test:coverage": "react-app-rewired test --coverage --reporters=default --reporters=jest-junit --env=jsdom --coverageDirectory=coverage --maxWorkers=4 --watchAll=false", - "test:e2e": "playwright test --trace=on", + "test:e2e": "A11Y_AUDIT=true playwright test --trace=on", "prettier": "prettier --write --loglevel warn 'src/**/*.{js,jsx}' playwright/tests/**/*.js'", "prettier-ci": "prettier --check 'src/**/*.{js,jsx}' playwright/tests/**/*.js", "lint": "eslint --ext .js,.jsx --max-warnings=0 src playwright/tests", diff --git a/pkg/factory/mto_service_item_factory.go b/pkg/factory/mto_service_item_factory.go index 2ec6548b02b..c39d7680d4b 100644 --- a/pkg/factory/mto_service_item_factory.go +++ b/pkg/factory/mto_service_item_factory.go @@ -9,6 +9,7 @@ import ( "github.com/transcom/mymove/pkg/models" "github.com/transcom/mymove/pkg/testdatagen" + "github.com/transcom/mymove/pkg/unit" ) type mtoServiceItemBuildType byte @@ -56,6 +57,8 @@ func buildMTOServiceItemWithBuildType(db *pop.Connection, customs []Customizatio requestedApprovalsRequestedStatus := false + var lockedPriceCents = unit.Cents(12303) + // Create default MTOServiceItem mtoServiceItem := models.MTOServiceItem{ MoveTaskOrder: move, @@ -67,6 +70,7 @@ func buildMTOServiceItemWithBuildType(db *pop.Connection, customs []Customizatio Status: models.MTOServiceItemStatusSubmitted, RequestedApprovalsRequestedStatus: &requestedApprovalsRequestedStatus, CustomerExpense: isCustomerExpense, + LockedPriceCents: &lockedPriceCents, } // only set SITOriginHHGOriginalAddress if a customization is provided @@ -341,15 +345,23 @@ var ( Type: models.ServiceItemParamTypeString, Origin: models.ServiceItemParamOriginPrime, } + paramLockedPriceCents = models.ServiceItemParamKey{ + Key: models.ServiceItemParamNameLockedPriceCents, + Description: "locked price cents", + Type: models.ServiceItemParamTypeInteger, + Origin: models.ServiceItemParamOriginSystem, + } fixtureServiceItemParamsMap = map[models.ReServiceCode]models.ServiceItemParamKeys{ models.ReServiceCodeCS: { - paramContractCode, paramMTOAvailableAToPrimeAt, + paramContractCode, + paramLockedPriceCents, paramPriceRateOrFactor, }, models.ReServiceCodeMS: { - paramContractCode, paramMTOAvailableAToPrimeAt, + paramContractCode, + paramLockedPriceCents, paramPriceRateOrFactor, }, models.ReServiceCodeDLH: { diff --git a/pkg/gen/ghcapi/configure_mymove.go b/pkg/gen/ghcapi/configure_mymove.go index 58c12bae385..dc17dbeda82 100644 --- a/pkg/gen/ghcapi/configure_mymove.go +++ b/pkg/gen/ghcapi/configure_mymove.go @@ -95,6 +95,11 @@ func configureAPI(api *ghcoperations.MymoveAPI) http.Handler { return middleware.NotImplemented("operation report_violations.AssociateReportViolations has not yet been implemented") }) } + if api.PaymentRequestsBulkDownloadHandler == nil { + api.PaymentRequestsBulkDownloadHandler = payment_requests.BulkDownloadHandlerFunc(func(params payment_requests.BulkDownloadParams) middleware.Responder { + return middleware.NotImplemented("operation payment_requests.BulkDownload has not yet been implemented") + }) + } if api.OrderCounselingUpdateAllowanceHandler == nil { api.OrderCounselingUpdateAllowanceHandler = order.CounselingUpdateAllowanceHandlerFunc(func(params order.CounselingUpdateAllowanceParams) middleware.Responder { return middleware.NotImplemented("operation order.CounselingUpdateAllowance has not yet been implemented") diff --git a/pkg/gen/ghcapi/embedded_spec.go b/pkg/gen/ghcapi/embedded_spec.go index 8325d7c1a82..2916c94cc19 100644 --- a/pkg/gen/ghcapi/embedded_spec.go +++ b/pkg/gen/ghcapi/embedded_spec.go @@ -3226,6 +3226,49 @@ func init() { } ] }, + "/payment-requests/{paymentRequestID}/bulkDownload": { + "get": { + "description": "This endpoint downloads all uploaded payment request documentation combined into a single PDF.\n", + "produces": [ + "application/pdf" + ], + "tags": [ + "paymentRequests" + ], + "summary": "Downloads all Payment Request documents as a PDF", + "operationId": "bulkDownload", + "responses": { + "200": { + "description": "Payment Request Files PDF", + "schema": { + "type": "file", + "format": "binary" + }, + "headers": { + "Content-Disposition": { + "type": "string", + "description": "File name to download" + } + } + }, + "400": { + "$ref": "#/responses/InvalidRequest" + }, + "500": { + "$ref": "#/responses/ServerError" + } + } + }, + "parameters": [ + { + "type": "string", + "description": "the id for the payment-request with files to be downloaded", + "name": "paymentRequestID", + "in": "path", + "required": true + } + ] + }, "/payment-requests/{paymentRequestID}/shipments-payment-sit-balance": { "get": { "description": "Returns all shipment payment request SIT usage to support partial SIT invoicing", @@ -5666,14 +5709,14 @@ func init() { }, "/transportation-offices": { "get": { - "description": "Returns the transportation offices matching the search query", + "description": "Returns the transportation offices matching the search query that is enabled for PPM closeout", "produces": [ "application/json" ], "tags": [ "transportationOffice" ], - "summary": "Returns the transportation offices matching the search query", + "summary": "Returns the transportation offices matching the search query that is enabled for PPM closeout", "operationId": "getTransportationOffices", "parameters": [ { @@ -6632,8 +6675,9 @@ func init() { }, "edipi": { "type": "string", - "x-nullable": true, - "example": "John" + "maxLength": 10, + "x-nullable": false, + "example": "1234567890" }, "emailIsPreferred": { "type": "boolean" @@ -7233,10 +7277,10 @@ func init() { "current_address": { "$ref": "#/definitions/Address" }, - "dodID": { + "eTag": { "type": "string" }, - "eTag": { + "edipi": { "type": "string" }, "email": { @@ -12317,7 +12361,8 @@ func init() { "ZipSITOriginHHGOriginalAddress", "StandaloneCrate", "StandaloneCrateCap", - "UncappedRequestTotal" + "UncappedRequestTotal", + "LockedPriceCents" ] }, "ServiceItemParamOrigin": { @@ -18012,6 +18057,55 @@ func init() { } ] }, + "/payment-requests/{paymentRequestID}/bulkDownload": { + "get": { + "description": "This endpoint downloads all uploaded payment request documentation combined into a single PDF.\n", + "produces": [ + "application/pdf" + ], + "tags": [ + "paymentRequests" + ], + "summary": "Downloads all Payment Request documents as a PDF", + "operationId": "bulkDownload", + "responses": { + "200": { + "description": "Payment Request Files PDF", + "schema": { + "type": "file", + "format": "binary" + }, + "headers": { + "Content-Disposition": { + "type": "string", + "description": "File name to download" + } + } + }, + "400": { + "description": "The request payload is invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "500": { + "description": "A server error occurred", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + }, + "parameters": [ + { + "type": "string", + "description": "the id for the payment-request with files to be downloaded", + "name": "paymentRequestID", + "in": "path", + "required": true + } + ] + }, "/payment-requests/{paymentRequestID}/shipments-payment-sit-balance": { "get": { "description": "Returns all shipment payment request SIT usage to support partial SIT invoicing", @@ -21066,14 +21160,14 @@ func init() { }, "/transportation-offices": { "get": { - "description": "Returns the transportation offices matching the search query", + "description": "Returns the transportation offices matching the search query that is enabled for PPM closeout", "produces": [ "application/json" ], "tags": [ "transportationOffice" ], - "summary": "Returns the transportation offices matching the search query", + "summary": "Returns the transportation offices matching the search query that is enabled for PPM closeout", "operationId": "getTransportationOffices", "parameters": [ { @@ -22082,8 +22176,9 @@ func init() { }, "edipi": { "type": "string", - "x-nullable": true, - "example": "John" + "maxLength": 10, + "x-nullable": false, + "example": "1234567890" }, "emailIsPreferred": { "type": "boolean" @@ -22683,10 +22778,10 @@ func init() { "current_address": { "$ref": "#/definitions/Address" }, - "dodID": { + "eTag": { "type": "string" }, - "eTag": { + "edipi": { "type": "string" }, "email": { @@ -27892,7 +27987,8 @@ func init() { "ZipSITOriginHHGOriginalAddress", "StandaloneCrate", "StandaloneCrateCap", - "UncappedRequestTotal" + "UncappedRequestTotal", + "LockedPriceCents" ] }, "ServiceItemParamOrigin": { diff --git a/pkg/gen/ghcapi/ghcoperations/mymove_api.go b/pkg/gen/ghcapi/ghcoperations/mymove_api.go index fe82a22c37f..e9eda51b651 100644 --- a/pkg/gen/ghcapi/ghcoperations/mymove_api.go +++ b/pkg/gen/ghcapi/ghcoperations/mymove_api.go @@ -84,6 +84,9 @@ func NewMymoveAPI(spec *loads.Document) *MymoveAPI { ReportViolationsAssociateReportViolationsHandler: report_violations.AssociateReportViolationsHandlerFunc(func(params report_violations.AssociateReportViolationsParams) middleware.Responder { return middleware.NotImplemented("operation report_violations.AssociateReportViolations has not yet been implemented") }), + PaymentRequestsBulkDownloadHandler: payment_requests.BulkDownloadHandlerFunc(func(params payment_requests.BulkDownloadParams) middleware.Responder { + return middleware.NotImplemented("operation payment_requests.BulkDownload has not yet been implemented") + }), OrderCounselingUpdateAllowanceHandler: order.CounselingUpdateAllowanceHandlerFunc(func(params order.CounselingUpdateAllowanceParams) middleware.Responder { return middleware.NotImplemented("operation order.CounselingUpdateAllowance has not yet been implemented") }), @@ -419,6 +422,8 @@ type MymoveAPI struct { ShipmentApproveShipmentDiversionHandler shipment.ApproveShipmentDiversionHandler // ReportViolationsAssociateReportViolationsHandler sets the operation handler for the associate report violations operation ReportViolationsAssociateReportViolationsHandler report_violations.AssociateReportViolationsHandler + // PaymentRequestsBulkDownloadHandler sets the operation handler for the bulk download operation + PaymentRequestsBulkDownloadHandler payment_requests.BulkDownloadHandler // OrderCounselingUpdateAllowanceHandler sets the operation handler for the counseling update allowance operation OrderCounselingUpdateAllowanceHandler order.CounselingUpdateAllowanceHandler // OrderCounselingUpdateOrderHandler sets the operation handler for the counseling update order operation @@ -703,6 +708,9 @@ func (o *MymoveAPI) Validate() error { if o.ReportViolationsAssociateReportViolationsHandler == nil { unregistered = append(unregistered, "report_violations.AssociateReportViolationsHandler") } + if o.PaymentRequestsBulkDownloadHandler == nil { + unregistered = append(unregistered, "payment_requests.BulkDownloadHandler") + } if o.OrderCounselingUpdateAllowanceHandler == nil { unregistered = append(unregistered, "order.CounselingUpdateAllowanceHandler") } @@ -1094,6 +1102,10 @@ func (o *MymoveAPI) initHandlerCache() { o.handlers["POST"] = make(map[string]http.Handler) } o.handlers["POST"]["/report-violations/{reportID}"] = report_violations.NewAssociateReportViolations(o.context, o.ReportViolationsAssociateReportViolationsHandler) + if o.handlers["GET"] == nil { + o.handlers["GET"] = make(map[string]http.Handler) + } + o.handlers["GET"]["/payment-requests/{paymentRequestID}/bulkDownload"] = payment_requests.NewBulkDownload(o.context, o.PaymentRequestsBulkDownloadHandler) if o.handlers["PATCH"] == nil { o.handlers["PATCH"] = make(map[string]http.Handler) } diff --git a/pkg/gen/ghcapi/ghcoperations/payment_requests/bulk_download.go b/pkg/gen/ghcapi/ghcoperations/payment_requests/bulk_download.go new file mode 100644 index 00000000000..78f7901ab05 --- /dev/null +++ b/pkg/gen/ghcapi/ghcoperations/payment_requests/bulk_download.go @@ -0,0 +1,58 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package payment_requests + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime/middleware" +) + +// BulkDownloadHandlerFunc turns a function with the right signature into a bulk download handler +type BulkDownloadHandlerFunc func(BulkDownloadParams) middleware.Responder + +// Handle executing the request and returning a response +func (fn BulkDownloadHandlerFunc) Handle(params BulkDownloadParams) middleware.Responder { + return fn(params) +} + +// BulkDownloadHandler interface for that can handle valid bulk download params +type BulkDownloadHandler interface { + Handle(BulkDownloadParams) middleware.Responder +} + +// NewBulkDownload creates a new http.Handler for the bulk download operation +func NewBulkDownload(ctx *middleware.Context, handler BulkDownloadHandler) *BulkDownload { + return &BulkDownload{Context: ctx, Handler: handler} +} + +/* + BulkDownload swagger:route GET /payment-requests/{paymentRequestID}/bulkDownload paymentRequests bulkDownload + +# Downloads all Payment Request documents as a PDF + +This endpoint downloads all uploaded payment request documentation combined into a single PDF. +*/ +type BulkDownload struct { + Context *middleware.Context + Handler BulkDownloadHandler +} + +func (o *BulkDownload) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + route, rCtx, _ := o.Context.RouteInfo(r) + if rCtx != nil { + *r = *rCtx + } + var Params = NewBulkDownloadParams() + if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params + o.Context.Respond(rw, r, route.Produces, route, err) + return + } + + res := o.Handler.Handle(Params) // actually handle the request + o.Context.Respond(rw, r, route.Produces, route, res) + +} diff --git a/pkg/gen/ghcapi/ghcoperations/payment_requests/bulk_download_parameters.go b/pkg/gen/ghcapi/ghcoperations/payment_requests/bulk_download_parameters.go new file mode 100644 index 00000000000..ff67f05c88a --- /dev/null +++ b/pkg/gen/ghcapi/ghcoperations/payment_requests/bulk_download_parameters.go @@ -0,0 +1,71 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package payment_requests + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime/middleware" + "github.com/go-openapi/strfmt" +) + +// NewBulkDownloadParams creates a new BulkDownloadParams object +// +// There are no default values defined in the spec. +func NewBulkDownloadParams() BulkDownloadParams { + + return BulkDownloadParams{} +} + +// BulkDownloadParams contains all the bound params for the bulk download operation +// typically these are obtained from a http.Request +// +// swagger:parameters bulkDownload +type BulkDownloadParams struct { + + // HTTP Request Object + HTTPRequest *http.Request `json:"-"` + + /*the id for the payment-request with files to be downloaded + Required: true + In: path + */ + PaymentRequestID string +} + +// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface +// for simple values it will use straight method calls. +// +// To ensure default values, the struct must have been initialized with NewBulkDownloadParams() beforehand. +func (o *BulkDownloadParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error { + var res []error + + o.HTTPRequest = r + + rPaymentRequestID, rhkPaymentRequestID, _ := route.Params.GetOK("paymentRequestID") + if err := o.bindPaymentRequestID(rPaymentRequestID, rhkPaymentRequestID, route.Formats); err != nil { + res = append(res, err) + } + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +// bindPaymentRequestID binds and validates parameter PaymentRequestID from path. +func (o *BulkDownloadParams) bindPaymentRequestID(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: true + // Parameter is provided by construction from the route + o.PaymentRequestID = raw + + return nil +} diff --git a/pkg/gen/ghcapi/ghcoperations/payment_requests/bulk_download_responses.go b/pkg/gen/ghcapi/ghcoperations/payment_requests/bulk_download_responses.go new file mode 100644 index 00000000000..b00eca8e5c1 --- /dev/null +++ b/pkg/gen/ghcapi/ghcoperations/payment_requests/bulk_download_responses.go @@ -0,0 +1,170 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package payment_requests + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "io" + "net/http" + + "github.com/go-openapi/runtime" + + "github.com/transcom/mymove/pkg/gen/ghcmessages" +) + +// BulkDownloadOKCode is the HTTP code returned for type BulkDownloadOK +const BulkDownloadOKCode int = 200 + +/* +BulkDownloadOK Payment Request Files PDF + +swagger:response bulkDownloadOK +*/ +type BulkDownloadOK struct { + /*File name to download + + */ + ContentDisposition string `json:"Content-Disposition"` + + /* + In: Body + */ + Payload io.ReadCloser `json:"body,omitempty"` +} + +// NewBulkDownloadOK creates BulkDownloadOK with default headers values +func NewBulkDownloadOK() *BulkDownloadOK { + + return &BulkDownloadOK{} +} + +// WithContentDisposition adds the contentDisposition to the bulk download o k response +func (o *BulkDownloadOK) WithContentDisposition(contentDisposition string) *BulkDownloadOK { + o.ContentDisposition = contentDisposition + return o +} + +// SetContentDisposition sets the contentDisposition to the bulk download o k response +func (o *BulkDownloadOK) SetContentDisposition(contentDisposition string) { + o.ContentDisposition = contentDisposition +} + +// WithPayload adds the payload to the bulk download o k response +func (o *BulkDownloadOK) WithPayload(payload io.ReadCloser) *BulkDownloadOK { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the bulk download o k response +func (o *BulkDownloadOK) SetPayload(payload io.ReadCloser) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *BulkDownloadOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + // response header Content-Disposition + + contentDisposition := o.ContentDisposition + if contentDisposition != "" { + rw.Header().Set("Content-Disposition", contentDisposition) + } + + rw.WriteHeader(200) + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } +} + +// BulkDownloadBadRequestCode is the HTTP code returned for type BulkDownloadBadRequest +const BulkDownloadBadRequestCode int = 400 + +/* +BulkDownloadBadRequest The request payload is invalid + +swagger:response bulkDownloadBadRequest +*/ +type BulkDownloadBadRequest struct { + + /* + In: Body + */ + Payload *ghcmessages.Error `json:"body,omitempty"` +} + +// NewBulkDownloadBadRequest creates BulkDownloadBadRequest with default headers values +func NewBulkDownloadBadRequest() *BulkDownloadBadRequest { + + return &BulkDownloadBadRequest{} +} + +// WithPayload adds the payload to the bulk download bad request response +func (o *BulkDownloadBadRequest) WithPayload(payload *ghcmessages.Error) *BulkDownloadBadRequest { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the bulk download bad request response +func (o *BulkDownloadBadRequest) SetPayload(payload *ghcmessages.Error) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *BulkDownloadBadRequest) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(400) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} + +// BulkDownloadInternalServerErrorCode is the HTTP code returned for type BulkDownloadInternalServerError +const BulkDownloadInternalServerErrorCode int = 500 + +/* +BulkDownloadInternalServerError A server error occurred + +swagger:response bulkDownloadInternalServerError +*/ +type BulkDownloadInternalServerError struct { + + /* + In: Body + */ + Payload *ghcmessages.Error `json:"body,omitempty"` +} + +// NewBulkDownloadInternalServerError creates BulkDownloadInternalServerError with default headers values +func NewBulkDownloadInternalServerError() *BulkDownloadInternalServerError { + + return &BulkDownloadInternalServerError{} +} + +// WithPayload adds the payload to the bulk download internal server error response +func (o *BulkDownloadInternalServerError) WithPayload(payload *ghcmessages.Error) *BulkDownloadInternalServerError { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the bulk download internal server error response +func (o *BulkDownloadInternalServerError) SetPayload(payload *ghcmessages.Error) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *BulkDownloadInternalServerError) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(500) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} diff --git a/pkg/gen/ghcapi/ghcoperations/payment_requests/bulk_download_urlbuilder.go b/pkg/gen/ghcapi/ghcoperations/payment_requests/bulk_download_urlbuilder.go new file mode 100644 index 00000000000..7f67d894191 --- /dev/null +++ b/pkg/gen/ghcapi/ghcoperations/payment_requests/bulk_download_urlbuilder.go @@ -0,0 +1,99 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package payment_requests + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "errors" + "net/url" + golangswaggerpaths "path" + "strings" +) + +// BulkDownloadURL generates an URL for the bulk download operation +type BulkDownloadURL struct { + PaymentRequestID string + + _basePath string + // avoid unkeyed usage + _ struct{} +} + +// WithBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *BulkDownloadURL) WithBasePath(bp string) *BulkDownloadURL { + o.SetBasePath(bp) + return o +} + +// SetBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *BulkDownloadURL) SetBasePath(bp string) { + o._basePath = bp +} + +// Build a url path and query string +func (o *BulkDownloadURL) Build() (*url.URL, error) { + var _result url.URL + + var _path = "/payment-requests/{paymentRequestID}/bulkDownload" + + paymentRequestID := o.PaymentRequestID + if paymentRequestID != "" { + _path = strings.Replace(_path, "{paymentRequestID}", paymentRequestID, -1) + } else { + return nil, errors.New("paymentRequestId is required on BulkDownloadURL") + } + + _basePath := o._basePath + if _basePath == "" { + _basePath = "/ghc/v1" + } + _result.Path = golangswaggerpaths.Join(_basePath, _path) + + return &_result, nil +} + +// Must is a helper function to panic when the url builder returns an error +func (o *BulkDownloadURL) Must(u *url.URL, err error) *url.URL { + if err != nil { + panic(err) + } + if u == nil { + panic("url can't be nil") + } + return u +} + +// String returns the string representation of the path with query string +func (o *BulkDownloadURL) String() string { + return o.Must(o.Build()).String() +} + +// BuildFull builds a full url with scheme, host, path and query string +func (o *BulkDownloadURL) BuildFull(scheme, host string) (*url.URL, error) { + if scheme == "" { + return nil, errors.New("scheme is required for a full url on BulkDownloadURL") + } + if host == "" { + return nil, errors.New("host is required for a full url on BulkDownloadURL") + } + + base, err := o.Build() + if err != nil { + return nil, err + } + + base.Scheme = scheme + base.Host = host + return base, nil +} + +// StringFull returns the string representation of a complete url +func (o *BulkDownloadURL) StringFull(scheme, host string) string { + return o.Must(o.BuildFull(scheme, host)).String() +} diff --git a/pkg/gen/ghcapi/ghcoperations/transportation_office/get_transportation_offices.go b/pkg/gen/ghcapi/ghcoperations/transportation_office/get_transportation_offices.go index 13c260a5655..fd19356220f 100644 --- a/pkg/gen/ghcapi/ghcoperations/transportation_office/get_transportation_offices.go +++ b/pkg/gen/ghcapi/ghcoperations/transportation_office/get_transportation_offices.go @@ -32,9 +32,9 @@ func NewGetTransportationOffices(ctx *middleware.Context, handler GetTransportat /* GetTransportationOffices swagger:route GET /transportation-offices transportationOffice getTransportationOffices -# Returns the transportation offices matching the search query +# Returns the transportation offices matching the search query that is enabled for PPM closeout -Returns the transportation offices matching the search query +Returns the transportation offices matching the search query that is enabled for PPM closeout */ type GetTransportationOffices struct { Context *middleware.Context diff --git a/pkg/gen/ghcmessages/create_customer_payload.go b/pkg/gen/ghcmessages/create_customer_payload.go index d9d28d6c570..68ece54fb88 100644 --- a/pkg/gen/ghcmessages/create_customer_payload.go +++ b/pkg/gen/ghcmessages/create_customer_payload.go @@ -37,8 +37,9 @@ type CreateCustomerPayload struct { CreateOktaAccount bool `json:"createOktaAccount,omitempty"` // edipi - // Example: John - Edipi *string `json:"edipi,omitempty"` + // Example: 1234567890 + // Max Length: 10 + Edipi string `json:"edipi,omitempty"` // email is preferred EmailIsPreferred bool `json:"emailIsPreferred,omitempty"` @@ -102,6 +103,10 @@ func (m *CreateCustomerPayload) Validate(formats strfmt.Registry) error { res = append(res, err) } + if err := m.validateEdipi(formats); err != nil { + res = append(res, err) + } + if err := m.validateEmplid(formats); err != nil { res = append(res, err) } @@ -174,6 +179,18 @@ func (m *CreateCustomerPayload) validateBackupMailingAddress(formats strfmt.Regi return nil } +func (m *CreateCustomerPayload) validateEdipi(formats strfmt.Registry) error { + if swag.IsZero(m.Edipi) { // not required + return nil + } + + if err := validate.MaxLength("edipi", "body", m.Edipi, 10); err != nil { + return err + } + + return nil +} + func (m *CreateCustomerPayload) validateEmplid(formats strfmt.Registry) error { if swag.IsZero(m.Emplid) { // not required return nil diff --git a/pkg/gen/ghcmessages/customer.go b/pkg/gen/ghcmessages/customer.go index 2bac3e7d0b3..c4034e3cfe2 100644 --- a/pkg/gen/ghcmessages/customer.go +++ b/pkg/gen/ghcmessages/customer.go @@ -34,12 +34,12 @@ type Customer struct { // current address CurrentAddress *Address `json:"current_address,omitempty"` - // dod ID - DodID string `json:"dodID,omitempty"` - // e tag ETag string `json:"eTag,omitempty"` + // edipi + Edipi string `json:"edipi,omitempty"` + // email // Pattern: ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ Email *string `json:"email,omitempty"` diff --git a/pkg/gen/ghcmessages/service_item_param_name.go b/pkg/gen/ghcmessages/service_item_param_name.go index dd10c8003cd..eff0f3d2734 100644 --- a/pkg/gen/ghcmessages/service_item_param_name.go +++ b/pkg/gen/ghcmessages/service_item_param_name.go @@ -236,6 +236,9 @@ const ( // ServiceItemParamNameUncappedRequestTotal captures enum value "UncappedRequestTotal" ServiceItemParamNameUncappedRequestTotal ServiceItemParamName = "UncappedRequestTotal" + + // ServiceItemParamNameLockedPriceCents captures enum value "LockedPriceCents" + ServiceItemParamNameLockedPriceCents ServiceItemParamName = "LockedPriceCents" ) // for schema @@ -243,7 +246,7 @@ var serviceItemParamNameEnum []interface{} func init() { var res []ServiceItemParamName - if err := json.Unmarshal([]byte(`["ActualPickupDate","ContractCode","ContractYearName","CubicFeetBilled","CubicFeetCrating","DimensionHeight","DimensionLength","DimensionWidth","DistanceZip","DistanceZipSITDest","DistanceZipSITOrigin","EIAFuelPrice","EscalationCompounded","FSCMultiplier","FSCPriceDifferenceInCents","FSCWeightBasedDistanceMultiplier","IsPeak","MarketDest","MarketOrigin","MTOAvailableToPrimeAt","NTSPackingFactor","NumberDaysSIT","PriceAreaDest","PriceAreaIntlDest","PriceAreaIntlOrigin","PriceAreaOrigin","PriceRateOrFactor","PSI_LinehaulDom","PSI_LinehaulDomPrice","PSI_LinehaulShort","PSI_LinehaulShortPrice","PSI_PriceDomDest","PSI_PriceDomDestPrice","PSI_PriceDomOrigin","PSI_PriceDomOriginPrice","PSI_ShippingLinehaulIntlCO","PSI_ShippingLinehaulIntlCOPrice","PSI_ShippingLinehaulIntlOC","PSI_ShippingLinehaulIntlOCPrice","PSI_ShippingLinehaulIntlOO","PSI_ShippingLinehaulIntlOOPrice","RateAreaNonStdDest","RateAreaNonStdOrigin","ReferenceDate","RequestedPickupDate","ServiceAreaDest","ServiceAreaOrigin","ServicesScheduleDest","ServicesScheduleOrigin","SITPaymentRequestEnd","SITPaymentRequestStart","SITScheduleDest","SITScheduleOrigin","SITServiceAreaDest","SITServiceAreaOrigin","WeightAdjusted","WeightBilled","WeightEstimated","WeightOriginal","WeightReweigh","ZipDestAddress","ZipPickupAddress","ZipSITDestHHGFinalAddress","ZipSITDestHHGOriginalAddress","ZipSITOriginHHGActualAddress","ZipSITOriginHHGOriginalAddress","StandaloneCrate","StandaloneCrateCap","UncappedRequestTotal"]`), &res); err != nil { + if err := json.Unmarshal([]byte(`["ActualPickupDate","ContractCode","ContractYearName","CubicFeetBilled","CubicFeetCrating","DimensionHeight","DimensionLength","DimensionWidth","DistanceZip","DistanceZipSITDest","DistanceZipSITOrigin","EIAFuelPrice","EscalationCompounded","FSCMultiplier","FSCPriceDifferenceInCents","FSCWeightBasedDistanceMultiplier","IsPeak","MarketDest","MarketOrigin","MTOAvailableToPrimeAt","NTSPackingFactor","NumberDaysSIT","PriceAreaDest","PriceAreaIntlDest","PriceAreaIntlOrigin","PriceAreaOrigin","PriceRateOrFactor","PSI_LinehaulDom","PSI_LinehaulDomPrice","PSI_LinehaulShort","PSI_LinehaulShortPrice","PSI_PriceDomDest","PSI_PriceDomDestPrice","PSI_PriceDomOrigin","PSI_PriceDomOriginPrice","PSI_ShippingLinehaulIntlCO","PSI_ShippingLinehaulIntlCOPrice","PSI_ShippingLinehaulIntlOC","PSI_ShippingLinehaulIntlOCPrice","PSI_ShippingLinehaulIntlOO","PSI_ShippingLinehaulIntlOOPrice","RateAreaNonStdDest","RateAreaNonStdOrigin","ReferenceDate","RequestedPickupDate","ServiceAreaDest","ServiceAreaOrigin","ServicesScheduleDest","ServicesScheduleOrigin","SITPaymentRequestEnd","SITPaymentRequestStart","SITScheduleDest","SITScheduleOrigin","SITServiceAreaDest","SITServiceAreaOrigin","WeightAdjusted","WeightBilled","WeightEstimated","WeightOriginal","WeightReweigh","ZipDestAddress","ZipPickupAddress","ZipSITDestHHGFinalAddress","ZipSITDestHHGOriginalAddress","ZipSITOriginHHGActualAddress","ZipSITOriginHHGOriginalAddress","StandaloneCrate","StandaloneCrateCap","UncappedRequestTotal","LockedPriceCents"]`), &res); err != nil { panic(err) } for _, v := range res { diff --git a/pkg/gen/internalapi/embedded_spec.go b/pkg/gen/internalapi/embedded_spec.go index 2c2fae1b7fd..6f380caa91a 100644 --- a/pkg/gen/internalapi/embedded_spec.go +++ b/pkg/gen/internalapi/embedded_spec.go @@ -3987,7 +3987,7 @@ func init() { "type": "string", "format": "telephone", "title": "Alternate phone", - "pattern": "^[2-9]\\d{2}-\\d{3}-\\d{4}$", + "pattern": "^([2-9]\\d{2}-\\d{3}-\\d{4})?$", "x-nullable": true, "example": "212-555-5555" }, @@ -6430,7 +6430,7 @@ func init() { "type": "string", "format": "telephone", "title": "Alternate Phone", - "pattern": "^[2-9]\\d{2}-\\d{3}-\\d{4}$", + "pattern": "^([2-9]\\d{2}-\\d{3}-\\d{4})?$", "x-nullable": true, "example": "212-555-5555" }, @@ -6906,7 +6906,7 @@ func init() { "type": "string", "format": "telephone", "title": "Secondary Phone", - "pattern": "^[2-9]\\d{2}-\\d{3}-\\d{4}$", + "pattern": "^([2-9]\\d{2}-\\d{3}-\\d{4})?$", "x-nullable": true, "example": "212-555-5555" }, @@ -12589,7 +12589,7 @@ func init() { "type": "string", "format": "telephone", "title": "Alternate phone", - "pattern": "^[2-9]\\d{2}-\\d{3}-\\d{4}$", + "pattern": "^([2-9]\\d{2}-\\d{3}-\\d{4})?$", "x-nullable": true, "example": "212-555-5555" }, @@ -15036,7 +15036,7 @@ func init() { "type": "string", "format": "telephone", "title": "Alternate Phone", - "pattern": "^[2-9]\\d{2}-\\d{3}-\\d{4}$", + "pattern": "^([2-9]\\d{2}-\\d{3}-\\d{4})?$", "x-nullable": true, "example": "212-555-5555" }, @@ -15514,7 +15514,7 @@ func init() { "type": "string", "format": "telephone", "title": "Secondary Phone", - "pattern": "^[2-9]\\d{2}-\\d{3}-\\d{4}$", + "pattern": "^([2-9]\\d{2}-\\d{3}-\\d{4})?$", "x-nullable": true, "example": "212-555-5555" }, diff --git a/pkg/gen/internalmessages/create_service_member_payload.go b/pkg/gen/internalmessages/create_service_member_payload.go index a2b48dce311..737d5a0a897 100644 --- a/pkg/gen/internalmessages/create_service_member_payload.go +++ b/pkg/gen/internalmessages/create_service_member_payload.go @@ -68,7 +68,7 @@ type CreateServiceMemberPayload struct { // Alternate phone // Example: 212-555-5555 - // Pattern: ^[2-9]\d{2}-\d{3}-\d{4}$ + // Pattern: ^([2-9]\d{2}-\d{3}-\d{4})?$ SecondaryTelephone *string `json:"secondary_telephone,omitempty"` // Suffix @@ -261,7 +261,7 @@ func (m *CreateServiceMemberPayload) validateSecondaryTelephone(formats strfmt.R return nil } - if err := validate.Pattern("secondary_telephone", "body", *m.SecondaryTelephone, `^[2-9]\d{2}-\d{3}-\d{4}$`); err != nil { + if err := validate.Pattern("secondary_telephone", "body", *m.SecondaryTelephone, `^([2-9]\d{2}-\d{3}-\d{4})?$`); err != nil { return err } diff --git a/pkg/gen/internalmessages/patch_service_member_payload.go b/pkg/gen/internalmessages/patch_service_member_payload.go index ec9b127dcda..a85374ad1eb 100644 --- a/pkg/gen/internalmessages/patch_service_member_payload.go +++ b/pkg/gen/internalmessages/patch_service_member_payload.go @@ -72,7 +72,7 @@ type PatchServiceMemberPayload struct { // Alternate Phone // Example: 212-555-5555 - // Pattern: ^[2-9]\d{2}-\d{3}-\d{4}$ + // Pattern: ^([2-9]\d{2}-\d{3}-\d{4})?$ SecondaryTelephone *string `json:"secondary_telephone,omitempty"` // Suffix @@ -266,7 +266,7 @@ func (m *PatchServiceMemberPayload) validateSecondaryTelephone(formats strfmt.Re return nil } - if err := validate.Pattern("secondary_telephone", "body", *m.SecondaryTelephone, `^[2-9]\d{2}-\d{3}-\d{4}$`); err != nil { + if err := validate.Pattern("secondary_telephone", "body", *m.SecondaryTelephone, `^([2-9]\d{2}-\d{3}-\d{4})?$`); err != nil { return err } diff --git a/pkg/gen/internalmessages/service_member_payload.go b/pkg/gen/internalmessages/service_member_payload.go index 463243292ff..a288cfba291 100644 --- a/pkg/gen/internalmessages/service_member_payload.go +++ b/pkg/gen/internalmessages/service_member_payload.go @@ -95,7 +95,7 @@ type ServiceMemberPayload struct { // Secondary Phone // Example: 212-555-5555 - // Pattern: ^[2-9]\d{2}-\d{3}-\d{4}$ + // Pattern: ^([2-9]\d{2}-\d{3}-\d{4})?$ SecondaryTelephone *string `json:"secondary_telephone,omitempty"` // Suffix @@ -411,7 +411,7 @@ func (m *ServiceMemberPayload) validateSecondaryTelephone(formats strfmt.Registr return nil } - if err := validate.Pattern("secondary_telephone", "body", *m.SecondaryTelephone, `^[2-9]\d{2}-\d{3}-\d{4}$`); err != nil { + if err := validate.Pattern("secondary_telephone", "body", *m.SecondaryTelephone, `^([2-9]\d{2}-\d{3}-\d{4})?$`); err != nil { return err } diff --git a/pkg/gen/primeapi/embedded_spec.go b/pkg/gen/primeapi/embedded_spec.go index 3f7c03e0d5c..07968eff65b 100644 --- a/pkg/gen/primeapi/embedded_spec.go +++ b/pkg/gen/primeapi/embedded_spec.go @@ -3599,7 +3599,8 @@ func init() { "ZipSITOriginHHGOriginalAddress", "StandaloneCrate", "StandaloneCrateCap", - "UncappedRequestTotal" + "UncappedRequestTotal", + "LockedPriceCents" ] }, "ServiceItemParamOrigin": { @@ -8293,7 +8294,8 @@ func init() { "ZipSITOriginHHGOriginalAddress", "StandaloneCrate", "StandaloneCrateCap", - "UncappedRequestTotal" + "UncappedRequestTotal", + "LockedPriceCents" ] }, "ServiceItemParamOrigin": { diff --git a/pkg/gen/primemessages/service_item_param_name.go b/pkg/gen/primemessages/service_item_param_name.go index 7f8e128b151..d43a63a69f7 100644 --- a/pkg/gen/primemessages/service_item_param_name.go +++ b/pkg/gen/primemessages/service_item_param_name.go @@ -236,6 +236,9 @@ const ( // ServiceItemParamNameUncappedRequestTotal captures enum value "UncappedRequestTotal" ServiceItemParamNameUncappedRequestTotal ServiceItemParamName = "UncappedRequestTotal" + + // ServiceItemParamNameLockedPriceCents captures enum value "LockedPriceCents" + ServiceItemParamNameLockedPriceCents ServiceItemParamName = "LockedPriceCents" ) // for schema @@ -243,7 +246,7 @@ var serviceItemParamNameEnum []interface{} func init() { var res []ServiceItemParamName - if err := json.Unmarshal([]byte(`["ActualPickupDate","ContractCode","ContractYearName","CubicFeetBilled","CubicFeetCrating","DimensionHeight","DimensionLength","DimensionWidth","DistanceZip","DistanceZipSITDest","DistanceZipSITOrigin","EIAFuelPrice","EscalationCompounded","FSCMultiplier","FSCPriceDifferenceInCents","FSCWeightBasedDistanceMultiplier","IsPeak","MarketDest","MarketOrigin","MTOAvailableToPrimeAt","NTSPackingFactor","NumberDaysSIT","PriceAreaDest","PriceAreaIntlDest","PriceAreaIntlOrigin","PriceAreaOrigin","PriceRateOrFactor","PSI_LinehaulDom","PSI_LinehaulDomPrice","PSI_LinehaulShort","PSI_LinehaulShortPrice","PSI_PriceDomDest","PSI_PriceDomDestPrice","PSI_PriceDomOrigin","PSI_PriceDomOriginPrice","PSI_ShippingLinehaulIntlCO","PSI_ShippingLinehaulIntlCOPrice","PSI_ShippingLinehaulIntlOC","PSI_ShippingLinehaulIntlOCPrice","PSI_ShippingLinehaulIntlOO","PSI_ShippingLinehaulIntlOOPrice","RateAreaNonStdDest","RateAreaNonStdOrigin","ReferenceDate","RequestedPickupDate","ServiceAreaDest","ServiceAreaOrigin","ServicesScheduleDest","ServicesScheduleOrigin","SITPaymentRequestEnd","SITPaymentRequestStart","SITScheduleDest","SITScheduleOrigin","SITServiceAreaDest","SITServiceAreaOrigin","WeightAdjusted","WeightBilled","WeightEstimated","WeightOriginal","WeightReweigh","ZipDestAddress","ZipPickupAddress","ZipSITDestHHGFinalAddress","ZipSITDestHHGOriginalAddress","ZipSITOriginHHGActualAddress","ZipSITOriginHHGOriginalAddress","StandaloneCrate","StandaloneCrateCap","UncappedRequestTotal"]`), &res); err != nil { + if err := json.Unmarshal([]byte(`["ActualPickupDate","ContractCode","ContractYearName","CubicFeetBilled","CubicFeetCrating","DimensionHeight","DimensionLength","DimensionWidth","DistanceZip","DistanceZipSITDest","DistanceZipSITOrigin","EIAFuelPrice","EscalationCompounded","FSCMultiplier","FSCPriceDifferenceInCents","FSCWeightBasedDistanceMultiplier","IsPeak","MarketDest","MarketOrigin","MTOAvailableToPrimeAt","NTSPackingFactor","NumberDaysSIT","PriceAreaDest","PriceAreaIntlDest","PriceAreaIntlOrigin","PriceAreaOrigin","PriceRateOrFactor","PSI_LinehaulDom","PSI_LinehaulDomPrice","PSI_LinehaulShort","PSI_LinehaulShortPrice","PSI_PriceDomDest","PSI_PriceDomDestPrice","PSI_PriceDomOrigin","PSI_PriceDomOriginPrice","PSI_ShippingLinehaulIntlCO","PSI_ShippingLinehaulIntlCOPrice","PSI_ShippingLinehaulIntlOC","PSI_ShippingLinehaulIntlOCPrice","PSI_ShippingLinehaulIntlOO","PSI_ShippingLinehaulIntlOOPrice","RateAreaNonStdDest","RateAreaNonStdOrigin","ReferenceDate","RequestedPickupDate","ServiceAreaDest","ServiceAreaOrigin","ServicesScheduleDest","ServicesScheduleOrigin","SITPaymentRequestEnd","SITPaymentRequestStart","SITScheduleDest","SITScheduleOrigin","SITServiceAreaDest","SITServiceAreaOrigin","WeightAdjusted","WeightBilled","WeightEstimated","WeightOriginal","WeightReweigh","ZipDestAddress","ZipPickupAddress","ZipSITDestHHGFinalAddress","ZipSITDestHHGOriginalAddress","ZipSITOriginHHGActualAddress","ZipSITOriginHHGOriginalAddress","StandaloneCrate","StandaloneCrateCap","UncappedRequestTotal","LockedPriceCents"]`), &res); err != nil { panic(err) } for _, v := range res { diff --git a/pkg/gen/primev2api/embedded_spec.go b/pkg/gen/primev2api/embedded_spec.go index c42bc689674..c06735be87b 100644 --- a/pkg/gen/primev2api/embedded_spec.go +++ b/pkg/gen/primev2api/embedded_spec.go @@ -2587,7 +2587,8 @@ func init() { "ZipSITOriginHHGOriginalAddress", "StandaloneCrate", "StandaloneCrateCap", - "UncappedRequestTotal" + "UncappedRequestTotal", + "LockedPriceCents" ] }, "ServiceItemParamOrigin": { @@ -5926,7 +5927,8 @@ func init() { "ZipSITOriginHHGOriginalAddress", "StandaloneCrate", "StandaloneCrateCap", - "UncappedRequestTotal" + "UncappedRequestTotal", + "LockedPriceCents" ] }, "ServiceItemParamOrigin": { diff --git a/pkg/gen/primev2messages/service_item_param_name.go b/pkg/gen/primev2messages/service_item_param_name.go index a6939ad00f4..fb2d4097030 100644 --- a/pkg/gen/primev2messages/service_item_param_name.go +++ b/pkg/gen/primev2messages/service_item_param_name.go @@ -236,6 +236,9 @@ const ( // ServiceItemParamNameUncappedRequestTotal captures enum value "UncappedRequestTotal" ServiceItemParamNameUncappedRequestTotal ServiceItemParamName = "UncappedRequestTotal" + + // ServiceItemParamNameLockedPriceCents captures enum value "LockedPriceCents" + ServiceItemParamNameLockedPriceCents ServiceItemParamName = "LockedPriceCents" ) // for schema @@ -243,7 +246,7 @@ var serviceItemParamNameEnum []interface{} func init() { var res []ServiceItemParamName - if err := json.Unmarshal([]byte(`["ActualPickupDate","ContractCode","ContractYearName","CubicFeetBilled","CubicFeetCrating","DimensionHeight","DimensionLength","DimensionWidth","DistanceZip","DistanceZipSITDest","DistanceZipSITOrigin","EIAFuelPrice","EscalationCompounded","FSCMultiplier","FSCPriceDifferenceInCents","FSCWeightBasedDistanceMultiplier","IsPeak","MarketDest","MarketOrigin","MTOAvailableToPrimeAt","NTSPackingFactor","NumberDaysSIT","PriceAreaDest","PriceAreaIntlDest","PriceAreaIntlOrigin","PriceAreaOrigin","PriceRateOrFactor","PSI_LinehaulDom","PSI_LinehaulDomPrice","PSI_LinehaulShort","PSI_LinehaulShortPrice","PSI_PriceDomDest","PSI_PriceDomDestPrice","PSI_PriceDomOrigin","PSI_PriceDomOriginPrice","PSI_ShippingLinehaulIntlCO","PSI_ShippingLinehaulIntlCOPrice","PSI_ShippingLinehaulIntlOC","PSI_ShippingLinehaulIntlOCPrice","PSI_ShippingLinehaulIntlOO","PSI_ShippingLinehaulIntlOOPrice","RateAreaNonStdDest","RateAreaNonStdOrigin","ReferenceDate","RequestedPickupDate","ServiceAreaDest","ServiceAreaOrigin","ServicesScheduleDest","ServicesScheduleOrigin","SITPaymentRequestEnd","SITPaymentRequestStart","SITScheduleDest","SITScheduleOrigin","SITServiceAreaDest","SITServiceAreaOrigin","WeightAdjusted","WeightBilled","WeightEstimated","WeightOriginal","WeightReweigh","ZipDestAddress","ZipPickupAddress","ZipSITDestHHGFinalAddress","ZipSITDestHHGOriginalAddress","ZipSITOriginHHGActualAddress","ZipSITOriginHHGOriginalAddress","StandaloneCrate","StandaloneCrateCap","UncappedRequestTotal"]`), &res); err != nil { + if err := json.Unmarshal([]byte(`["ActualPickupDate","ContractCode","ContractYearName","CubicFeetBilled","CubicFeetCrating","DimensionHeight","DimensionLength","DimensionWidth","DistanceZip","DistanceZipSITDest","DistanceZipSITOrigin","EIAFuelPrice","EscalationCompounded","FSCMultiplier","FSCPriceDifferenceInCents","FSCWeightBasedDistanceMultiplier","IsPeak","MarketDest","MarketOrigin","MTOAvailableToPrimeAt","NTSPackingFactor","NumberDaysSIT","PriceAreaDest","PriceAreaIntlDest","PriceAreaIntlOrigin","PriceAreaOrigin","PriceRateOrFactor","PSI_LinehaulDom","PSI_LinehaulDomPrice","PSI_LinehaulShort","PSI_LinehaulShortPrice","PSI_PriceDomDest","PSI_PriceDomDestPrice","PSI_PriceDomOrigin","PSI_PriceDomOriginPrice","PSI_ShippingLinehaulIntlCO","PSI_ShippingLinehaulIntlCOPrice","PSI_ShippingLinehaulIntlOC","PSI_ShippingLinehaulIntlOCPrice","PSI_ShippingLinehaulIntlOO","PSI_ShippingLinehaulIntlOOPrice","RateAreaNonStdDest","RateAreaNonStdOrigin","ReferenceDate","RequestedPickupDate","ServiceAreaDest","ServiceAreaOrigin","ServicesScheduleDest","ServicesScheduleOrigin","SITPaymentRequestEnd","SITPaymentRequestStart","SITScheduleDest","SITScheduleOrigin","SITServiceAreaDest","SITServiceAreaOrigin","WeightAdjusted","WeightBilled","WeightEstimated","WeightOriginal","WeightReweigh","ZipDestAddress","ZipPickupAddress","ZipSITDestHHGFinalAddress","ZipSITDestHHGOriginalAddress","ZipSITOriginHHGActualAddress","ZipSITOriginHHGOriginalAddress","StandaloneCrate","StandaloneCrateCap","UncappedRequestTotal","LockedPriceCents"]`), &res); err != nil { panic(err) } for _, v := range res { diff --git a/pkg/gen/primev3api/embedded_spec.go b/pkg/gen/primev3api/embedded_spec.go index 06f06d9386b..ebf4567cbcd 100644 --- a/pkg/gen/primev3api/embedded_spec.go +++ b/pkg/gen/primev3api/embedded_spec.go @@ -2647,7 +2647,8 @@ func init() { "ZipSITOriginHHGOriginalAddress", "StandaloneCrate", "StandaloneCrateCap", - "UncappedRequestTotal" + "UncappedRequestTotal", + "LockedPriceCents" ] }, "ServiceItemParamOrigin": { @@ -3101,6 +3102,22 @@ func init() { "$ref": "#/definitions/StorageFacility" } ] + }, + "tertiaryDeliveryAddress": { + "description": "A third delivery address for this shipment, if the customer entered one. An optional field.", + "allOf": [ + { + "$ref": "#/definitions/Address" + } + ] + }, + "tertiaryPickupAddress": { + "description": "A third pickup address for this shipment, if the customer entered one. An optional field.", + "allOf": [ + { + "$ref": "#/definitions/Address" + } + ] } } }, @@ -3155,6 +3172,16 @@ func init() { "x-nullable": true, "x-omitempty": false }, + "hasTertiaryDestinationAddress": { + "type": "boolean", + "x-nullable": true, + "x-omitempty": false + }, + "hasTertiaryPickupAddress": { + "type": "boolean", + "x-nullable": true, + "x-omitempty": false + }, "pickupAddress": { "description": "The address of the origin location where goods are being moved from.\n", "allOf": [ @@ -3221,6 +3248,22 @@ func init() { "description": "The estimated weight of the pro-gear being moved belonging to a spouse.", "type": "integer", "x-nullable": true + }, + "tertiaryDestinationAddress": { + "description": "An optional third address near the destination where goods will be dropped off.\n", + "allOf": [ + { + "$ref": "#/definitions/Address" + } + ] + }, + "tertiaryPickupAddress": { + "description": "An optional third pickup location near the origin where additional goods exist.\n", + "allOf": [ + { + "$ref": "#/definitions/Address" + } + ] } } }, @@ -6090,7 +6133,8 @@ func init() { "ZipSITOriginHHGOriginalAddress", "StandaloneCrate", "StandaloneCrateCap", - "UncappedRequestTotal" + "UncappedRequestTotal", + "LockedPriceCents" ] }, "ServiceItemParamOrigin": { @@ -6546,6 +6590,22 @@ func init() { "$ref": "#/definitions/StorageFacility" } ] + }, + "tertiaryDeliveryAddress": { + "description": "A third delivery address for this shipment, if the customer entered one. An optional field.", + "allOf": [ + { + "$ref": "#/definitions/Address" + } + ] + }, + "tertiaryPickupAddress": { + "description": "A third pickup address for this shipment, if the customer entered one. An optional field.", + "allOf": [ + { + "$ref": "#/definitions/Address" + } + ] } } }, @@ -6600,6 +6660,16 @@ func init() { "x-nullable": true, "x-omitempty": false }, + "hasTertiaryDestinationAddress": { + "type": "boolean", + "x-nullable": true, + "x-omitempty": false + }, + "hasTertiaryPickupAddress": { + "type": "boolean", + "x-nullable": true, + "x-omitempty": false + }, "pickupAddress": { "description": "The address of the origin location where goods are being moved from.\n", "allOf": [ @@ -6666,6 +6736,22 @@ func init() { "description": "The estimated weight of the pro-gear being moved belonging to a spouse.", "type": "integer", "x-nullable": true + }, + "tertiaryDestinationAddress": { + "description": "An optional third address near the destination where goods will be dropped off.\n", + "allOf": [ + { + "$ref": "#/definitions/Address" + } + ] + }, + "tertiaryPickupAddress": { + "description": "An optional third pickup location near the origin where additional goods exist.\n", + "allOf": [ + { + "$ref": "#/definitions/Address" + } + ] } } }, diff --git a/pkg/gen/primev3messages/service_item_param_name.go b/pkg/gen/primev3messages/service_item_param_name.go index ab53b71f111..a7e2fdf7ea3 100644 --- a/pkg/gen/primev3messages/service_item_param_name.go +++ b/pkg/gen/primev3messages/service_item_param_name.go @@ -236,6 +236,9 @@ const ( // ServiceItemParamNameUncappedRequestTotal captures enum value "UncappedRequestTotal" ServiceItemParamNameUncappedRequestTotal ServiceItemParamName = "UncappedRequestTotal" + + // ServiceItemParamNameLockedPriceCents captures enum value "LockedPriceCents" + ServiceItemParamNameLockedPriceCents ServiceItemParamName = "LockedPriceCents" ) // for schema @@ -243,7 +246,7 @@ var serviceItemParamNameEnum []interface{} func init() { var res []ServiceItemParamName - if err := json.Unmarshal([]byte(`["ActualPickupDate","ContractCode","ContractYearName","CubicFeetBilled","CubicFeetCrating","DimensionHeight","DimensionLength","DimensionWidth","DistanceZip","DistanceZipSITDest","DistanceZipSITOrigin","EIAFuelPrice","EscalationCompounded","FSCMultiplier","FSCPriceDifferenceInCents","FSCWeightBasedDistanceMultiplier","IsPeak","MarketDest","MarketOrigin","MTOAvailableToPrimeAt","NTSPackingFactor","NumberDaysSIT","PriceAreaDest","PriceAreaIntlDest","PriceAreaIntlOrigin","PriceAreaOrigin","PriceRateOrFactor","PSI_LinehaulDom","PSI_LinehaulDomPrice","PSI_LinehaulShort","PSI_LinehaulShortPrice","PSI_PriceDomDest","PSI_PriceDomDestPrice","PSI_PriceDomOrigin","PSI_PriceDomOriginPrice","PSI_ShippingLinehaulIntlCO","PSI_ShippingLinehaulIntlCOPrice","PSI_ShippingLinehaulIntlOC","PSI_ShippingLinehaulIntlOCPrice","PSI_ShippingLinehaulIntlOO","PSI_ShippingLinehaulIntlOOPrice","RateAreaNonStdDest","RateAreaNonStdOrigin","ReferenceDate","RequestedPickupDate","ServiceAreaDest","ServiceAreaOrigin","ServicesScheduleDest","ServicesScheduleOrigin","SITPaymentRequestEnd","SITPaymentRequestStart","SITScheduleDest","SITScheduleOrigin","SITServiceAreaDest","SITServiceAreaOrigin","WeightAdjusted","WeightBilled","WeightEstimated","WeightOriginal","WeightReweigh","ZipDestAddress","ZipPickupAddress","ZipSITDestHHGFinalAddress","ZipSITDestHHGOriginalAddress","ZipSITOriginHHGActualAddress","ZipSITOriginHHGOriginalAddress","StandaloneCrate","StandaloneCrateCap","UncappedRequestTotal"]`), &res); err != nil { + if err := json.Unmarshal([]byte(`["ActualPickupDate","ContractCode","ContractYearName","CubicFeetBilled","CubicFeetCrating","DimensionHeight","DimensionLength","DimensionWidth","DistanceZip","DistanceZipSITDest","DistanceZipSITOrigin","EIAFuelPrice","EscalationCompounded","FSCMultiplier","FSCPriceDifferenceInCents","FSCWeightBasedDistanceMultiplier","IsPeak","MarketDest","MarketOrigin","MTOAvailableToPrimeAt","NTSPackingFactor","NumberDaysSIT","PriceAreaDest","PriceAreaIntlDest","PriceAreaIntlOrigin","PriceAreaOrigin","PriceRateOrFactor","PSI_LinehaulDom","PSI_LinehaulDomPrice","PSI_LinehaulShort","PSI_LinehaulShortPrice","PSI_PriceDomDest","PSI_PriceDomDestPrice","PSI_PriceDomOrigin","PSI_PriceDomOriginPrice","PSI_ShippingLinehaulIntlCO","PSI_ShippingLinehaulIntlCOPrice","PSI_ShippingLinehaulIntlOC","PSI_ShippingLinehaulIntlOCPrice","PSI_ShippingLinehaulIntlOO","PSI_ShippingLinehaulIntlOOPrice","RateAreaNonStdDest","RateAreaNonStdOrigin","ReferenceDate","RequestedPickupDate","ServiceAreaDest","ServiceAreaOrigin","ServicesScheduleDest","ServicesScheduleOrigin","SITPaymentRequestEnd","SITPaymentRequestStart","SITScheduleDest","SITScheduleOrigin","SITServiceAreaDest","SITServiceAreaOrigin","WeightAdjusted","WeightBilled","WeightEstimated","WeightOriginal","WeightReweigh","ZipDestAddress","ZipPickupAddress","ZipSITDestHHGFinalAddress","ZipSITDestHHGOriginalAddress","ZipSITOriginHHGActualAddress","ZipSITOriginHHGOriginalAddress","StandaloneCrate","StandaloneCrateCap","UncappedRequestTotal","LockedPriceCents"]`), &res); err != nil { panic(err) } for _, v := range res { diff --git a/pkg/gen/primev3messages/update_m_t_o_shipment.go b/pkg/gen/primev3messages/update_m_t_o_shipment.go index 037dbf4557f..df0c18d8763 100644 --- a/pkg/gen/primev3messages/update_m_t_o_shipment.go +++ b/pkg/gen/primev3messages/update_m_t_o_shipment.go @@ -112,6 +112,16 @@ type UpdateMTOShipment struct { // storage facility StorageFacility *StorageFacility `json:"storageFacility,omitempty"` + + // A third delivery address for this shipment, if the customer entered one. An optional field. + TertiaryDeliveryAddress struct { + Address + } `json:"tertiaryDeliveryAddress,omitempty"` + + // A third pickup address for this shipment, if the customer entered one. An optional field. + TertiaryPickupAddress struct { + Address + } `json:"tertiaryPickupAddress,omitempty"` } // Validate validates this update m t o shipment @@ -178,6 +188,14 @@ func (m *UpdateMTOShipment) Validate(formats strfmt.Registry) error { res = append(res, err) } + if err := m.validateTertiaryDeliveryAddress(formats); err != nil { + res = append(res, err) + } + + if err := m.validateTertiaryPickupAddress(formats); err != nil { + res = append(res, err) + } + if len(res) > 0 { return errors.CompositeValidationError(res...) } @@ -374,6 +392,22 @@ func (m *UpdateMTOShipment) validateStorageFacility(formats strfmt.Registry) err return nil } +func (m *UpdateMTOShipment) validateTertiaryDeliveryAddress(formats strfmt.Registry) error { + if swag.IsZero(m.TertiaryDeliveryAddress) { // not required + return nil + } + + return nil +} + +func (m *UpdateMTOShipment) validateTertiaryPickupAddress(formats strfmt.Registry) error { + if swag.IsZero(m.TertiaryPickupAddress) { // not required + return nil + } + + return nil +} + // ContextValidate validate this update m t o shipment based on the context it is used func (m *UpdateMTOShipment) ContextValidate(ctx context.Context, formats strfmt.Registry) error { var res []error @@ -410,6 +444,14 @@ func (m *UpdateMTOShipment) ContextValidate(ctx context.Context, formats strfmt. res = append(res, err) } + if err := m.contextValidateTertiaryDeliveryAddress(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateTertiaryPickupAddress(ctx, formats); err != nil { + res = append(res, err) + } + if len(res) > 0 { return errors.CompositeValidationError(res...) } @@ -517,6 +559,16 @@ func (m *UpdateMTOShipment) contextValidateStorageFacility(ctx context.Context, return nil } +func (m *UpdateMTOShipment) contextValidateTertiaryDeliveryAddress(ctx context.Context, formats strfmt.Registry) error { + + return nil +} + +func (m *UpdateMTOShipment) contextValidateTertiaryPickupAddress(ctx context.Context, formats strfmt.Registry) error { + + return nil +} + // MarshalBinary interface implementation func (m *UpdateMTOShipment) MarshalBinary() ([]byte, error) { if m == nil { diff --git a/pkg/gen/primev3messages/update_p_p_m_shipment.go b/pkg/gen/primev3messages/update_p_p_m_shipment.go index 6778ab0cb05..ead98493601 100644 --- a/pkg/gen/primev3messages/update_p_p_m_shipment.go +++ b/pkg/gen/primev3messages/update_p_p_m_shipment.go @@ -44,6 +44,12 @@ type UpdatePPMShipment struct { // has secondary pickup address HasSecondaryPickupAddress *bool `json:"hasSecondaryPickupAddress"` + // has tertiary destination address + HasTertiaryDestinationAddress *bool `json:"hasTertiaryDestinationAddress"` + + // has tertiary pickup address + HasTertiaryPickupAddress *bool `json:"hasTertiaryPickupAddress"` + // The address of the origin location where goods are being moved from. // PickupAddress struct { @@ -88,6 +94,18 @@ type UpdatePPMShipment struct { // The estimated weight of the pro-gear being moved belonging to a spouse. SpouseProGearWeight *int64 `json:"spouseProGearWeight,omitempty"` + + // An optional third address near the destination where goods will be dropped off. + // + TertiaryDestinationAddress struct { + Address + } `json:"tertiaryDestinationAddress,omitempty"` + + // An optional third pickup location near the origin where additional goods exist. + // + TertiaryPickupAddress struct { + Address + } `json:"tertiaryPickupAddress,omitempty"` } // Validate validates this update p p m shipment @@ -126,6 +144,14 @@ func (m *UpdatePPMShipment) Validate(formats strfmt.Registry) error { res = append(res, err) } + if err := m.validateTertiaryDestinationAddress(formats); err != nil { + res = append(res, err) + } + + if err := m.validateTertiaryPickupAddress(formats); err != nil { + res = append(res, err) + } + if len(res) > 0 { return errors.CompositeValidationError(res...) } @@ -219,6 +245,22 @@ func (m *UpdatePPMShipment) validateSitLocation(formats strfmt.Registry) error { return nil } +func (m *UpdatePPMShipment) validateTertiaryDestinationAddress(formats strfmt.Registry) error { + if swag.IsZero(m.TertiaryDestinationAddress) { // not required + return nil + } + + return nil +} + +func (m *UpdatePPMShipment) validateTertiaryPickupAddress(formats strfmt.Registry) error { + if swag.IsZero(m.TertiaryPickupAddress) { // not required + return nil + } + + return nil +} + // ContextValidate validate this update p p m shipment based on the context it is used func (m *UpdatePPMShipment) ContextValidate(ctx context.Context, formats strfmt.Registry) error { var res []error @@ -243,6 +285,14 @@ func (m *UpdatePPMShipment) ContextValidate(ctx context.Context, formats strfmt. res = append(res, err) } + if err := m.contextValidateTertiaryDestinationAddress(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateTertiaryPickupAddress(ctx, formats); err != nil { + res = append(res, err) + } + if len(res) > 0 { return errors.CompositeValidationError(res...) } @@ -290,6 +340,16 @@ func (m *UpdatePPMShipment) contextValidateSitLocation(ctx context.Context, form return nil } +func (m *UpdatePPMShipment) contextValidateTertiaryDestinationAddress(ctx context.Context, formats strfmt.Registry) error { + + return nil +} + +func (m *UpdatePPMShipment) contextValidateTertiaryPickupAddress(ctx context.Context, formats strfmt.Registry) error { + + return nil +} + // MarshalBinary interface implementation func (m *UpdatePPMShipment) MarshalBinary() ([]byte, error) { if m == nil { diff --git a/pkg/handlers/ghcapi/api.go b/pkg/handlers/ghcapi/api.go index 29e8f58963d..eb3bafeceb6 100644 --- a/pkg/handlers/ghcapi/api.go +++ b/pkg/handlers/ghcapi/api.go @@ -671,6 +671,12 @@ func NewGhcAPIHandler(handlerConfig handlers.HandlerConfig) *ghcops.MymoveAPI { move.NewMoveCanceler(), } + paymentRequestBulkDownloadCreator := paymentrequest.NewPaymentRequestBulkDownloadCreator(pdfGenerator) + ghcAPI.PaymentRequestsBulkDownloadHandler = PaymentRequestBulkDownloadHandler{ + handlerConfig, + paymentRequestBulkDownloadCreator, + } + dateSelectionChecker := dateservice.NewDateSelectionChecker() ghcAPI.CalendarIsDateWeekendHolidayHandler = IsDateWeekendHolidayHandler{handlerConfig, dateSelectionChecker} diff --git a/pkg/handlers/ghcapi/customer.go b/pkg/handlers/ghcapi/customer.go index 51e57284945..7452fa9589c 100644 --- a/pkg/handlers/ghcapi/customer.go +++ b/pkg/handlers/ghcapi/customer.go @@ -163,7 +163,6 @@ func (h CreateCustomerWithOktaOptionHandler) Handle(params customercodeop.Create payload := params.Body var err error var serviceMembers []models.ServiceMember - var edipi *string var dodidUniqueFeatureFlag bool // evaluating feature flag to see if we need to check if the DODID exists already @@ -177,30 +176,31 @@ func (h CreateCustomerWithOktaOptionHandler) Handle(params customercodeop.Create } if dodidUniqueFeatureFlag { - if payload.Edipi == nil || *payload.Edipi == "" { - edipi = nil - } else { - query := `SELECT service_members.edipi + query := `SELECT service_members.edipi FROM service_members WHERE service_members.edipi = $1` - err := appCtx.DB().RawQuery(query, payload.Edipi).All(&serviceMembers) - if err != nil { - errorMsg := apperror.NewBadDataError("error when checking for existing service member") - payload := payloadForValidationError("Unable to create a customer", errorMsg.Error(), h.GetTraceIDFromRequest(params.HTTPRequest), validate.NewErrors()) - return customercodeop.NewCreateCustomerWithOktaOptionUnprocessableEntity().WithPayload(payload), errorMsg - } else if len(serviceMembers) > 0 { - errorMsg := apperror.NewConflictError(h.GetTraceIDFromRequest(params.HTTPRequest), "Service member with this DODID already exists. Please use a different DODID number.") - payload := payloadForValidationError("Unable to create a customer", errorMsg.Error(), h.GetTraceIDFromRequest(params.HTTPRequest), validate.NewErrors()) - return customercodeop.NewCreateCustomerWithOktaOptionUnprocessableEntity().WithPayload(payload), errorMsg - } + err := appCtx.DB().RawQuery(query, payload.Edipi).All(&serviceMembers) + if err != nil { + errorMsg := apperror.NewBadDataError("error when checking for existing service member") + payload := payloadForValidationError("Unable to create a customer", errorMsg.Error(), h.GetTraceIDFromRequest(params.HTTPRequest), validate.NewErrors()) + return customercodeop.NewCreateCustomerWithOktaOptionUnprocessableEntity().WithPayload(payload), errorMsg + } else if len(serviceMembers) > 0 { + errorMsg := apperror.NewConflictError(h.GetTraceIDFromRequest(params.HTTPRequest), "Service member with this DODID already exists. Please use a different DODID number.") + payload := payloadForValidationError("Unable to create a customer", errorMsg.Error(), h.GetTraceIDFromRequest(params.HTTPRequest), validate.NewErrors()) + return customercodeop.NewCreateCustomerWithOktaOptionUnprocessableEntity().WithPayload(payload), errorMsg } + } - if len(serviceMembers) == 0 { - edipi = params.Body.Edipi + // Endpoint specific EDIPI and EMPLID check + // The following validation currently is only intended for the customer creation + // conducted by an office user such as the Service Counselor + if payload.Affiliation != nil && *payload.Affiliation == ghcmessages.AffiliationCOASTGUARD { + // EMPLID cannot be null + if payload.Emplid == nil { + errorMsg := apperror.NewConflictError(h.GetTraceIDFromRequest(params.HTTPRequest), "Service members from the Coast Guard require an EMPLID for creation.") + payload := payloadForValidationError("Unable to create a customer", errorMsg.Error(), h.GetTraceIDFromRequest(params.HTTPRequest), validate.NewErrors()) + return customercodeop.NewCreateCustomerWithOktaOptionUnprocessableEntity().WithPayload(payload), errorMsg } - } else { - // If the feature flag is not enabled, we will just set the dodid and continue - edipi = params.Body.Edipi } var newServiceMember models.ServiceMember @@ -250,18 +250,11 @@ func (h CreateCustomerWithOktaOptionHandler) Handle(params customercodeop.Create residentialAddress := addressModelFromPayload(&payload.ResidentialAddress.Address) backupMailingAddress := addressModelFromPayload(&payload.BackupMailingAddress.Address) - var emplid *string - if *payload.Emplid == "" { - emplid = nil - } else { - emplid = payload.Emplid - } - // Create a new serviceMember using the userID newServiceMember = models.ServiceMember{ UserID: userID, - Edipi: edipi, - Emplid: emplid, + Edipi: &payload.Edipi, + Emplid: payload.Emplid, Affiliation: (*models.ServiceMemberAffiliation)(payload.Affiliation), FirstName: &payload.FirstName, MiddleName: payload.MiddleName, diff --git a/pkg/handlers/ghcapi/customer_test.go b/pkg/handlers/ghcapi/customer_test.go index ca46f85b2a5..3a6a3305172 100644 --- a/pkg/handlers/ghcapi/customer_test.go +++ b/pkg/handlers/ghcapi/customer_test.go @@ -53,7 +53,7 @@ func (suite *HandlerSuite) TestGetCustomerHandlerIntegration() { suite.NoError(getCustomerPayload.Validate(strfmt.Default)) suite.Equal(strfmt.UUID(customer.ID.String()), getCustomerPayload.ID) - suite.Equal(*customer.Edipi, getCustomerPayload.DodID) + suite.Equal(*customer.Edipi, getCustomerPayload.Edipi) suite.Equal(strfmt.UUID(customer.UserID.String()), getCustomerPayload.UserID) suite.Equal(customer.Affiliation.String(), getCustomerPayload.Agency) suite.Equal(customer.PersonalEmail, getCustomerPayload.Email) @@ -162,7 +162,7 @@ func (suite *HandlerSuite) TestCreateCustomerWithOktaOptionHandler() { FirstName: "First", Telephone: handlers.FmtString("223-455-3399"), Affiliation: &affiliation, - Edipi: handlers.FmtString(""), + Edipi: "", Emplid: handlers.FmtString(""), PersonalEmail: *handlers.FmtString("email@email.com"), BackupContact: &ghcmessages.BackupContact{ @@ -260,7 +260,7 @@ func (suite *HandlerSuite) TestCreateCustomerWithOktaOptionHandler() { FirstName: "First", Telephone: handlers.FmtString("223-455-3399"), Affiliation: &affiliation, - Edipi: customer.Edipi, + Edipi: *customer.Edipi, PersonalEmail: *handlers.FmtString("email@email.com"), BackupContact: &ghcmessages.BackupContact{ Name: handlers.FmtString("New Backup Contact"), @@ -298,6 +298,81 @@ func (suite *HandlerSuite) TestCreateCustomerWithOktaOptionHandler() { response := handler.Handle(params) suite.Assertions.IsType(&customerops.CreateCustomerWithOktaOptionUnprocessableEntity{}, response) }) + + suite.Run("Unable to create customer of affiliation Coast Guard with no EMPLID", func() { + // in order to call the endpoint, we need to be an authenticated office user that's a SC + officeUser := factory.BuildOfficeUserWithRoles(suite.DB(), nil, []roles.RoleType{roles.RoleTypeTOO}) + officeUser.User.Roles = append(officeUser.User.Roles, roles.Role{ + RoleType: roles.RoleTypeServicesCounselor, + }) + + // Build provider + provider, err := factory.BuildOktaProvider(officeProviderName) + suite.NoError(err) + + mockAndActivateOktaEndpoints(provider) + + residentialAddress := ghcmessages.Address{ + StreetAddress1: handlers.FmtString("123 New Street"), + City: handlers.FmtString("Newcity"), + State: handlers.FmtString("MA"), + PostalCode: handlers.FmtString("02110"), + } + + backupAddress := ghcmessages.Address{ + StreetAddress1: handlers.FmtString("123 Backup Street"), + City: handlers.FmtString("Backupcity"), + State: handlers.FmtString("MA"), + PostalCode: handlers.FmtString("02115"), + } + + affiliation := ghcmessages.AffiliationCOASTGUARD + + body := &ghcmessages.CreateCustomerPayload{ + LastName: "Last", + FirstName: "First", + Telephone: handlers.FmtString("223-455-3399"), + Affiliation: &affiliation, + Edipi: "1234567890", + PersonalEmail: *handlers.FmtString("email@email.com"), + BackupContact: &ghcmessages.BackupContact{ + Name: handlers.FmtString("New Backup Contact"), + Phone: handlers.FmtString("445-345-1212"), + Email: handlers.FmtString("newbackup@mail.com"), + }, + ResidentialAddress: struct { + ghcmessages.Address + }{ + Address: residentialAddress, + }, + BackupMailingAddress: struct { + ghcmessages.Address + }{ + Address: backupAddress, + }, + CreateOktaAccount: true, + // when CacUser is false, this indicates a non-CAC user so CacValidated is set to true + CacUser: false, + } + + defer goth.ClearProviders() + goth.UseProviders(provider) + + request := httptest.NewRequest("POST", "/customer", nil) + request = suite.AuthenticateOfficeRequest(request, officeUser) + params := customerops.CreateCustomerWithOktaOptionParams{ + HTTPRequest: request, + Body: body, + } + handlerConfig := suite.HandlerConfig() + handler := CreateCustomerWithOktaOptionHandler{ + handlerConfig, + } + response := handler.Handle(params) + suite.Assertions.IsType(&customerops.CreateCustomerWithOktaOptionUnprocessableEntity{}, response) + failedToCreateCustomerPayload := response.(*customerops.CreateCustomerWithOktaOptionUnprocessableEntity).Payload.ClientError.Detail + suite.Equal("ID: 00000000-0000-0000-0000-000000000000 is in a conflicting state Service members from the Coast Guard require an EMPLID for creation.", *failedToCreateCustomerPayload) + }) } func (suite *HandlerSuite) TestSearchCustomersHandler() { diff --git a/pkg/handlers/ghcapi/internal/payloads/model_to_payload.go b/pkg/handlers/ghcapi/internal/payloads/model_to_payload.go index a73ba39398c..7db1f9be932 100644 --- a/pkg/handlers/ghcapi/internal/payloads/model_to_payload.go +++ b/pkg/handlers/ghcapi/internal/payloads/model_to_payload.go @@ -490,7 +490,7 @@ func Customer(customer *models.ServiceMember) *ghcmessages.Customer { payload := ghcmessages.Customer{ Agency: swag.StringValue((*string)(customer.Affiliation)), CurrentAddress: Address(customer.ResidentialAddress), - DodID: swag.StringValue(customer.Edipi), + Edipi: swag.StringValue(customer.Edipi), Email: customer.PersonalEmail, FirstName: swag.StringValue(customer.FirstName), ID: strfmt.UUID(customer.ID.String()), diff --git a/pkg/handlers/ghcapi/mto_service_items.go b/pkg/handlers/ghcapi/mto_service_items.go index 160f96db12b..33b34ae1373 100644 --- a/pkg/handlers/ghcapi/mto_service_items.go +++ b/pkg/handlers/ghcapi/mto_service_items.go @@ -17,7 +17,6 @@ import ( "github.com/transcom/mymove/pkg/handlers" "github.com/transcom/mymove/pkg/handlers/ghcapi/internal/payloads" "github.com/transcom/mymove/pkg/models" - serviceparamlookups "github.com/transcom/mymove/pkg/payment_request/service_param_value_lookups" "github.com/transcom/mymove/pkg/services" "github.com/transcom/mymove/pkg/services/audit" "github.com/transcom/mymove/pkg/services/event" @@ -384,7 +383,6 @@ func (h ListMTOServiceItemsHandler) Handle(params mtoserviceitemop.ListMTOServic } if len(indices) > 0 { - contract, err := serviceparamlookups.FetchContract(appCtx, *moveTaskOrder.AvailableToPrimeAt) if err != nil { return mtoserviceitemop.NewListMTOServiceItemsInternalServerError(), err } @@ -394,9 +392,9 @@ func (h ListMTOServiceItemsHandler) Handle(params mtoserviceitemop.ListMTOServic var displayParams services.PricingDisplayParams var err error if serviceItems[index].ReService.Code == "CS" { - price, displayParams, err = h.counselingPricer.Price(appCtx, contract.Code, *moveTaskOrder.AvailableToPrimeAt) + price, displayParams, err = h.counselingPricer.Price(appCtx, serviceItems[index].LockedPriceCents) } else if serviceItems[index].ReService.Code == "MS" { - price, displayParams, err = h.moveManagementPricer.Price(appCtx, contract.Code, *moveTaskOrder.AvailableToPrimeAt) + price, displayParams, err = h.moveManagementPricer.Price(appCtx, serviceItems[index].LockedPriceCents) } for _, param := range displayParams { diff --git a/pkg/handlers/ghcapi/orders.go b/pkg/handlers/ghcapi/orders.go index 925c0cf2084..a4231d9df06 100644 --- a/pkg/handlers/ghcapi/orders.go +++ b/pkg/handlers/ghcapi/orders.go @@ -325,24 +325,12 @@ func (h CreateOrderHandler) Handle(params orderop.CreateOrderParams) middleware. } if newOrder.OrdersType == "SAFETY" { - // if creating a Safety move, clear out the DoDID and OktaID for the customer + // if creating a Safety move, clear out the OktaID for the customer since they won't log into MilMove err = models.UpdateUserOktaID(appCtx.DB(), &newOrder.ServiceMember.User, "") if err != nil { appCtx.Logger().Error("Authorization error updating user", zap.Error(err)) return orderop.NewUpdateOrderInternalServerError(), err } - - err = models.UpdateServiceMemberDoDID(appCtx.DB(), &newOrder.ServiceMember, nil) - if err != nil { - appCtx.Logger().Error("Authorization error updating service member", zap.Error(err)) - return orderop.NewUpdateOrderInternalServerError(), err - } - - err = models.UpdateServiceMemberEMPLID(appCtx.DB(), &newOrder.ServiceMember, nil) - if err != nil { - appCtx.Logger().Error("Authorization error updating service member", zap.Error(err)) - return orderop.NewUpdateOrderInternalServerError(), err - } } newMove, verrs, err := newOrder.CreateNewMove(appCtx.DB(), moveOptions) diff --git a/pkg/handlers/ghcapi/payment_request.go b/pkg/handlers/ghcapi/payment_request.go index 757993d2669..622d38b1abc 100644 --- a/pkg/handlers/ghcapi/payment_request.go +++ b/pkg/handlers/ghcapi/payment_request.go @@ -2,6 +2,7 @@ package ghcapi import ( "fmt" + "io" "reflect" "time" @@ -261,3 +262,39 @@ func (h ShipmentsSITBalanceHandler) Handle( return paymentrequestop.NewGetShipmentsPaymentSITBalanceOK().WithPayload(payload), nil }) } + +type PaymentRequestBulkDownloadHandler struct { + handlers.HandlerConfig + services.PaymentRequestBulkDownloadCreator +} + +func (h PaymentRequestBulkDownloadHandler) Handle(params paymentrequestop.BulkDownloadParams) middleware.Responder { + return h.AuditableAppContextFromRequestWithErrors(params.HTTPRequest, + func(appCtx appcontext.AppContext) (middleware.Responder, error) { + logger := appCtx.Logger() + + paymentRequestID, err := uuid.FromString(params.PaymentRequestID) + if err != nil { + errInstance := fmt.Sprintf("Instance: %s", h.GetTraceIDFromRequest(params.HTTPRequest)) + + errPayload := &ghcmessages.Error{Message: &errInstance} + + appCtx.Logger().Error(err.Error()) + return paymentrequestop.NewBulkDownloadBadRequest().WithPayload(errPayload), err + } + + paymentRequestPacket, err := h.PaymentRequestBulkDownloadCreator.CreatePaymentRequestBulkDownload(appCtx, paymentRequestID) + if err != nil { + logger.Error("Error creating Payment Request Downloads Packet", zap.Error(err)) + errInstance := fmt.Sprintf("Instance: %s", h.GetTraceIDFromRequest(params.HTTPRequest)) + errPayload := &ghcmessages.Error{Message: &errInstance} + return paymentrequestop.NewBulkDownloadInternalServerError(). + WithPayload(errPayload), err + } + + payload := io.NopCloser(paymentRequestPacket) + filename := fmt.Sprintf("inline; filename=\"PaymentRequestBulkPacket-%s.pdf\"", time.Now().Format("01-02-2006_15-04-05")) + + return paymentrequestop.NewBulkDownloadOK().WithContentDisposition(filename).WithPayload(payload), nil + }) +} diff --git a/pkg/handlers/ghcapi/queues_test.go b/pkg/handlers/ghcapi/queues_test.go index f1efe5028fe..4cc6aaaa1da 100644 --- a/pkg/handlers/ghcapi/queues_test.go +++ b/pkg/handlers/ghcapi/queues_test.go @@ -833,7 +833,7 @@ func (suite *HandlerSuite) TestGetMoveQueuesHandlerCustomerInfoFilters() { result := payload.QueueMoves[0] suite.Len(payload.QueueMoves, 1) - suite.Equal("11111", result.Customer.DodID) + suite.Equal("11111", result.Customer.Edipi) }) suite.Run("returns results matching Move ID search term", func() { @@ -1525,7 +1525,7 @@ func (suite *HandlerSuite) TestGetServicesCounselingQueueHandler() { suite.Len(payload.QueueMoves, 2) suite.Equal(order.ServiceMember.ID.String(), result1.Customer.ID.String()) - suite.Equal(*order.ServiceMember.Edipi, result1.Customer.DodID) + suite.Equal(*order.ServiceMember.Edipi, result1.Customer.Edipi) suite.Equal(subtestData.needsCounselingMove.Locator, result1.Locator) suite.EqualValues(subtestData.needsCounselingMove.Status, result1.Status) suite.Equal(subtestData.needsCounselingEarliestShipment.RequestedPickupDate.Format(time.RFC3339Nano), (time.Time)(*result1.RequestedMoveDate).Format(time.RFC3339Nano)) diff --git a/pkg/handlers/ghcapi/tranportation_offices.go b/pkg/handlers/ghcapi/tranportation_offices.go index d60f4b0a70a..405580923bb 100644 --- a/pkg/handlers/ghcapi/tranportation_offices.go +++ b/pkg/handlers/ghcapi/tranportation_offices.go @@ -20,7 +20,10 @@ func (h GetTransportationOfficesHandler) Handle(params transportationofficeop.Ge return h.AuditableAppContextFromRequestWithErrors(params.HTTPRequest, func(appCtx appcontext.AppContext) (middleware.Responder, error) { - transportationOffices, err := h.TransportationOfficesFetcher.GetTransportationOffices(appCtx, params.Search, false) + // B-21022: forPpm param is set true. This is used by PPM closeout widget. Need to ensure certain offices are included/excluded + // if location has ppm closedout enabled. + transportationOffices, err := h.TransportationOfficesFetcher.GetTransportationOffices(appCtx, params.Search, true) + if err != nil { appCtx.Logger().Error("Error searching for Transportation Offices: ", zap.Error(err)) return transportationofficeop.NewGetTransportationOfficesInternalServerError(), err diff --git a/pkg/handlers/primeapi/mto_shipment_address.go b/pkg/handlers/primeapi/mto_shipment_address.go index 64e658d251c..b45999955a9 100644 --- a/pkg/handlers/primeapi/mto_shipment_address.go +++ b/pkg/handlers/primeapi/mto_shipment_address.go @@ -39,19 +39,19 @@ func (h UpdateMTOShipmentAddressHandler) Handle(params mtoshipmentops.UpdateMTOS } if dbShipment.ShipmentType == models.MTOShipmentTypeHHGIntoNTSDom && - (*dbShipment.DestinationAddressID == addressID) { + (dbShipment.DestinationAddressID != nil && *dbShipment.DestinationAddressID == addressID) { return mtoshipmentops.NewUpdateMTOShipmentAddressUnprocessableEntity().WithPayload(payloads.ValidationError( "Cannot update the destination address of an NTS shipment directly, please update the storage facility address instead", h.GetTraceIDFromRequest(params.HTTPRequest), nil)), err } if dbShipment.Status == models.MTOShipmentStatusApproved && - (*dbShipment.DestinationAddressID == addressID) { + (dbShipment.DestinationAddressID != nil && *dbShipment.DestinationAddressID == addressID) { return mtoshipmentops.NewUpdateMTOShipmentAddressUnprocessableEntity().WithPayload(payloads.ValidationError( "This shipment is approved, please use the updateShipmentDestinationAddress endpoint to update the destination address of an approved shipment", h.GetTraceIDFromRequest(params.HTTPRequest), nil)), err } if dbShipment.ShipmentType == models.MTOShipmentTypeHHGOutOfNTSDom && - (*dbShipment.PickupAddressID == addressID) { + (*dbShipment.PickupAddressID != uuid.Nil && *dbShipment.PickupAddressID == addressID) { return mtoshipmentops.NewUpdateMTOShipmentAddressUnprocessableEntity().WithPayload(payloads.ValidationError( "Cannot update the pickup address of an NTS-Release shipment directly, please update the storage facility address instead", h.GetTraceIDFromRequest(params.HTTPRequest), nil)), err } diff --git a/pkg/handlers/primeapiv3/payloads/payload_to_model.go b/pkg/handlers/primeapiv3/payloads/payload_to_model.go index 28be5539b61..50588acfb28 100644 --- a/pkg/handlers/primeapiv3/payloads/payload_to_model.go +++ b/pkg/handlers/primeapiv3/payloads/payload_to_model.go @@ -341,6 +341,14 @@ func MTOShipmentModelFromUpdate(mtoShipment *primev3messages.UpdateMTOShipment, model.HasSecondaryPickupAddress = handlers.FmtBool(true) } + addressModel = AddressModel(&mtoShipment.TertiaryPickupAddress.Address) + if addressModel != nil { + model.TertiaryPickupAddress = addressModel + tertiaryPickupAddressID := uuid.FromStringOrNil(addressModel.ID.String()) + model.TertiaryPickupAddressID = &tertiaryPickupAddressID + model.HasTertiaryPickupAddress = handlers.FmtBool(true) + } + addressModel = AddressModel(&mtoShipment.SecondaryDeliveryAddress.Address) if addressModel != nil { model.SecondaryDeliveryAddress = addressModel @@ -349,6 +357,14 @@ func MTOShipmentModelFromUpdate(mtoShipment *primev3messages.UpdateMTOShipment, model.HasSecondaryDeliveryAddress = handlers.FmtBool(true) } + addressModel = AddressModel(&mtoShipment.TertiaryDeliveryAddress.Address) + if addressModel != nil { + model.TertiaryDeliveryAddress = addressModel + tertiaryDeliveryAddressID := uuid.FromStringOrNil(addressModel.ID.String()) + model.TertiaryDeliveryAddressID = &tertiaryDeliveryAddressID + model.HasTertiaryDeliveryAddress = handlers.FmtBool(true) + } + if mtoShipment.PpmShipment != nil { model.PPMShipment = PPMShipmentModelFromUpdate(mtoShipment.PpmShipment) model.PPMShipment.Shipment = *model @@ -391,6 +407,15 @@ func PPMShipmentModelFromUpdate(ppmShipment *primev3messages.UpdatePPMShipment) } } + if ppmShipment.HasTertiaryPickupAddress != nil && *ppmShipment.HasTertiaryPickupAddress { + addressModel = AddressModel(&ppmShipment.TertiaryPickupAddress.Address) + if addressModel != nil { + model.TertiaryPickupAddress = addressModel + tertiaryPickupAddressID := uuid.FromStringOrNil(addressModel.ID.String()) + model.TertiaryPickupAddressID = &tertiaryPickupAddressID + } + } + addressModel = AddressModel(&ppmShipment.DestinationAddress.Address) if addressModel != nil { model.DestinationAddress = addressModel @@ -406,6 +431,15 @@ func PPMShipmentModelFromUpdate(ppmShipment *primev3messages.UpdatePPMShipment) } } + if ppmShipment.HasTertiaryDestinationAddress != nil && *ppmShipment.HasTertiaryDestinationAddress { + addressModel = AddressModel(&ppmShipment.TertiaryDestinationAddress.Address) + if addressModel != nil { + model.TertiaryDestinationAddress = addressModel + tertiaryDestinationAddressID := uuid.FromStringOrNil(addressModel.ID.String()) + model.TertiaryDestinationAddressID = &tertiaryDestinationAddressID + } + } + expectedDepartureDate := handlers.FmtDatePtrToPopPtr(ppmShipment.ExpectedDepartureDate) if expectedDepartureDate != nil && !expectedDepartureDate.IsZero() { model.ExpectedDepartureDate = *expectedDepartureDate diff --git a/pkg/models/service_item_param_key.go b/pkg/models/service_item_param_key.go index 6a708cb8f94..3bfd789dcc4 100644 --- a/pkg/models/service_item_param_key.go +++ b/pkg/models/service_item_param_key.go @@ -155,6 +155,8 @@ const ( ServiceItemParamNameStandaloneCrateCap ServiceItemParamName = "StandaloneCrateCap" // ServiceItemParamNameUncappedRequestTotal is the param key name UncappedRequestTotal ServiceItemParamNameUncappedRequestTotal ServiceItemParamName = "UncappedRequestTotal" + // ServiceItemParamNameLockedPriceCents is the param key name LockedPriceCents + ServiceItemParamNameLockedPriceCents ServiceItemParamName = "LockedPriceCents" ) // ServiceItemParamType is a type of service item parameter @@ -272,6 +274,7 @@ var ValidServiceItemParamNames = []ServiceItemParamName{ ServiceItemParamNameStandaloneCrate, ServiceItemParamNameStandaloneCrateCap, ServiceItemParamNameUncappedRequestTotal, + ServiceItemParamNameLockedPriceCents, } // ValidServiceItemParamNameStrings lists all valid service item param key names @@ -345,6 +348,7 @@ var ValidServiceItemParamNameStrings = []string{ string(ServiceItemParamNameStandaloneCrate), string(ServiceItemParamNameStandaloneCrateCap), string(ServiceItemParamNameUncappedRequestTotal), + string(ServiceItemParamNameLockedPriceCents), } // ValidServiceItemParamTypes lists all valid service item param types diff --git a/pkg/paperwork/generator.go b/pkg/paperwork/generator.go index 881f58c454a..23ee0accdc3 100644 --- a/pkg/paperwork/generator.go +++ b/pkg/paperwork/generator.go @@ -213,7 +213,7 @@ func (g *Generator) GetPdfFileInfoByContents(file afero.File) (*pdfcpu.PDFInfo, // CreateMergedPDFUpload converts Uploads to PDF and merges them into a single PDF func (g *Generator) CreateMergedPDFUpload(appCtx appcontext.AppContext, uploads models.Uploads) (afero.File, error) { - pdfs, err := g.ConvertUploadsToPDF(appCtx, uploads) + pdfs, err := g.ConvertUploadsToPDF(appCtx, uploads, true) if err != nil { return nil, errors.Wrap(err, "Error while converting uploads") } @@ -227,7 +227,7 @@ func (g *Generator) CreateMergedPDFUpload(appCtx appcontext.AppContext, uploads } // ConvertUploadsToPDF turns a slice of Uploads into a slice of paths to converted PDF files -func (g *Generator) ConvertUploadsToPDF(appCtx appcontext.AppContext, uploads models.Uploads) ([]string, error) { +func (g *Generator) ConvertUploadsToPDF(appCtx appcontext.AppContext, uploads models.Uploads, doRotation bool) ([]string, error) { // tempfile paths to be returned pdfs := make([]string, 0) @@ -240,9 +240,18 @@ func (g *Generator) ConvertUploadsToPDF(appCtx appcontext.AppContext, uploads mo if len(images) > 0 { // We want to retain page order and will generate a PDF for images // that have already been encountered before handling this PDF. - pdf, err := g.PDFFromImages(appCtx, images) - if err != nil { - return nil, errors.Wrap(err, "Converting images") + var pdf string + var err error + if doRotation { + pdf, err = g.PDFFromImages(appCtx, images) + if err != nil { + return nil, errors.Wrap(err, "Converting images") + } + } else { + pdf, err = g.PDFFromImagesNoRotation(appCtx, images) + if err != nil { + return nil, errors.Wrap(err, "Converting images") + } } pdfs = append(pdfs, pdf) images = make([]inputFile, 0) @@ -282,9 +291,19 @@ func (g *Generator) ConvertUploadsToPDF(appCtx appcontext.AppContext, uploads mo // Merge all remaining images in urls into a new PDF if len(images) > 0 { - pdf, err := g.PDFFromImages(appCtx, images) - if err != nil { - return nil, errors.Wrap(err, "Converting remaining images to pdf") + var pdf string + var err error + + if doRotation { + pdf, err = g.PDFFromImages(appCtx, images) + if err != nil { + return nil, errors.Wrap(err, "Converting remaining images to pdf") + } + } else { + pdf, err = g.PDFFromImagesNoRotation(appCtx, images) + if err != nil { + return nil, errors.Wrap(err, "Converting remaining images to pdf") + } } pdfs = append(pdfs, pdf) } @@ -514,6 +533,115 @@ func (g *Generator) PDFFromImages(appCtx appcontext.AppContext, images []inputFi return outputFile.Name(), nil } +// PDFFromImages returns the path to tempfile PDF containing all images included +// in urls. +// +// The files at those paths will be tempfiles that will need to be cleaned +// up by the caller. +func (g *Generator) PDFFromImagesNoRotation(appCtx appcontext.AppContext, images []inputFile) (string, error) { + // These constants are based on A4 page size, which we currently default to. + horizontalMargin := 0.0 + topMargin := 0.0 + bodyWidth := PdfPageWidth - (horizontalMargin * 2) + bodyHeight := PdfPageHeight - (topMargin * 2) + wToHRatio := bodyWidth / bodyHeight + + pdf := gofpdf.New(PdfOrientation, PdfUnit, PdfPageSize, PdfFontDir) + pdf.SetMargins(horizontalMargin, topMargin, horizontalMargin) + + if len(images) == 0 { + return "", errors.New("No images provided") + } + + appCtx.Logger().Debug("generating PDF from image files", zap.Any("images", images)) + + outputFile, err := g.newTempFile() + if err != nil { + return "", err + } + + defer func() { + if closeErr := outputFile.Close(); closeErr != nil { + appCtx.Logger().Debug("Failed to close file", zap.Error(closeErr)) + } + }() + + var opt gofpdf.ImageOptions + for _, img := range images { + pdf.AddPage() + file, openErr := g.fs.Open(img.Path) + if openErr != nil { + return "", errors.Wrap(openErr, "Opening image file") + } + + defer func() { + if closeErr := file.Close(); closeErr != nil { + appCtx.Logger().Debug("Failed to close file", zap.Error(closeErr)) + } + }() + + if img.ContentType == uploader.FileTypePNG { + appCtx.Logger().Debug("Converting png to 8-bit") + // gofpdf isn't able to process 16-bit PNGs, so to be safe we convert all PNGs to an 8-bit color depth + newFile, newTemplateFileErr := g.newTempFile() + if newTemplateFileErr != nil { + return "", errors.Wrap(newTemplateFileErr, "Creating temp file for png conversion") + } + + defer func() { + if closeErr := newFile.Close(); closeErr != nil { + appCtx.Logger().Debug("Failed to close file", zap.Error(closeErr)) + } + }() + + convertTo8BitPNGErr := convertTo8BitPNG(file, newFile) + if convertTo8BitPNGErr != nil { + return "", errors.Wrap(convertTo8BitPNGErr, "Converting to 8-bit png") + } + file = newFile + _, fileSeekErr := file.Seek(0, io.SeekStart) + if fileSeekErr != nil { + return "", errors.Wrapf(fileSeekErr, "file.Seek offset: 0 whence: %d", io.SeekStart) + } + } + + widthInPdf := bodyWidth + heightInPdf := 0.0 + + // Scale using the imageOptions below + // BodyWidth should be set to 0 when the image height the proportion of the page + // is taller than wide as compared to an A4 page. + // + // The opposite is true and defaulted for when the image is wider than it is tall, + // in comparison to an A4 page. + if float64(bodyWidth/bodyHeight) < wToHRatio { + widthInPdf = 0 + heightInPdf = bodyHeight + } + + // Seek to the beginning of the file so when we register the image, it doesn't start + // at the end of the file. + _, fileSeekErr := file.Seek(0, io.SeekStart) + if fileSeekErr != nil { + return "", errors.Wrapf(fileSeekErr, "file.Seek offset: 0 whence: %d", io.SeekStart) + } + // Need to register the image using an afero reader, else it uses default filesystem + pdf.RegisterImageReader(img.Path, contentTypeToImageType[img.ContentType], file) + opt.ImageType = contentTypeToImageType[img.ContentType] + + pdf.ImageOptions(img.Path, horizontalMargin, topMargin, widthInPdf, heightInPdf, false, opt, 0, "") + fileCloseErr := file.Close() + if fileCloseErr != nil { + return "", errors.Wrapf(err, "error closing file: %s", file.Name()) + } + } + + if err = pdf.OutputAndClose(outputFile); err != nil { + return "", errors.Wrap(err, "could not write PDF to outputfile") + } + return outputFile.Name(), nil +} + // MergePDFFiles Merges a slice of paths to PDF files into a single PDF func (g *Generator) MergePDFFiles(_ appcontext.AppContext, paths []string) (afero.File, error) { var err error diff --git a/pkg/paperwork/generator_test.go b/pkg/paperwork/generator_test.go index 6ee8396155f..de3636f28da 100644 --- a/pkg/paperwork/generator_test.go +++ b/pkg/paperwork/generator_test.go @@ -143,6 +143,65 @@ func (suite *PaperworkSuite) TestPDFFromImages() { suite.Contains(checksums, orders2Checksum, "did not find hash for orders2.jpg") } +func (suite *PaperworkSuite) TestPDFFromImagesNoRotation() { + generator, newGeneratorErr := NewGenerator(suite.userUploader.Uploader()) + suite.FatalNil(newGeneratorErr) + + images := []inputFile{ + {Path: "testdata/orders1.jpg", ContentType: uploader.FileTypeJPEG}, + {Path: "testdata/orders2.jpg", ContentType: uploader.FileTypeJPEG}, + } + for _, image := range images { + _, err := suite.openLocalFile(image.Path, generator.fs) + suite.FatalNil(err) + } + + generatedPath, err := generator.PDFFromImagesNoRotation(suite.AppContextForTest(), images) + suite.FatalNil(err, "failed to generate pdf") + aferoFile, err := generator.fs.Open(generatedPath) + suite.FatalNil(err, "afero failed to open pdf") + + suite.NotEmpty(generatedPath, "got an empty path to the generated file") + suite.FatalNil(err) + + // verify that the images are in the pdf by extracting them and checking their checksums + file, err := afero.ReadAll(aferoFile) + suite.FatalNil(err) + tmpDir, err := os.MkdirTemp("", "images") + suite.FatalNil(err) + f, err := os.CreateTemp(tmpDir, "") + suite.FatalNil(err) + err = os.WriteFile(f.Name(), file, os.ModePerm) + suite.FatalNil(err) + err = api.ExtractImagesFile(f.Name(), tmpDir, []string{"-2"}, generator.pdfConfig) + suite.FatalNil(err) + err = os.Remove(f.Name()) + suite.FatalNil(err) + + checksums := make([]string, 2) + files, err := os.ReadDir(tmpDir) + suite.FatalNil(err) + + suite.Equal(4, len(files), "did not find 2 images") + + for _, file := range files { + checksum, sha256ForPathErr := suite.sha256ForPath(path.Join(tmpDir, file.Name()), nil) + suite.FatalNil(sha256ForPathErr, "error calculating hash") + if sha256ForPathErr != nil { + suite.FailNow(sha256ForPathErr.Error()) + } + checksums = append(checksums, checksum) + } + + orders1Checksum, err := suite.sha256ForPath("testdata/orders1.jpg", generator.fs) + suite.Nil(err, "error calculating hash") + suite.Contains(checksums, orders1Checksum, "did not find hash for orders1.jpg") + + orders2Checksum, err := suite.sha256ForPath("testdata/orders2.jpg", generator.fs) + suite.Nil(err, "error calculating hash") + suite.Contains(checksums, orders2Checksum, "did not find hash for orders2.jpg") +} + func (suite *PaperworkSuite) TestPDFFromImages16BitPNG() { generator, err := NewGenerator(suite.userUploader.Uploader()) suite.FatalNil(err) @@ -187,7 +246,7 @@ func (suite *PaperworkSuite) TestGenerateUploadsPDF() { uploads, err := models.UploadsFromUserUploads(suite.DB(), order.UploadedOrders.UserUploads) suite.FatalNil(err) - paths, err := generator.ConvertUploadsToPDF(suite.AppContextForTest(), uploads) + paths, err := generator.ConvertUploadsToPDF(suite.AppContextForTest(), uploads, true) suite.FatalNil(err) suite.Equal(3, len(paths), "wrong number of paths returned") diff --git a/pkg/payment_request/service_param_value_lookups/locked_price_cents_lookup.go b/pkg/payment_request/service_param_value_lookups/locked_price_cents_lookup.go new file mode 100644 index 00000000000..f0a5dc62c49 --- /dev/null +++ b/pkg/payment_request/service_param_value_lookups/locked_price_cents_lookup.go @@ -0,0 +1,21 @@ +package serviceparamvaluelookups + +import ( + "github.com/transcom/mymove/pkg/appcontext" + "github.com/transcom/mymove/pkg/apperror" + "github.com/transcom/mymove/pkg/models" +) + +// LockedPriceCents does lookup on serviceItem +type LockedPriceCentsLookup struct { + ServiceItem models.MTOServiceItem +} + +func (r LockedPriceCentsLookup) lookup(appCtx appcontext.AppContext, _ *ServiceItemParamKeyData) (string, error) { + lockedPriceCents := r.ServiceItem.LockedPriceCents + if lockedPriceCents == nil { + return "0", apperror.NewConflictError(r.ServiceItem.ID, "unable to find locked price cents") + } + + return lockedPriceCents.ToMillicents().ToCents().String(), nil +} diff --git a/pkg/payment_request/service_param_value_lookups/service_param_value_lookups.go b/pkg/payment_request/service_param_value_lookups/service_param_value_lookups.go index 69ade8412e6..ff628c0e501 100644 --- a/pkg/payment_request/service_param_value_lookups/service_param_value_lookups.go +++ b/pkg/payment_request/service_param_value_lookups/service_param_value_lookups.go @@ -84,6 +84,7 @@ var ServiceItemParamsWithLookups = []models.ServiceItemParamName{ models.ServiceItemParamNameDimensionWidth, models.ServiceItemParamNameStandaloneCrate, models.ServiceItemParamNameStandaloneCrateCap, + models.ServiceItemParamNameLockedPriceCents, } // ServiceParamLookupInitialize initializes service parameter lookup @@ -425,6 +426,10 @@ func InitializeLookups(appCtx appcontext.AppContext, shipment models.MTOShipment ServiceItem: serviceItem, } + lookups[models.ServiceItemParamNameLockedPriceCents] = LockedPriceCentsLookup{ + ServiceItem: serviceItem, + } + return lookups } diff --git a/pkg/services/ghc_rate_engine.go b/pkg/services/ghc_rate_engine.go index 51bd1d29d5e..5d17e0388ce 100644 --- a/pkg/services/ghc_rate_engine.go +++ b/pkg/services/ghc_rate_engine.go @@ -33,7 +33,7 @@ type ParamsPricer interface { // //go:generate mockery --name ManagementServicesPricer type ManagementServicesPricer interface { - Price(appCtx appcontext.AppContext, contractCode string, mtoAvailableToPrimeAt time.Time) (unit.Cents, PricingDisplayParams, error) + Price(appCtx appcontext.AppContext, lockedPriceCents *unit.Cents) (unit.Cents, PricingDisplayParams, error) ParamsPricer } @@ -41,7 +41,7 @@ type ManagementServicesPricer interface { // //go:generate mockery --name CounselingServicesPricer type CounselingServicesPricer interface { - Price(appCtx appcontext.AppContext, contractCode string, mtoAvailableToPrimeAt time.Time) (unit.Cents, PricingDisplayParams, error) + Price(appCtx appcontext.AppContext, lockedPriceCents *unit.Cents) (unit.Cents, PricingDisplayParams, error) ParamsPricer } diff --git a/pkg/services/ghcrateengine/counseling_services_pricer.go b/pkg/services/ghcrateengine/counseling_services_pricer.go index e2c3869474f..f695e9753c3 100644 --- a/pkg/services/ghcrateengine/counseling_services_pricer.go +++ b/pkg/services/ghcrateengine/counseling_services_pricer.go @@ -2,7 +2,6 @@ package ghcrateengine import ( "fmt" - "time" "github.com/transcom/mymove/pkg/appcontext" "github.com/transcom/mymove/pkg/models" @@ -19,32 +18,30 @@ func NewCounselingServicesPricer() services.CounselingServicesPricer { } // Price determines the price for a counseling service -func (p counselingServicesPricer) Price(appCtx appcontext.AppContext, contractCode string, mtoAvailableToPrimeAt time.Time) (unit.Cents, services.PricingDisplayParams, error) { - taskOrderFee, err := fetchTaskOrderFee(appCtx, contractCode, models.ReServiceCodeCS, mtoAvailableToPrimeAt) - if err != nil { - return unit.Cents(0), nil, fmt.Errorf("could not fetch task order fee: %w", err) +func (p counselingServicesPricer) Price(appCtx appcontext.AppContext, lockedPriceCents *unit.Cents) (unit.Cents, services.PricingDisplayParams, error) { + + if lockedPriceCents == nil { + return 0, nil, fmt.Errorf("invalid value for locked_price_cents") } - displayPriceParams := services.PricingDisplayParams{ + params := services.PricingDisplayParams{ { Key: models.ServiceItemParamNamePriceRateOrFactor, - Value: FormatCents(taskOrderFee.PriceCents), + Value: FormatCents(*lockedPriceCents), }, } - return taskOrderFee.PriceCents, displayPriceParams, nil + + return *lockedPriceCents, params, nil } // PriceUsingParams determines the price for a counseling service given PaymentServiceItemParams func (p counselingServicesPricer) PriceUsingParams(appCtx appcontext.AppContext, params models.PaymentServiceItemParams) (unit.Cents, services.PricingDisplayParams, error) { - contractCode, err := getParamString(params, models.ServiceItemParamNameContractCode) - if err != nil { - return unit.Cents(0), nil, err - } - mtoAvailableToPrimeAt, err := getParamTime(params, models.ServiceItemParamNameMTOAvailableToPrimeAt) + lockedPriceCents, err := getParamInt(params, models.ServiceItemParamNameLockedPriceCents) if err != nil { return unit.Cents(0), nil, err } - return p.Price(appCtx, contractCode, mtoAvailableToPrimeAt) + lockedPrice := unit.Cents(lockedPriceCents) + return p.Price(appCtx, &lockedPrice) } diff --git a/pkg/services/ghcrateengine/counseling_services_pricer_test.go b/pkg/services/ghcrateengine/counseling_services_pricer_test.go index e59368337ac..258ee8ce40a 100644 --- a/pkg/services/ghcrateengine/counseling_services_pricer_test.go +++ b/pkg/services/ghcrateengine/counseling_services_pricer_test.go @@ -1,26 +1,21 @@ package ghcrateengine import ( - "time" - "github.com/transcom/mymove/pkg/factory" "github.com/transcom/mymove/pkg/models" "github.com/transcom/mymove/pkg/services" - "github.com/transcom/mymove/pkg/testdatagen" "github.com/transcom/mymove/pkg/unit" ) const ( - csPriceCents = unit.Cents(8327) + csPriceCents = unit.Cents(12303) ) -var csAvailableToPrimeAt = time.Date(testdatagen.TestYear, time.June, 5, 7, 33, 11, 456, time.UTC) - func (suite *GHCRateEngineServiceSuite) TestPriceCounselingServices() { + lockedPrice := csPriceCents counselingServicesPricer := NewCounselingServicesPricer() suite.Run("success using PaymentServiceItemParams", func() { - suite.setupTaskOrderFeeData(models.ReServiceCodeCS, csPriceCents) paymentServiceItem := suite.setupCounselingServicesItem() priceCents, displayParams, err := counselingServicesPricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) @@ -37,7 +32,7 @@ func (suite *GHCRateEngineServiceSuite) TestPriceCounselingServices() { suite.Run("success without PaymentServiceItemParams", func() { suite.setupTaskOrderFeeData(models.ReServiceCodeCS, csPriceCents) - priceCents, _, err := counselingServicesPricer.Price(suite.AppContextForTest(), testdatagen.DefaultContractCode, csAvailableToPrimeAt) + priceCents, _, err := counselingServicesPricer.Price(suite.AppContextForTest(), &lockedPrice) suite.NoError(err) suite.Equal(csPriceCents, priceCents) }) @@ -48,11 +43,6 @@ func (suite *GHCRateEngineServiceSuite) TestPriceCounselingServices() { _, _, err := counselingServicesPricer.PriceUsingParams(suite.AppContextForTest(), models.PaymentServiceItemParams{}) suite.Error(err) }) - - suite.Run("not finding a rate record", func() { - _, _, err := counselingServicesPricer.Price(suite.AppContextForTest(), "BOGUS", csAvailableToPrimeAt) - suite.Error(err) - }) } func (suite *GHCRateEngineServiceSuite) setupCounselingServicesItem() models.PaymentServiceItem { @@ -61,14 +51,9 @@ func (suite *GHCRateEngineServiceSuite) setupCounselingServicesItem() models.Pay models.ReServiceCodeCS, []factory.CreatePaymentServiceItemParams{ { - Key: models.ServiceItemParamNameContractCode, - KeyType: models.ServiceItemParamTypeString, - Value: factory.DefaultContractCode, - }, - { - Key: models.ServiceItemParamNameMTOAvailableToPrimeAt, - KeyType: models.ServiceItemParamTypeTimestamp, - Value: csAvailableToPrimeAt.Format(TimestampParamFormat), + Key: models.ServiceItemParamNameLockedPriceCents, + KeyType: models.ServiceItemParamTypeInteger, + Value: csPriceCents.ToMillicents().ToCents().String(), }, }, nil, nil, ) diff --git a/pkg/services/ghcrateengine/management_services_pricer.go b/pkg/services/ghcrateengine/management_services_pricer.go index f01c990a1de..995003c196b 100644 --- a/pkg/services/ghcrateengine/management_services_pricer.go +++ b/pkg/services/ghcrateengine/management_services_pricer.go @@ -2,7 +2,6 @@ package ghcrateengine import ( "fmt" - "time" "github.com/transcom/mymove/pkg/appcontext" "github.com/transcom/mymove/pkg/models" @@ -19,32 +18,30 @@ func NewManagementServicesPricer() services.ManagementServicesPricer { } // Price determines the price for a management service -func (p managementServicesPricer) Price(appCtx appcontext.AppContext, contractCode string, mtoAvailableToPrimeAt time.Time) (unit.Cents, services.PricingDisplayParams, error) { - taskOrderFee, err := fetchTaskOrderFee(appCtx, contractCode, models.ReServiceCodeMS, mtoAvailableToPrimeAt) - if err != nil { - return unit.Cents(0), nil, fmt.Errorf("could not fetch task order fee: %w", err) +func (p managementServicesPricer) Price(appCtx appcontext.AppContext, lockedPriceCents *unit.Cents) (unit.Cents, services.PricingDisplayParams, error) { + + if lockedPriceCents == nil { + return 0, nil, fmt.Errorf("invalid value for locked_price_cents") } + params := services.PricingDisplayParams{ { Key: models.ServiceItemParamNamePriceRateOrFactor, - Value: FormatCents(taskOrderFee.PriceCents), + Value: FormatCents(*lockedPriceCents), }, } - return taskOrderFee.PriceCents, params, nil + return *lockedPriceCents, params, nil } // PriceUsingParams determines the price for a management service given PaymentServiceItemParams func (p managementServicesPricer) PriceUsingParams(appCtx appcontext.AppContext, params models.PaymentServiceItemParams) (unit.Cents, services.PricingDisplayParams, error) { - contractCode, err := getParamString(params, models.ServiceItemParamNameContractCode) - if err != nil { - return unit.Cents(0), nil, err - } - mtoAvailableToPrimeAt, err := getParamTime(params, models.ServiceItemParamNameMTOAvailableToPrimeAt) + lockedPriceCents, err := getParamInt(params, models.ServiceItemParamNameLockedPriceCents) if err != nil { return unit.Cents(0), nil, err } - return p.Price(appCtx, contractCode, mtoAvailableToPrimeAt) + lockedPrice := unit.Cents(lockedPriceCents) + return p.Price(appCtx, &lockedPrice) } diff --git a/pkg/services/ghcrateengine/management_services_pricer_test.go b/pkg/services/ghcrateengine/management_services_pricer_test.go index 52355a2695b..01452e741c4 100644 --- a/pkg/services/ghcrateengine/management_services_pricer_test.go +++ b/pkg/services/ghcrateengine/management_services_pricer_test.go @@ -1,12 +1,9 @@ package ghcrateengine import ( - "time" - "github.com/transcom/mymove/pkg/factory" "github.com/transcom/mymove/pkg/models" "github.com/transcom/mymove/pkg/services" - "github.com/transcom/mymove/pkg/testdatagen" "github.com/transcom/mymove/pkg/unit" ) @@ -14,9 +11,8 @@ const ( msPriceCents = unit.Cents(12303) ) -var msAvailableToPrimeAt = time.Date(testdatagen.TestYear, time.June, 3, 12, 57, 33, 123, time.UTC) - func (suite *GHCRateEngineServiceSuite) TestPriceManagementServices() { + lockedPrice := csPriceCents suite.Run("success using PaymentServiceItemParams", func() { suite.setupTaskOrderFeeData(models.ReServiceCodeMS, msPriceCents) paymentServiceItem := suite.setupManagementServicesItem() @@ -37,7 +33,7 @@ func (suite *GHCRateEngineServiceSuite) TestPriceManagementServices() { suite.setupTaskOrderFeeData(models.ReServiceCodeMS, msPriceCents) managementServicesPricer := NewManagementServicesPricer() - priceCents, _, err := managementServicesPricer.Price(suite.AppContextForTest(), testdatagen.DefaultContractCode, msAvailableToPrimeAt) + priceCents, _, err := managementServicesPricer.Price(suite.AppContextForTest(), &lockedPrice) suite.NoError(err) suite.Equal(msPriceCents, priceCents) }) @@ -49,14 +45,6 @@ func (suite *GHCRateEngineServiceSuite) TestPriceManagementServices() { _, _, err := managementServicesPricer.PriceUsingParams(suite.AppContextForTest(), models.PaymentServiceItemParams{}) suite.Error(err) }) - - suite.Run("not finding a rate record", func() { - suite.setupTaskOrderFeeData(models.ReServiceCodeMS, msPriceCents) - managementServicesPricer := NewManagementServicesPricer() - - _, _, err := managementServicesPricer.Price(suite.AppContextForTest(), "BOGUS", msAvailableToPrimeAt) - suite.Error(err) - }) } func (suite *GHCRateEngineServiceSuite) setupManagementServicesItem() models.PaymentServiceItem { @@ -65,14 +53,9 @@ func (suite *GHCRateEngineServiceSuite) setupManagementServicesItem() models.Pay models.ReServiceCodeMS, []factory.CreatePaymentServiceItemParams{ { - Key: models.ServiceItemParamNameContractCode, - KeyType: models.ServiceItemParamTypeString, - Value: factory.DefaultContractCode, - }, - { - Key: models.ServiceItemParamNameMTOAvailableToPrimeAt, - KeyType: models.ServiceItemParamTypeTimestamp, - Value: msAvailableToPrimeAt.Format(TimestampParamFormat), + Key: models.ServiceItemParamNameLockedPriceCents, + KeyType: models.ServiceItemParamTypeInteger, + Value: msPriceCents.ToMillicents().ToCents().String(), }, }, nil, nil, ) diff --git a/pkg/services/ghcrateengine/service_item_pricer_test.go b/pkg/services/ghcrateengine/service_item_pricer_test.go index ca9ae0cb724..6ebfec34a29 100644 --- a/pkg/services/ghcrateengine/service_item_pricer_test.go +++ b/pkg/services/ghcrateengine/service_item_pricer_test.go @@ -115,14 +115,9 @@ func (suite *GHCRateEngineServiceSuite) setupPriceServiceItem() models.PaymentSe models.ReServiceCodeMS, []factory.CreatePaymentServiceItemParams{ { - Key: models.ServiceItemParamNameContractCode, - KeyType: models.ServiceItemParamTypeString, - Value: factory.DefaultContractCode, - }, - { - Key: models.ServiceItemParamNameMTOAvailableToPrimeAt, - KeyType: models.ServiceItemParamTypeTimestamp, - Value: msAvailableToPrimeAt.Format(TimestampParamFormat), + Key: models.ServiceItemParamNameLockedPriceCents, + KeyType: models.ServiceItemParamTypeInteger, + Value: msPriceCents.ToMillicents().ToCents().String(), }, }, nil, nil, ) diff --git a/pkg/services/invoice/process_edi997.go b/pkg/services/invoice/process_edi997.go index c67fbc1227b..27f65603b2a 100644 --- a/pkg/services/invoice/process_edi997.go +++ b/pkg/services/invoice/process_edi997.go @@ -2,6 +2,7 @@ package invoice import ( "fmt" + "time" "github.com/gofrs/uuid" "go.uber.org/zap" @@ -105,6 +106,8 @@ func (e *edi997Processor) ProcessFile(appCtx appcontext.AppContext, _ string, st } paymentRequest.Status = models.PaymentRequestStatusTppsReceived + ReceivedByGexAt := time.Now() + paymentRequest.ReceivedByGexAt = &ReceivedByGexAt err = txnAppCtx.DB().Update(&paymentRequest) if err != nil { txnAppCtx.Logger().Error("failure updating payment request", zap.Error(err)) diff --git a/pkg/services/invoice/process_edi997_test.go b/pkg/services/invoice/process_edi997_test.go index 7287b043c7f..39d4c34cba3 100644 --- a/pkg/services/invoice/process_edi997_test.go +++ b/pkg/services/invoice/process_edi997_test.go @@ -196,6 +196,7 @@ IEA*1*000000995 err = suite.DB().Where("id = ?", paymentRequest.ID).First(&updatedPR) suite.NoError(err) suite.Equal(models.PaymentRequestStatusTppsReceived, updatedPR.Status) + suite.NotNil(updatedPR.ReceivedByGexAt) }) suite.Run("can handle 997 and 858 with same ICN", func() { @@ -250,6 +251,7 @@ IEA*1*000000995 err = suite.DB().Where("id = ?", paymentRequest.ID).First(&updatedPR) suite.FatalNoError(err) suite.Equal(models.PaymentRequestStatusTppsReceived, updatedPR.Status) + suite.NotNil(updatedPR.ReceivedByGexAt) }) suite.Run("does not error out if edi with same icn is processed for the same payment request", func() { @@ -304,6 +306,7 @@ IEA*1*000000995 err = suite.DB().Where("id = ?", paymentRequest.ID).First(&updatedPR) suite.FatalNoError(err) suite.Equal(models.PaymentRequestStatusTppsReceived, updatedPR.Status) + suite.NotNil(updatedPR.ReceivedByGexAt) }) suite.Run("doesn't update a payment request status after processing an invalid EDI997", func() { @@ -345,6 +348,7 @@ IEA*1*000000022 err = suite.DB().Where("id = ?", paymentRequest.ID).First(&updatedPR) suite.NoError(err) suite.Equal(models.PaymentRequestStatusSentToGex, updatedPR.Status) + suite.Nil(updatedPR.ReceivedByGexAt) }) suite.Run("throw an error when edi997 is missing a transaction set", func() { diff --git a/pkg/services/mocks/CounselingServicesPricer.go b/pkg/services/mocks/CounselingServicesPricer.go index ea987e819c8..4262468e29f 100644 --- a/pkg/services/mocks/CounselingServicesPricer.go +++ b/pkg/services/mocks/CounselingServicesPricer.go @@ -10,8 +10,6 @@ import ( services "github.com/transcom/mymove/pkg/services" - time "time" - unit "github.com/transcom/mymove/pkg/unit" ) @@ -20,9 +18,9 @@ type CounselingServicesPricer struct { mock.Mock } -// Price provides a mock function with given fields: appCtx, contractCode, mtoAvailableToPrimeAt -func (_m *CounselingServicesPricer) Price(appCtx appcontext.AppContext, contractCode string, mtoAvailableToPrimeAt time.Time) (unit.Cents, services.PricingDisplayParams, error) { - ret := _m.Called(appCtx, contractCode, mtoAvailableToPrimeAt) +// Price provides a mock function with given fields: appCtx, lockedPriceCents +func (_m *CounselingServicesPricer) Price(appCtx appcontext.AppContext, lockedPriceCents *unit.Cents) (unit.Cents, services.PricingDisplayParams, error) { + ret := _m.Called(appCtx, lockedPriceCents) if len(ret) == 0 { panic("no return value specified for Price") @@ -31,25 +29,25 @@ func (_m *CounselingServicesPricer) Price(appCtx appcontext.AppContext, contract var r0 unit.Cents var r1 services.PricingDisplayParams var r2 error - if rf, ok := ret.Get(0).(func(appcontext.AppContext, string, time.Time) (unit.Cents, services.PricingDisplayParams, error)); ok { - return rf(appCtx, contractCode, mtoAvailableToPrimeAt) + if rf, ok := ret.Get(0).(func(appcontext.AppContext, *unit.Cents) (unit.Cents, services.PricingDisplayParams, error)); ok { + return rf(appCtx, lockedPriceCents) } - if rf, ok := ret.Get(0).(func(appcontext.AppContext, string, time.Time) unit.Cents); ok { - r0 = rf(appCtx, contractCode, mtoAvailableToPrimeAt) + if rf, ok := ret.Get(0).(func(appcontext.AppContext, *unit.Cents) unit.Cents); ok { + r0 = rf(appCtx, lockedPriceCents) } else { r0 = ret.Get(0).(unit.Cents) } - if rf, ok := ret.Get(1).(func(appcontext.AppContext, string, time.Time) services.PricingDisplayParams); ok { - r1 = rf(appCtx, contractCode, mtoAvailableToPrimeAt) + if rf, ok := ret.Get(1).(func(appcontext.AppContext, *unit.Cents) services.PricingDisplayParams); ok { + r1 = rf(appCtx, lockedPriceCents) } else { if ret.Get(1) != nil { r1 = ret.Get(1).(services.PricingDisplayParams) } } - if rf, ok := ret.Get(2).(func(appcontext.AppContext, string, time.Time) error); ok { - r2 = rf(appCtx, contractCode, mtoAvailableToPrimeAt) + if rf, ok := ret.Get(2).(func(appcontext.AppContext, *unit.Cents) error); ok { + r2 = rf(appCtx, lockedPriceCents) } else { r2 = ret.Error(2) } diff --git a/pkg/services/mocks/ManagementServicesPricer.go b/pkg/services/mocks/ManagementServicesPricer.go index 2d77f2a9767..3cac867eee4 100644 --- a/pkg/services/mocks/ManagementServicesPricer.go +++ b/pkg/services/mocks/ManagementServicesPricer.go @@ -10,8 +10,6 @@ import ( services "github.com/transcom/mymove/pkg/services" - time "time" - unit "github.com/transcom/mymove/pkg/unit" ) @@ -20,9 +18,9 @@ type ManagementServicesPricer struct { mock.Mock } -// Price provides a mock function with given fields: appCtx, contractCode, mtoAvailableToPrimeAt -func (_m *ManagementServicesPricer) Price(appCtx appcontext.AppContext, contractCode string, mtoAvailableToPrimeAt time.Time) (unit.Cents, services.PricingDisplayParams, error) { - ret := _m.Called(appCtx, contractCode, mtoAvailableToPrimeAt) +// Price provides a mock function with given fields: appCtx, lockedPriceCents +func (_m *ManagementServicesPricer) Price(appCtx appcontext.AppContext, lockedPriceCents *unit.Cents) (unit.Cents, services.PricingDisplayParams, error) { + ret := _m.Called(appCtx, lockedPriceCents) if len(ret) == 0 { panic("no return value specified for Price") @@ -31,25 +29,25 @@ func (_m *ManagementServicesPricer) Price(appCtx appcontext.AppContext, contract var r0 unit.Cents var r1 services.PricingDisplayParams var r2 error - if rf, ok := ret.Get(0).(func(appcontext.AppContext, string, time.Time) (unit.Cents, services.PricingDisplayParams, error)); ok { - return rf(appCtx, contractCode, mtoAvailableToPrimeAt) + if rf, ok := ret.Get(0).(func(appcontext.AppContext, *unit.Cents) (unit.Cents, services.PricingDisplayParams, error)); ok { + return rf(appCtx, lockedPriceCents) } - if rf, ok := ret.Get(0).(func(appcontext.AppContext, string, time.Time) unit.Cents); ok { - r0 = rf(appCtx, contractCode, mtoAvailableToPrimeAt) + if rf, ok := ret.Get(0).(func(appcontext.AppContext, *unit.Cents) unit.Cents); ok { + r0 = rf(appCtx, lockedPriceCents) } else { r0 = ret.Get(0).(unit.Cents) } - if rf, ok := ret.Get(1).(func(appcontext.AppContext, string, time.Time) services.PricingDisplayParams); ok { - r1 = rf(appCtx, contractCode, mtoAvailableToPrimeAt) + if rf, ok := ret.Get(1).(func(appcontext.AppContext, *unit.Cents) services.PricingDisplayParams); ok { + r1 = rf(appCtx, lockedPriceCents) } else { if ret.Get(1) != nil { r1 = ret.Get(1).(services.PricingDisplayParams) } } - if rf, ok := ret.Get(2).(func(appcontext.AppContext, string, time.Time) error); ok { - r2 = rf(appCtx, contractCode, mtoAvailableToPrimeAt) + if rf, ok := ret.Get(2).(func(appcontext.AppContext, *unit.Cents) error); ok { + r2 = rf(appCtx, lockedPriceCents) } else { r2 = ret.Error(2) } diff --git a/pkg/services/mto_shipment/mto_shipment_address_updater.go b/pkg/services/mto_shipment/mto_shipment_address_updater.go index 1f9ea1d08ba..edab362940f 100644 --- a/pkg/services/mto_shipment/mto_shipment_address_updater.go +++ b/pkg/services/mto_shipment/mto_shipment_address_updater.go @@ -36,6 +36,8 @@ func isAddressOnShipment(address *models.Address, mtoShipment *models.MTOShipmen mtoShipment.DestinationAddressID, mtoShipment.SecondaryDeliveryAddressID, mtoShipment.SecondaryPickupAddressID, + mtoShipment.TertiaryDeliveryAddressID, + mtoShipment.TertiaryPickupAddressID, } for _, id := range addressIDs { diff --git a/pkg/services/payment_request.go b/pkg/services/payment_request.go index 5f81f2215c9..25f62be43a4 100644 --- a/pkg/services/payment_request.go +++ b/pkg/services/payment_request.go @@ -5,6 +5,7 @@ import ( "time" "github.com/gofrs/uuid" + "github.com/spf13/afero" "github.com/transcom/mymove/pkg/appcontext" "github.com/transcom/mymove/pkg/models" @@ -115,3 +116,7 @@ type ShipmentPaymentSITBalance struct { type ShipmentsPaymentSITBalance interface { ListShipmentPaymentSITBalance(appCtx appcontext.AppContext, paymentRequestID uuid.UUID) ([]ShipmentPaymentSITBalance, error) } + +type PaymentRequestBulkDownloadCreator interface { + CreatePaymentRequestBulkDownload(appCtx appcontext.AppContext, paymentRequestID uuid.UUID) (afero.File, error) +} diff --git a/pkg/services/payment_request/payment_request_bulk_download_creator.go b/pkg/services/payment_request/payment_request_bulk_download_creator.go new file mode 100644 index 00000000000..1178f4c991c --- /dev/null +++ b/pkg/services/payment_request/payment_request_bulk_download_creator.go @@ -0,0 +1,57 @@ +package paymentrequest + +import ( + "fmt" + + "github.com/gofrs/uuid" + "github.com/spf13/afero" + + "github.com/transcom/mymove/pkg/appcontext" + "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/paperwork" + "github.com/transcom/mymove/pkg/services" +) + +type paymentRequestBulkDownloadCreator struct { + pdfGenerator *paperwork.Generator +} + +func NewPaymentRequestBulkDownloadCreator(pdfGenerator *paperwork.Generator) services.PaymentRequestBulkDownloadCreator { + return &paymentRequestBulkDownloadCreator{ + pdfGenerator, + } +} + +func (p *paymentRequestBulkDownloadCreator) CreatePaymentRequestBulkDownload(appCtx appcontext.AppContext, paymentRequestID uuid.UUID) (afero.File, error) { + errMsgPrefix := "error creating Payment Request packet" + + paymentRequest := models.PaymentRequest{} + err := appCtx.DB().Q().Eager( + "MoveTaskOrder", + "ProofOfServiceDocs", + "ProofOfServiceDocs.PrimeUploads", + "ProofOfServiceDocs.PrimeUploads.Upload", + ).Find(&paymentRequest, paymentRequestID) + if err != nil || len(paymentRequest.ProofOfServiceDocs) < 1 { + return nil, fmt.Errorf("%s: %w", errMsgPrefix, err) + } + + var primeUploads models.Uploads + for _, serviceDoc := range paymentRequest.ProofOfServiceDocs { + for _, upload := range serviceDoc.PrimeUploads { + primeUploads = append(primeUploads, upload.Upload) + } + } + + pdfs, err := p.pdfGenerator.ConvertUploadsToPDF(appCtx, primeUploads, false) + if err != nil { + return nil, fmt.Errorf("%s error generating pdf", err) + } + + pdfFile, err := p.pdfGenerator.MergePDFFiles(appCtx, pdfs) + if err != nil { + return nil, fmt.Errorf("%s error generating merged pdf", err) + } + + return pdfFile, nil +} diff --git a/pkg/services/payment_request/payment_request_recalculator_test.go b/pkg/services/payment_request/payment_request_recalculator_test.go index 82da8ed74c2..55b4a2ec47c 100644 --- a/pkg/services/payment_request/payment_request_recalculator_test.go +++ b/pkg/services/payment_request/payment_request_recalculator_test.go @@ -23,8 +23,8 @@ import ( const ( recalculateTestPickupZip = "30907" recalculateTestDestinationZip = "78234" - recalculateTestMSFee = unit.Cents(25513) - recalculateTestCSFee = unit.Cents(22399) + recalculateTestMSFee = unit.Cents(12303) + recalculateTestCSFee = unit.Cents(12303) recalculateTestDLHPrice = unit.Millicents(6000) recalculateTestFSCPrice = unit.Millicents(277600) recalculateTestDomOtherPrice = unit.Cents(2159) @@ -154,12 +154,12 @@ func (suite *PaymentRequestServiceSuite) TestRecalculatePaymentRequestSuccess() { paymentRequest: &oldPaymentRequest, serviceCode: models.ReServiceCodeMS, - priceCents: unit.Cents(25513), + priceCents: unit.Cents(12303), }, { paymentRequest: &oldPaymentRequest, serviceCode: models.ReServiceCodeCS, - priceCents: unit.Cents(22399), + priceCents: unit.Cents(12303), }, { paymentRequest: &oldPaymentRequest, @@ -196,13 +196,13 @@ func (suite *PaymentRequestServiceSuite) TestRecalculatePaymentRequestSuccess() isNewPaymentRequest: true, paymentRequest: newPaymentRequest, serviceCode: models.ReServiceCodeMS, - priceCents: unit.Cents(25513), + priceCents: unit.Cents(12303), }, { isNewPaymentRequest: true, paymentRequest: newPaymentRequest, serviceCode: models.ReServiceCodeCS, - priceCents: unit.Cents(22399), + priceCents: unit.Cents(12303), }, { isNewPaymentRequest: true, @@ -407,24 +407,6 @@ func (suite *PaymentRequestServiceSuite) setupRecalculateData1() (models.Move, m }, }) - // MS price data - msService := factory.BuildReServiceByCode(suite.DB(), models.ReServiceCodeMS) - msTaskOrderFee := models.ReTaskOrderFee{ - ContractYearID: contractYear.ID, - ServiceID: msService.ID, - PriceCents: recalculateTestMSFee, - } - suite.MustSave(&msTaskOrderFee) - - // CS price data - csService := factory.BuildReServiceByCode(suite.DB(), models.ReServiceCodeCS) - csTaskOrderFee := models.ReTaskOrderFee{ - ContractYearID: contractYear.ID, - ServiceID: csService.ID, - PriceCents: recalculateTestCSFee, - } - suite.MustSave(&csTaskOrderFee) - // DLH price data testdatagen.MakeReDomesticLinehaulPrice(suite.DB(), testdatagen.Assertions{ ReDomesticLinehaulPrice: models.ReDomesticLinehaulPrice{ diff --git a/playwright/tests/office/primesimulator/primeSimulatorFlows.spec.js b/playwright/tests/office/primesimulator/primeSimulatorFlows.spec.js index 5b6f56fbb57..a6cc84e9f04 100644 --- a/playwright/tests/office/primesimulator/primeSimulatorFlows.spec.js +++ b/playwright/tests/office/primesimulator/primeSimulatorFlows.spec.js @@ -148,7 +148,7 @@ test.describe('Prime simulator user', () => { await page.locator('input[name="destinationAddress.city"]').fill('Joshua Tree'); await page.locator('select[name="destinationAddress.state"]').selectOption({ label: 'CA' }); await page.locator('input[name="destinationAddress.postalCode"]').fill('92252'); - await page.getByTestId('dropdown').nth(1).selectOption('Home of record (HOR)'); + await page.getByTestId('dropdown').nth(5).selectOption('Home of record (HOR)'); await page.getByText('Save').click(); await expect(page.getByText('Successfully updated shipment')).toHaveCount(1); diff --git a/playwright/tests/utils/waitForPage.js b/playwright/tests/utils/waitForPage.js index 7a79f419f38..08f582ce3f9 100644 --- a/playwright/tests/utils/waitForPage.js +++ b/playwright/tests/utils/waitForPage.js @@ -19,18 +19,17 @@ export class WaitForPage { */ async runAccessibilityAudit() { - if (process.env.A11Y_AUDIT) { - await checkA11y( - this.page, - undefined, - { - detailedReport: true, - }, - // skip failures - true, - 'default', - ); - } + await checkA11y( + this.page, + undefined, + { + detailedReport: true, + detailedReportOptions: { html: true }, + }, + // skip failures + false, + 'html', + ); } /** diff --git a/scripts/run-e2e-test b/scripts/run-e2e-test index 92cce285789..e0272b245c0 100755 --- a/scripts/run-e2e-test +++ b/scripts/run-e2e-test @@ -31,4 +31,4 @@ done trap cleanup SIGINT trap cleanup exit -yarn playwright test "$@" +A11Y_AUDIT=true yarn playwright test "$@" diff --git a/src/components/CustomerHeader/CustomerHeader.test.jsx b/src/components/CustomerHeader/CustomerHeader.test.jsx index 1668afb9f29..fec1c7c7b73 100644 --- a/src/components/CustomerHeader/CustomerHeader.test.jsx +++ b/src/components/CustomerHeader/CustomerHeader.test.jsx @@ -5,7 +5,7 @@ import { mount } from 'enzyme'; import CustomerHeader from './index'; const props = { - customer: { last_name: 'Kerry', first_name: 'Smith', dodID: '999999999', emplid: '7777777', agency: 'COAST_GUARD' }, + customer: { last_name: 'Kerry', first_name: 'Smith', edipi: '999999999', emplid: '7777777', agency: 'COAST_GUARD' }, order: { agency: 'COAST_GUARD', grade: 'E_6', @@ -25,7 +25,7 @@ const props = { }; const propsRetiree = { - customer: { last_name: 'Kerry', first_name: 'Smith', dodID: '999999999' }, + customer: { last_name: 'Kerry', first_name: 'Smith', edipi: '999999999' }, order: { agency: 'NAVY', grade: 'E_6', @@ -45,7 +45,7 @@ const propsRetiree = { }; const propsUSMC = { - customer: { last_name: 'Kerry', first_name: 'Smith', dodID: '999999999' }, + customer: { last_name: 'Kerry', first_name: 'Smith', edipi: '999999999' }, order: { agency: 'MARINES', grade: 'E_6', @@ -77,7 +77,7 @@ describe('CustomerHeader component', () => { expect(wrapper.find('[data-testid="nameBlock"]').text()).toContain('Kerry, Smith'); expect(wrapper.find('[data-testid="nameBlock"]').text()).toContain('FKLCTR'); expect(wrapper.find('[data-testid="deptPayGrade"]').text()).toContain('Coast Guard E-6'); - expect(wrapper.find('[data-testid="dodId"]').text()).toContain('DoD ID 999999999'); + expect(wrapper.find('[data-testid="edipi"]').text()).toContain('DoD ID 999999999'); expect(wrapper.find('[data-testid="emplid"]').text()).toContain('EMPLID 7777777'); expect(wrapper.find('[data-testid="infoBlock"]').text()).toContain('JBSA Lackland'); expect(wrapper.find('[data-testid="infoBlock"]').text()).toContain('JB Lewis-McChord'); diff --git a/src/components/CustomerHeader/index.jsx b/src/components/CustomerHeader/index.jsx index 36d8ebc037a..857e2469997 100644 --- a/src/components/CustomerHeader/index.jsx +++ b/src/components/CustomerHeader/index.jsx @@ -55,8 +55,8 @@ const CustomerHeader = ({ customer, order, moveCode, move, userRole }) => { {ORDERS_BRANCH_OPTIONS[`${order.agency}`]} {ORDERS_PAY_GRADE_OPTIONS[`${order.grade}`]} | - - DoD ID {customer.dodID} + + DoD ID {customer.edipi} {isCoastGuard && ( <> diff --git a/src/components/DocumentViewer/DocumentViewer.jsx b/src/components/DocumentViewer/DocumentViewer.jsx index 98494e3f163..73daff482d3 100644 --- a/src/components/DocumentViewer/DocumentViewer.jsx +++ b/src/components/DocumentViewer/DocumentViewer.jsx @@ -12,9 +12,10 @@ import Menu from './Menu/Menu'; import { milmoveLogger } from 'utils/milmoveLog'; import { UPLOADS } from 'constants/queryKeys'; -import { updateUpload } from 'services/ghcApi'; +import { bulkDownloadPaymentRequest, updateUpload } from 'services/ghcApi'; import { formatDate } from 'shared/dates'; import { filenameFromPath } from 'utils/formatters'; +import AsyncPacketDownloadLink from 'shared/AsyncPacketDownloadLink/AsyncPacketDownloadLink'; /** * TODO @@ -23,7 +24,7 @@ import { filenameFromPath } from 'utils/formatters'; * - handle fetch doc errors */ -const DocumentViewer = ({ files, allowDownload }) => { +const DocumentViewer = ({ files, allowDownload, paymentRequestId }) => { const [selectedFileIndex, selectFile] = useState(0); const [disableSaveButton, setDisableSaveButton] = useState(false); const [menuIsOpen, setMenuOpen] = useState(false); @@ -117,6 +118,20 @@ const DocumentViewer = ({ files, allowDownload }) => { } }; + const paymentPacketDownload = ( +
+
+

+ +

+
+
+ ); + return (
@@ -133,6 +148,7 @@ const DocumentViewer = ({ files, allowDownload }) => {

)} + {paymentRequestId !== undefined ? paymentPacketDownload : null}
{ const container = document.querySelector('[data-testid="menuButtonContainer"]'); if (container) { @@ -50,6 +52,11 @@ const mockFiles = [ }, ]; +jest.mock('services/ghcApi', () => ({ + ...jest.requireActual('services/ghcApi'), + bulkDownloadPaymentRequest: jest.fn(), +})); + jest.mock('./Content/Content', () => ({ __esModule: true, default: ({ id, filename, contentType, url, createdAt, rotation }) => ( @@ -172,4 +179,37 @@ describe('DocumentViewer component', () => { expect(screen.getByText('id: undefined')).toBeInTheDocument(); }); + + describe('when clicking download Download All Files button', () => { + it('downloads a bulk packet', async () => { + const mockResponse = { + ok: true, + headers: { + 'content-disposition': 'filename="test.pdf"', + }, + status: 200, + data: null, + }; + + render( + + + , + ); + + bulkDownloadPaymentRequest.mockImplementation(() => Promise.resolve(mockResponse)); + + const downloadButton = screen.getByText('Download All Files (PDF)', { exact: false }); + await userEvent.click(downloadButton); + await waitFor(() => { + expect(bulkDownloadPaymentRequest).toHaveBeenCalledTimes(1); + }); + }); + }); }); diff --git a/src/components/Office/DefinitionLists/CustomerInfoList.jsx b/src/components/Office/DefinitionLists/CustomerInfoList.jsx index b1b79d9ac01..16a698529f4 100644 --- a/src/components/Office/DefinitionLists/CustomerInfoList.jsx +++ b/src/components/Office/DefinitionLists/CustomerInfoList.jsx @@ -19,7 +19,7 @@ const CustomerInfoList = ({ customerInfo }) => {
DoD ID
-
{customerInfo.dodId}
+
{customerInfo.edipi}
{customerInfo.agency === departmentIndicators.COAST_GUARD && (
@@ -81,7 +81,7 @@ const CustomerInfoList = ({ customerInfo }) => { CustomerInfoList.propTypes = { customerInfo: PropTypes.shape({ name: PropTypes.string, - dodId: PropTypes.string, + edipi: PropTypes.string, phone: PropTypes.string, email: PropTypes.string, currentAddress: AddressShape, diff --git a/src/components/Office/DefinitionLists/CustomerInfoList.test.jsx b/src/components/Office/DefinitionLists/CustomerInfoList.test.jsx index 18682850c78..d957be52b4f 100644 --- a/src/components/Office/DefinitionLists/CustomerInfoList.test.jsx +++ b/src/components/Office/DefinitionLists/CustomerInfoList.test.jsx @@ -6,7 +6,7 @@ import CustomerInfoList from './CustomerInfoList'; const info = { name: 'Smith, Kerry', agency: 'COAST_GUARD', - dodId: '9999999999', + edipi: '9999999999', emplid: '7777777', phone: '999-999-9999', altPhone: '888-888-8888', diff --git a/src/components/Office/PaymentRequestCard/PaymentRequestCard.jsx b/src/components/Office/PaymentRequestCard/PaymentRequestCard.jsx index 8e1ed250eec..88ab4ab916d 100644 --- a/src/components/Office/PaymentRequestCard/PaymentRequestCard.jsx +++ b/src/components/Office/PaymentRequestCard/PaymentRequestCard.jsx @@ -265,12 +265,8 @@ const PaymentRequestCard = ({ ); }; - const renderPaymentRequestDetailsForStatus = (paymentRequestStatus) => { - if ( - (paymentRequestStatus === PAYMENT_REQUEST_STATUS.PAID || - paymentRequestStatus === PAYMENT_REQUEST_STATUS.EDI_ERROR) && - tppsInvoiceSellerPaidDate - ) { + const renderApprovedRejectedPaymentRequestDetails = () => { + if (approvedAmount > 0 || rejectedAmount > 0) { return (
{approvedAmount > 0 && ( @@ -293,6 +289,20 @@ const PaymentRequestCard = ({
)} + + ); + } + return null; + }; + + const renderPaymentRequestDetailsForStatus = (paymentRequestStatus) => { + if ( + (paymentRequestStatus === PAYMENT_REQUEST_STATUS.PAID || + paymentRequestStatus === PAYMENT_REQUEST_STATUS.EDI_ERROR) && + tppsInvoiceSellerPaidDate + ) { + return ( +
{tppsInvoiceAmountPaidTotalMillicents > 0 && (
@@ -313,22 +323,12 @@ const PaymentRequestCard = ({ ) { return (
- {approvedAmount > 0 && ( + {paymentRequest.receivedByGexAt && (
-
+

{toDollarString(formatCents(approvedAmount))}

- Received - on {formatDateFromIso(paymentRequest.receivedByGexAt, 'DD MMM YYYY')} -
-
- )} - {rejectedAmount > 0 && ( -
- -
-

{toDollarString(formatCents(rejectedAmount))}

- Rejected + TPPS Received on {formatDateFromIso(paymentRequest.receivedByGexAt, 'DD MMM YYYY')}
@@ -336,36 +336,7 @@ const PaymentRequestCard = ({
); } - if ( - paymentRequestStatus === PAYMENT_REQUEST_STATUS.REVIEWED || - paymentRequestStatus === PAYMENT_REQUEST_STATUS.REVIEWED_AND_ALL_SERVICE_ITEMS_REJECTED || - paymentRequestStatus === PAYMENT_REQUEST_STATUS.EDI_ERROR - ) { - return ( -
- {approvedAmount > 0 && ( -
- -
-

{toDollarString(formatCents(approvedAmount))}

- Accepted - on {formatDateFromIso(paymentRequest.reviewedAt, 'DD MMM YYYY')} -
-
- )} - {rejectedAmount > 0 && ( -
- -
-

{toDollarString(formatCents(rejectedAmount))}

- Rejected - on {formatDateFromIso(paymentRequest.reviewedAt, 'DD MMM YYYY')} -
-
- )} -
- ); - } + if ( paymentRequestStatus === PAYMENT_REQUEST_STATUS.SENT_TO_GEX || (paymentRequestStatus === PAYMENT_REQUEST_STATUS.EDI_ERROR && approvedAmount > 0) @@ -373,7 +344,7 @@ const PaymentRequestCard = ({ return (
-
+

{toDollarString(formatCents(approvedAmount))}

Sent to GEX @@ -395,7 +366,7 @@ const PaymentRequestCard = ({
); } - return
; + return null; }; return ( @@ -420,7 +391,10 @@ const PaymentRequestCard = ({
-
{paymentRequest.status && renderPaymentRequestDetailsForStatus(paymentRequest.status)}
+
+ {paymentRequest.status && renderApprovedRejectedPaymentRequestDetails(paymentRequest)} + {paymentRequest.status && renderPaymentRequestDetailsForStatus(paymentRequest.status)} +
{paymentRequest.status === PAYMENT_REQUEST_STATUS.PENDING && renderReviewServiceItemsBtnForTIOandTOO()}
{ediErrorsExistForPaymentRequest && renderEDIErrorDetails()} diff --git a/src/components/Office/PaymentRequestCard/PaymentRequestCard.test.jsx b/src/components/Office/PaymentRequestCard/PaymentRequestCard.test.jsx index 04486d323c7..1443e72d928 100644 --- a/src/components/Office/PaymentRequestCard/PaymentRequestCard.test.jsx +++ b/src/components/Office/PaymentRequestCard/PaymentRequestCard.test.jsx @@ -601,7 +601,7 @@ describe('PaymentRequestCard', () => { createdAt: '2020-12-01T00:00:00.000Z', mtoServiceItemID: 'f8c2f97f-99e7-4fb1-9cc4-473debd24dbc', priceCents: 2000001, - status: 'DENIED', + status: 'APPROVED', }, { id: '39474c6a-69b6-4501-8e08-670a12512a5f', @@ -626,6 +626,14 @@ describe('PaymentRequestCard', () => { ); expect(sentToGex.find({ 'data-testid': 'tag' }).contains('Sent to GEX')).toBe(true); expect(sentToGex.find({ 'data-testid': 'sentToGexDetails' }).exists()).toBe(true); + // displays the sent to gex sum, milmove accepted amount, and milmove rejected amount + expect(sentToGex.find({ 'data-testid': 'sentToGexDetailsDollarAmountTotal' }).contains('$20,000.01')).toBe(true); + expect(sentToGex.find({ 'data-testid': 'milMoveAcceptedDetailsDollarAmountTotal' }).contains('$20,000.01')).toBe( + true, + ); + expect(sentToGex.find({ 'data-testid': 'milMoveRejectedDetailsDollarAmountTotal' }).contains('$40,000.01')).toBe( + true, + ); }); it('renders - for the date it was sent to gex if sentToGexAt is null', () => { @@ -653,13 +661,14 @@ describe('PaymentRequestCard', () => { paymentRequestNumber: '1843-9061-2', status: 'TPPS_RECEIVED', moveTaskOrder: move, + receivedByGexAt: '2020-12-01T00:00:00.000Z', serviceItems: [ { id: '09474c6a-69b6-4501-8e08-670a12512a5f', createdAt: '2020-12-01T00:00:00.000Z', mtoServiceItemID: 'f8c2f97f-99e7-4fb1-9cc4-473debd24dbc', priceCents: 2000001, - status: 'DENIED', + status: 'APPROVED', }, { id: '39474c6a-69b6-4501-8e08-670a12512a5f', @@ -681,6 +690,16 @@ describe('PaymentRequestCard', () => { , ); expect(receivedByGex.find({ 'data-testid': 'tag' }).contains('TPPS Received')).toBe(true); + // displays the tpps received sum, milmove accepted amount, and milmove rejected amount + expect(receivedByGex.find({ 'data-testid': 'tppsReceivedDetailsDollarAmountTotal' }).contains('$20,000.01')).toBe( + true, + ); + expect( + receivedByGex.find({ 'data-testid': 'milMoveAcceptedDetailsDollarAmountTotal' }).contains('$20,000.01'), + ).toBe(true); + expect( + receivedByGex.find({ 'data-testid': 'milMoveRejectedDetailsDollarAmountTotal' }).contains('$40,000.01'), + ).toBe(true); }); it('renders the paid status tag for paid request', () => { diff --git a/src/components/Office/ServicesCounselingTabNav/ServicesCounselingTabNav.jsx b/src/components/Office/ServicesCounselingTabNav/ServicesCounselingTabNav.jsx index 3dc4c9dadab..2d557e2c874 100644 --- a/src/components/Office/ServicesCounselingTabNav/ServicesCounselingTabNav.jsx +++ b/src/components/Office/ServicesCounselingTabNav/ServicesCounselingTabNav.jsx @@ -10,7 +10,7 @@ import 'styles/office.scss'; import TabNav from 'components/TabNav'; import { isBooleanFlagEnabled } from 'utils/featureFlags'; -const ServicesCounselingTabNav = ({ unapprovedShipmentCount = 0, moveCode }) => { +const ServicesCounselingTabNav = ({ unapprovedShipmentCount = 0, missingOrdersInfoCount, moveCode }) => { const [supportingDocsFF, setSupportingDocsFF] = React.useState(false); React.useEffect(() => { const fetchData = async () => { @@ -19,6 +19,14 @@ const ServicesCounselingTabNav = ({ unapprovedShipmentCount = 0, moveCode }) => fetchData(); }, []); + let moveDetailsTagCount = 0; + if (unapprovedShipmentCount > 0) { + moveDetailsTagCount += unapprovedShipmentCount; + } + if (missingOrdersInfoCount > 0) { + moveDetailsTagCount += missingOrdersInfoCount; + } + const items = [ data-testid="MoveDetails-Tab" > Move details - {unapprovedShipmentCount > 0 && {unapprovedShipmentCount}} + {moveDetailsTagCount > 0 && {moveDetailsTagCount}} , { expect(within(moveDetailsTab).queryByTestId('tag')).not.toBeInTheDocument(); }); - it('should render the move details tab container with a tag that shows the count of unapproved shipments', () => { + it('should render the move details tab container with a tag that shows the count of action items', () => { const moveDetailsShipmentAndAmendedOrders = { ...basicNavProps, unapprovedShipmentCount: 6, + missingOrdersInfoCount: 4, }; render(, { wrapper: MemoryRouter }); const moveDetailsTab = screen.getByTestId('MoveDetails-Tab'); - expect(within(moveDetailsTab).getByTestId('tag')).toHaveTextContent('6'); + expect(within(moveDetailsTab).getByTestId('tag')).toHaveTextContent('10'); }); }); diff --git a/src/components/Office/TXOTabNav/TXOTabNav.jsx b/src/components/Office/TXOTabNav/TXOTabNav.jsx index 9732c46407e..d3db9fbd1b9 100644 --- a/src/components/Office/TXOTabNav/TXOTabNav.jsx +++ b/src/components/Office/TXOTabNav/TXOTabNav.jsx @@ -17,6 +17,7 @@ const TXOTabNav = ({ excessWeightRiskCount, pendingPaymentRequestCount, unapprovedSITExtensionCount, + missingOrdersInfoCount, shipmentsWithDeliveryAddressUpdateRequestedCount, order, moveCode, @@ -39,6 +40,9 @@ const TXOTabNav = ({ if (shipmentsWithDeliveryAddressUpdateRequestedCount) { moveDetailsTagCount += shipmentsWithDeliveryAddressUpdateRequestedCount; } + if (missingOrdersInfoCount > 0) { + moveDetailsTagCount += missingOrdersInfoCount; + } let moveTaskOrderTagCount = 0; if (unapprovedServiceItemCount > 0) { diff --git a/src/components/Office/TXOTabNav/TXOTabNav.test.jsx b/src/components/Office/TXOTabNav/TXOTabNav.test.jsx index 7298a9d4d08..2779c53b11e 100644 --- a/src/components/Office/TXOTabNav/TXOTabNav.test.jsx +++ b/src/components/Office/TXOTabNav/TXOTabNav.test.jsx @@ -41,11 +41,12 @@ describe('Move details tag rendering', () => { const moveDetailsOneShipment = { ...basicNavProps, unapprovedShipmentCount: 1, + missingOrdersInfoCount: 4, }; render(, { wrapper: MemoryRouter }); const moveDetailsTab = screen.getByTestId('MoveDetails-Tab'); - expect(within(moveDetailsTab).getByTestId('tag')).toHaveTextContent('1'); + expect(within(moveDetailsTab).getByTestId('tag')).toHaveTextContent('5'); }); it('should render the move details tab container with a tag that shows the count of items that need attention when there are approved shipments with a destination address update requiring TXO review', () => { diff --git a/src/components/PrimeUI/Shipment/Shipment.jsx b/src/components/PrimeUI/Shipment/Shipment.jsx index ba59e51a98e..f7849d7585a 100644 --- a/src/components/PrimeUI/Shipment/Shipment.jsx +++ b/src/components/PrimeUI/Shipment/Shipment.jsx @@ -13,7 +13,7 @@ import { ShipmentShape } from 'types/shipment'; import { primeSimulatorRoutes } from 'constants/routes'; import { ppmShipmentStatuses, shipmentDestinationTypes } from 'constants/shipments'; import styles from 'pages/PrimeUI/MoveTaskOrder/MoveDetails.module.scss'; -import { SHIPMENT_OPTIONS } from 'shared/constants'; +import { ADDRESS_TYPES, SHIPMENT_OPTIONS } from 'shared/constants'; const Shipment = ({ shipment, moveId, onDelete, mtoServiceItems }) => { const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); @@ -188,32 +188,68 @@ const Shipment = ({ shipment, moveId, onDelete, mtoServiceItems }) => {
Pickup Address:
{formatPrimeAPIShipmentAddress(shipment.pickupAddress)}
-
{shipment.pickupAddress?.id && moveId && Edit}
+
+ {shipment.pickupAddress?.id && moveId && ( + + Edit + + )} +
Second Pickup Address:
{formatPrimeAPIShipmentAddress(shipment.secondaryPickupAddress)}
-
{shipment.secondaryPickupAddress?.id && moveId && Edit}
+
+ {shipment.secondaryPickupAddress?.id && moveId && ( + + Edit + + )} +
Third Pickup Address:
{formatPrimeAPIShipmentAddress(shipment.tertiaryPickupAddress)}
-
{shipment.tertiaryPickupAddress?.id && moveId && Edit}
+
+ {shipment.tertiaryPickupAddress?.id && moveId && ( + + Edit + + )} +
Destination Address:
{formatPrimeAPIShipmentAddress(shipment.destinationAddress)}
-
{shipment.destinationAddress?.id && moveId && Edit}
+
+ {shipment.destinationAddress?.id && moveId && ( + + Edit + + )} +
Second Destination Address:
{formatPrimeAPIShipmentAddress(shipment.secondaryDeliveryAddress)}
-
{shipment.secondaryDeliveryAddress?.id && moveId && Edit}
+
+ {shipment.secondaryDeliveryAddress?.id && moveId && ( + + Edit + + )} +
Third Destination Address:
{formatPrimeAPIShipmentAddress(shipment.tertiaryDeliveryAddress)}
-
{shipment.tertiaryDeliveryAddress?.id && moveId && Edit}
+
+ {shipment.tertiaryDeliveryAddress?.id && moveId && ( + + Edit + + )} +
Destination type:
diff --git a/src/components/Table/SearchResultsTable.jsx b/src/components/Table/SearchResultsTable.jsx index 78a0daf5180..f36fc385faa 100644 --- a/src/components/Table/SearchResultsTable.jsx +++ b/src/components/Table/SearchResultsTable.jsx @@ -13,12 +13,7 @@ import DateSelectFilter from 'components/Table/Filters/DateSelectFilter'; import LoadingPlaceholder from 'shared/LoadingPlaceholder'; import SomethingWentWrong from 'shared/SomethingWentWrong'; import TextBoxFilter from 'components/Table/Filters/TextBoxFilter'; -import { - BRANCH_OPTIONS_WITH_MARINE_CORPS, - MOVE_STATUS_LABELS, - SEARCH_QUEUE_STATUS_FILTER_OPTIONS, - SortShape, -} from 'constants/queues'; +import { BRANCH_OPTIONS, MOVE_STATUS_LABELS, SEARCH_QUEUE_STATUS_FILTER_OPTIONS, SortShape } from 'constants/queues'; import { DATE_FORMAT_STRING } from 'shared/constants'; import { formatDateFromIso, serviceMemberAgencyLabel } from 'utils/formatters'; import MultiSelectCheckBoxFilter from 'components/Table/Filters/MultiSelectCheckBoxFilter'; @@ -107,7 +102,7 @@ const moveSearchColumns = (moveLockFlag, handleEditProfileClick) => [ isFilterable: true, Filter: (props) => ( // eslint-disable-next-line react/jsx-props-no-spreading - + ), }, ), diff --git a/src/components/Table/TableCSVExportButton.jsx b/src/components/Table/TableCSVExportButton.jsx index 813f5b7a50d..5d3180ede70 100644 --- a/src/components/Table/TableCSVExportButton.jsx +++ b/src/components/Table/TableCSVExportButton.jsx @@ -1,10 +1,12 @@ -import React, { useState, useRef } from 'react'; +import React, { useState, useRef, useContext } from 'react'; import { CSVLink } from 'react-csv'; -import { Link } from '@trussworks/react-uswds'; +import { Button } from '@trussworks/react-uswds'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import moment from 'moment'; import PropTypes from 'prop-types'; +import SelectedGblocContext from 'components/Office/GblocSwitcher/SelectedGblocContext'; + const TableCSVExportButton = ({ labelText, filePrefix, @@ -16,12 +18,16 @@ const TableCSVExportButton = ({ paramSort, paramFilters, className, + isHeadquartersUser, }) => { const [isLoading, setIsLoading] = useState(false); const [csvRows, setCsvRows] = useState([]); const csvLinkRef = useRef(null); const { id: sortColumn, desc: sortOrder } = paramSort.length ? paramSort[0] : {}; + const gblocContext = useContext(SelectedGblocContext); + const { selectedGbloc } = isHeadquartersUser && gblocContext ? gblocContext : { selectedGbloc: undefined }; + const formatDataForExport = (data, columns = tableColumns) => { const formattedData = []; data.forEach((row) => { @@ -50,6 +56,7 @@ const TableCSVExportButton = ({ order: sortOrder ? 'desc' : 'asc', filters: paramFilters, currentPageSize: totalCount, + viewAsGBLOC: selectedGbloc, }); const formattedData = formatDataForExport(response[queueFetcherKey]); @@ -61,15 +68,23 @@ const TableCSVExportButton = ({ return (

- + @@ -96,6 +111,8 @@ TableCSVExportButton.propTypes = { paramSort: PropTypes.array, // paramSort is the filter columns and values currently applied to the queue paramFilters: PropTypes.array, + // isHeadquartersUser identifies if the active role is a headquarters user to allow switching GBLOCs + isHeadquartersUser: PropTypes.bool, }; TableCSVExportButton.defaultProps = { @@ -104,6 +121,7 @@ TableCSVExportButton.defaultProps = { hiddenColumns: [], paramSort: [], paramFilters: [], + isHeadquartersUser: false, }; export default TableCSVExportButton; diff --git a/src/components/Table/TableCSVExportButton.test.jsx b/src/components/Table/TableCSVExportButton.test.jsx index a3847752000..da8b2102f13 100644 --- a/src/components/Table/TableCSVExportButton.test.jsx +++ b/src/components/Table/TableCSVExportButton.test.jsx @@ -55,6 +55,11 @@ const paymentRequestsResponse = { ], }; +const paymentRequestsNoResultsResponse = { + page: 1, + perPage: 10, +}; + const paymentRequestColumns = [ { Header: ' ', @@ -129,6 +134,9 @@ const paymentRequestColumns = [ jest.mock('services/ghcApi', () => ({ getPaymentRequestsQueue: jest.fn().mockImplementation(() => Promise.resolve(paymentRequestsResponse)), + getPaymentRequestsNoResultsQueue: jest + .fn() + .mockImplementation(() => Promise.resolve(paymentRequestsNoResultsResponse)), })); describe('TableCSVExportButton', () => { @@ -155,4 +163,22 @@ describe('TableCSVExportButton', () => { expect(getPaymentRequestsQueue).toBeCalled(); }); + + const noResultsProps = { + tableColumns: paymentRequestColumns, + queueFetcher: () => Promise.resolve(paymentRequestsNoResultsResponse), + queueFetcherKey: 'queuePaymentRequests', + totalCount: 0, + }; + + it('is diabled when there is nothing to export', () => { + act(() => { + const wrapper = mount(); + const exportButton = wrapper.find('span[data-test-id="csv-export-btn-text"]'); + exportButton.simulate('click'); + wrapper.update(); + }); + + expect(getPaymentRequestsQueue).toBeCalled(); + }); }); diff --git a/src/components/Table/TableQueue.jsx b/src/components/Table/TableQueue.jsx index 5182fed428d..d09605914c4 100644 --- a/src/components/Table/TableQueue.jsx +++ b/src/components/Table/TableQueue.jsx @@ -48,6 +48,7 @@ const TableQueue = ({ csvExportQueueFetcher, csvExportQueueFetcherKey, sessionStorageKey, + isHeadquartersUser, }) => { const [isPageReload, setIsPageReload] = useState(true); useEffect(() => { @@ -87,7 +88,7 @@ const TableQueue = ({ const { id, desc } = paramSort.length ? paramSort[0] : {}; const gblocContext = useContext(SelectedGblocContext); - const { selectedGbloc } = gblocContext || { selectedGbloc: undefined }; + const { selectedGbloc } = isHeadquartersUser && gblocContext ? gblocContext : { selectedGbloc: undefined }; const multiSelectValueDelimiter = ','; @@ -312,6 +313,7 @@ const TableQueue = ({ totalCount={totalCount} paramSort={paramSort} paramFilters={paramFilters} + isHeadquartersUser={isHeadquartersUser} /> )}

@@ -381,6 +383,8 @@ TableQueue.propTypes = { csvExportQueueFetcherKey: PropTypes.string, // session storage key to store search filters sessionStorageKey: PropTypes.string, + // isHeadquartersUser identifies if the active role is a headquarters user to allow switching GBLOCs + isHeadquartersUser: PropTypes.bool, }; TableQueue.defaultProps = { @@ -399,5 +403,6 @@ TableQueue.defaultProps = { csvExportQueueFetcher: null, csvExportQueueFetcherKey: null, sessionStorageKey: 'default', + isHeadquartersUser: false, }; export default TableQueue; diff --git a/src/constants/queues.js b/src/constants/queues.js index 87d23e1ed04..17a6e9d4066 100644 --- a/src/constants/queues.js +++ b/src/constants/queues.js @@ -52,15 +52,6 @@ export const BRANCH_OPTIONS = [ { value: 'AIR_FORCE', label: 'Air Force' }, { value: 'COAST_GUARD', label: 'Coast Guard' }, { value: 'SPACE_FORCE', label: 'Space Force' }, -]; - -export const BRANCH_OPTIONS_WITH_MARINE_CORPS = [ - { value: '', label: 'All' }, - { value: 'ARMY', label: 'Army' }, - { value: 'NAVY', label: 'Navy' }, - { value: 'AIR_FORCE', label: 'Air Force' }, - { value: 'COAST_GUARD', label: 'Coast Guard' }, - { value: 'SPACE_FORCE', label: 'Space Force' }, { value: 'MARINES', label: 'Marine Corps' }, ]; diff --git a/src/pages/MyMove/Profile/ContactInfo.jsx b/src/pages/MyMove/Profile/ContactInfo.jsx index 2b25f520797..298789034a0 100644 --- a/src/pages/MyMove/Profile/ContactInfo.jsx +++ b/src/pages/MyMove/Profile/ContactInfo.jsx @@ -37,13 +37,13 @@ export const ContactInfo = ({ serviceMember, updateServiceMember, userEmail }) = const payload = { id: serviceMember.id, telephone: values?.telephone, - secondary_telephone: values?.secondary_telephone, + secondary_telephone: values?.secondary_telephone || '', personal_email: values?.personal_email, phone_is_preferred: values?.phone_is_preferred, email_is_preferred: values?.email_is_preferred, }; - if (!payload.secondary_telephone) { - delete payload.secondary_telephone; + if (!payload.secondary_telephone || payload.secondary_telephone === '') { + payload.secondary_telephone = ''; } return patchServiceMember(payload) diff --git a/src/pages/MyMove/Profile/EditContactInfo.jsx b/src/pages/MyMove/Profile/EditContactInfo.jsx index aa27b30eae0..898031e46b8 100644 --- a/src/pages/MyMove/Profile/EditContactInfo.jsx +++ b/src/pages/MyMove/Profile/EditContactInfo.jsx @@ -73,9 +73,7 @@ export const EditContactInfo = ({ backup_mailing_address: values[backupAddressName.toString()], }; - if (values?.secondary_telephone) { - serviceMemberPayload.secondary_telephone = values?.secondary_telephone; - } + serviceMemberPayload.secondary_telephone = values?.secondary_telephone; const backupContactPayload = { id: currentBackupContacts[0].id, diff --git a/src/pages/Office/CustomerOnboarding/CreateCustomerForm.jsx b/src/pages/Office/CustomerOnboarding/CreateCustomerForm.jsx index b8a6ed6a6fa..69354c506e6 100644 --- a/src/pages/Office/CustomerOnboarding/CreateCustomerForm.jsx +++ b/src/pages/Office/CustomerOnboarding/CreateCustomerForm.jsx @@ -27,11 +27,14 @@ import { setFlashMessage as setFlashMessageAction } from 'store/flash/actions'; import { elevatedPrivilegeTypes } from 'constants/userPrivileges'; import { isBooleanFlagEnabled } from 'utils/featureFlags'; import departmentIndicators from 'constants/departmentIndicators'; +import { generateUniqueDodid, generateUniqueEmplid } from 'utils/customer'; +import Hint from 'components/Hint'; export const CreateCustomerForm = ({ userPrivileges, setFlashMessage }) => { const [serverError, setServerError] = useState(null); const [showEmplid, setShowEmplid] = useState(false); const [isSafetyMove, setIsSafetyMove] = useState(false); + const [showSafetyMoveHint, setShowSafetyMoveHint] = useState(false); const navigate = useNavigate(); const branchOptions = dropdownInputOptions(SERVICE_MEMBER_AGENCY_LABELS); @@ -42,6 +45,9 @@ export const CreateCustomerForm = ({ userPrivileges, setFlashMessage }) => { const [isSafetyMoveFF, setSafetyMoveFF] = useState(false); + const uniqueDodid = generateUniqueDodid(); + const uniqueEmplid = generateUniqueEmplid(); + useEffect(() => { isBooleanFlagEnabled('safety_move')?.then((enabled) => { setSafetyMoveFF(enabled); @@ -55,6 +61,7 @@ export const CreateCustomerForm = ({ userPrivileges, setFlashMessage }) => { const initialValues = { affiliation: '', edipi: '', + emplid: '', first_name: '', middle_name: '', last_name: '', @@ -87,7 +94,7 @@ export const CreateCustomerForm = ({ userPrivileges, setFlashMessage }) => { }, create_okta_account: '', cac_user: '', - is_safety_move: false, + is_safety_move: 'false', }; const handleBack = () => { @@ -144,10 +151,26 @@ export const CreateCustomerForm = ({ userPrivileges, setFlashMessage }) => { const validationSchema = Yup.object().shape({ affiliation: Yup.mixed().oneOf(Object.keys(SERVICE_MEMBER_AGENCY_LABELS)).required('Required'), - edipi: Yup.string().matches(/[0-9]{10}/, 'Enter a 10-digit DOD ID number'), - emplid: Yup.string() - .notRequired() - .matches(/[0-9]{7}/, 'Enter a 7-digit EMPLID number'), + // All branches require an EDIPI unless it is a safety move + // where a fake DoD ID may be used + edipi: + !isSafetyMove && + Yup.string() + .matches(/^(SM[0-9]{8}|[0-9]{10})$/, 'Enter a 10-digit DoD ID number') + .required('Required'), + // Only the coast guard requires both EDIPI and EMPLID + // unless it is a safety move + emplid: + !isSafetyMove && + showEmplid && + Yup.string().when('affiliation', { + is: (affiliationValue) => affiliationValue === departmentIndicators.COAST_GUARD, + then: () => + Yup.string() + .matches(/^(SM[0-9]{5}|[0-9]{7})$/, 'Enter a 7-digit EMPLID number') + .required(`EMPLID is required for the Coast Guard`), + otherwise: Yup.string().notRequired(), + }), first_name: Yup.string().required('Required'), middle_name: Yup.string(), last_name: Yup.string().required('Required'), @@ -193,11 +216,10 @@ export const CreateCustomerForm = ({ userPrivileges, setFlashMessage }) => { const { value } = e.target; if (value === 'true') { setIsSafetyMove(true); - // clear out DoDID, emplid, and OKTA fields + setShowSafetyMoveHint(true); setValues({ ...values, - edipi: '', - emplid: '', + affiliation: '', create_okta_account: '', cac_user: 'true', is_safety_move: 'true', @@ -206,15 +228,47 @@ export const CreateCustomerForm = ({ userPrivileges, setFlashMessage }) => { setIsSafetyMove(false); setValues({ ...values, + affiliation: '', + edipi: '', + emplid: '', is_safety_move: 'false', }); } }; const handleBranchChange = (e) => { - if (e.target.value === departmentIndicators.COAST_GUARD) { + setShowSafetyMoveHint(false); + if (e.target.value === departmentIndicators.COAST_GUARD && isSafetyMove) { setShowEmplid(true); + setValues({ + ...values, + affiliation: e.target.value, + edipi: uniqueDodid, + emplid: uniqueEmplid, + }); + } else if (e.target.value === departmentIndicators.COAST_GUARD && !isSafetyMove) { + setShowEmplid(true); + setValues({ + ...values, + affiliation: e.target.value, + edipi: '', + emplid: '', + }); + } else if (e.target.value !== departmentIndicators.COAST_GUARD && isSafetyMove) { + setShowEmplid(false); + setValues({ + ...values, + affiliation: e.target.value, + edipi: uniqueDodid, + emplid: '', + }); } else { setShowEmplid(false); + setValues({ + ...values, + affiliation: e.target.value, + edipi: '', + emplid: '', + }); } }; return ( @@ -234,6 +288,7 @@ export const CreateCustomerForm = ({ userPrivileges, setFlashMessage }) => { value="true" data-testid="is-safety-move-yes" onChange={handleIsSafetyMove} + checked={values.is_safety_move === 'true'} /> { value="false" data-testid="is-safety-move-no" onChange={handleIsSafetyMove} + checked={values.is_safety_move === 'false'} />
@@ -262,9 +318,9 @@ export const CreateCustomerForm = ({ userPrivileges, setFlashMessage }) => { label="DoD ID number" name="edipi" id="edipi" - labelHint="Optional" maxLength="10" isDisabled={isSafetyMove} + data-testid="edipiInput" /> {showEmplid && ( { name="emplid" id="emplid" maxLength="7" - labelHint="Optional" inputMode="numeric" pattern="[0-9]{7}" isDisabled={isSafetyMove} + data-testid="emplidInput" /> )} + {isSafetyMove && showSafetyMoveHint && ( + + Once a branch is selected, this will generate a random safety move identifier + + )}

Customer Name

diff --git a/src/pages/Office/CustomerOnboarding/CreateCustomerForm.test.jsx b/src/pages/Office/CustomerOnboarding/CreateCustomerForm.test.jsx index 7167c40da84..841f7eb7c5e 100644 --- a/src/pages/Office/CustomerOnboarding/CreateCustomerForm.test.jsx +++ b/src/pages/Office/CustomerOnboarding/CreateCustomerForm.test.jsx @@ -69,7 +69,7 @@ const fakePayload = { }, create_okta_account: 'true', cac_user: 'false', - is_safety_move: 'false', + is_safety_move: false, }; const fakeResponse = { @@ -215,6 +215,67 @@ describe('CreateCustomerForm', () => { expect(screen.getByText('EMPLID')).toBeInTheDocument(); }); + it('payload can have an empty secondary phone number', async () => { + createCustomerWithOktaOption.mockImplementation(() => Promise.resolve(fakeResponse)); + + const { getByLabelText, getByTestId, getByRole } = render( + + + , + ); + + const user = userEvent.setup(); + + const saveBtn = await screen.findByRole('button', { name: 'Save' }); + expect(saveBtn).toBeInTheDocument(); + + await user.selectOptions(getByLabelText('Branch of service'), [fakePayload.affiliation]); + + await user.type(getByLabelText('First name'), fakePayload.first_name); + await user.type(getByLabelText('Last name'), fakePayload.last_name); + + await user.type(getByLabelText('Best contact phone'), fakePayload.telephone); + await user.type(getByLabelText('Personal email'), fakePayload.personal_email); + await userEvent.type(getByTestId('edipiInput'), fakePayload.edipi); + + await user.type(getByTestId('res-add-street1'), fakePayload.residential_address.streetAddress1); + await user.type(getByTestId('res-add-city'), fakePayload.residential_address.city); + await user.selectOptions(getByTestId('res-add-state'), [fakePayload.residential_address.state]); + await user.type(getByTestId('res-add-zip'), fakePayload.residential_address.postalCode); + + await user.type(getByTestId('backup-add-street1'), fakePayload.backup_mailing_address.streetAddress1); + await user.type(getByTestId('backup-add-city'), fakePayload.backup_mailing_address.city); + await user.selectOptions(getByTestId('backup-add-state'), [fakePayload.backup_mailing_address.state]); + await user.type(getByTestId('backup-add-zip'), fakePayload.backup_mailing_address.postalCode); + + await user.type(getByLabelText('Name'), fakePayload.backup_contact.name); + await user.type(getByRole('textbox', { name: 'Email' }), fakePayload.backup_contact.email); + await user.type(getByRole('textbox', { name: 'Phone' }), fakePayload.backup_contact.telephone); + + await userEvent.type(getByTestId('create-okta-account-yes'), fakePayload.create_okta_account); + + await userEvent.type(getByTestId('cac-user-no'), fakePayload.cac_user); + + await waitFor(() => { + expect(saveBtn).toBeEnabled(); + }); + + const waiter = waitFor(() => { + expect(createCustomerWithOktaOption).toHaveBeenCalled(); + expect(mockNavigate).toHaveBeenCalledWith(ordersPath, { + state: { + isSafetyMoveSelected: false, + }, + }); + }); + + await user.click(saveBtn); + await waiter; + expect(mockNavigate).toHaveBeenCalled(); + + expect(createCustomerWithOktaOption.mock.calls[0][0]).not.toHaveProperty('secondary_number'); + }, 10000); + it('navigates the user on cancel click', async () => { const { getByText } = render( @@ -249,6 +310,8 @@ describe('CreateCustomerForm', () => { await user.type(getByLabelText('Best contact phone'), fakePayload.telephone); await user.type(getByLabelText('Personal email'), fakePayload.personal_email); + await userEvent.type(getByTestId('edipiInput'), fakePayload.edipi); + await userEvent.type(getByTestId('res-add-street1'), fakePayload.residential_address.streetAddress1); await userEvent.type(getByTestId('res-add-city'), fakePayload.residential_address.city); await userEvent.selectOptions(getByTestId('res-add-state'), [fakePayload.residential_address.state]); @@ -282,6 +345,57 @@ describe('CreateCustomerForm', () => { }); }, 10000); + it('validates emplid against a coast guard member', async () => { + createCustomerWithOktaOption.mockImplementation(() => Promise.resolve(fakeResponse)); + + const { getByLabelText, getByTestId, getByRole } = render( + + + , + ); + + const user = userEvent.setup(); + + const saveBtn = await screen.findByRole('button', { name: 'Save' }); + expect(saveBtn).toBeInTheDocument(); + + await user.selectOptions(getByLabelText('Branch of service'), 'COAST_GUARD'); + + await user.type(getByLabelText('First name'), fakePayload.first_name); + await user.type(getByLabelText('Last name'), fakePayload.last_name); + + await user.type(getByLabelText('Best contact phone'), fakePayload.telephone); + await user.type(getByLabelText('Personal email'), fakePayload.personal_email); + + await userEvent.type(getByTestId('edipiInput'), fakePayload.edipi); + + await userEvent.type(getByTestId('res-add-street1'), fakePayload.residential_address.streetAddress1); + await userEvent.type(getByTestId('res-add-city'), fakePayload.residential_address.city); + await userEvent.selectOptions(getByTestId('res-add-state'), [fakePayload.residential_address.state]); + await userEvent.type(getByTestId('res-add-zip'), fakePayload.residential_address.postalCode); + + await userEvent.type(getByTestId('backup-add-street1'), fakePayload.backup_mailing_address.streetAddress1); + await userEvent.type(getByTestId('backup-add-city'), fakePayload.backup_mailing_address.city); + await userEvent.selectOptions(getByTestId('backup-add-state'), [fakePayload.backup_mailing_address.state]); + await userEvent.type(getByTestId('backup-add-zip'), fakePayload.backup_mailing_address.postalCode); + + await userEvent.type(getByLabelText('Name'), fakePayload.backup_contact.name); + await userEvent.type(getByRole('textbox', { name: 'Email' }), fakePayload.backup_contact.email); + await userEvent.type(getByRole('textbox', { name: 'Phone' }), fakePayload.backup_contact.telephone); + + await userEvent.type(getByTestId('create-okta-account-yes'), fakePayload.create_okta_account); + + await userEvent.type(getByTestId('cac-user-no'), fakePayload.cac_user); + + await waitFor(() => { + expect(saveBtn).toBeDisabled(); // EMPLID not set yet + }); + await userEvent.type(getByTestId('emplidInput'), '1234567'); + await waitFor(() => { + expect(saveBtn).toBeEnabled(); // EMPLID is set now, all validations true + }); + }, 10000); + it('allows safety privileged users to pass safety move status to orders screen', async () => { createCustomerWithOktaOption.mockImplementation(() => Promise.resolve(fakeResponse)); isBooleanFlagEnabled.mockImplementation(() => Promise.resolve(true)); @@ -335,6 +449,71 @@ describe('CreateCustomerForm', () => { }); }, 10000); + it('disables and populates DODID and EMPLID inputs when safety move is selected', async () => { + createCustomerWithOktaOption.mockImplementation(() => Promise.resolve(fakeResponse)); + isBooleanFlagEnabled.mockImplementation(() => Promise.resolve(true)); + + const { getByLabelText, getByTestId, getByRole } = render( + + + , + ); + + const user = userEvent.setup(); + + const safetyMove = await screen.findByTestId('is-safety-move-no'); + expect(safetyMove).toBeChecked(); + + // check the safety move box + await userEvent.type(getByTestId('is-safety-move-yes'), safetyPayload.is_safety_move); + + expect(await screen.findByTestId('safetyMoveHint')).toBeInTheDocument(); + + await user.selectOptions(getByLabelText('Branch of service'), ['COAST_GUARD']); + + // the input boxes should now be disabled + expect(await screen.findByTestId('edipiInput')).toBeDisabled(); + expect(await screen.findByTestId('emplidInput')).toBeDisabled(); + + // should be able to submit the form + await user.type(getByLabelText('First name'), safetyPayload.first_name); + await user.type(getByLabelText('Last name'), safetyPayload.last_name); + + await user.type(getByLabelText('Best contact phone'), safetyPayload.telephone); + await user.type(getByLabelText('Personal email'), safetyPayload.personal_email); + + await userEvent.type(getByTestId('res-add-street1'), safetyPayload.residential_address.streetAddress1); + await userEvent.type(getByTestId('res-add-city'), safetyPayload.residential_address.city); + await userEvent.selectOptions(getByTestId('res-add-state'), [safetyPayload.residential_address.state]); + await userEvent.type(getByTestId('res-add-zip'), safetyPayload.residential_address.postalCode); + + await userEvent.type(getByTestId('backup-add-street1'), safetyPayload.backup_mailing_address.streetAddress1); + await userEvent.type(getByTestId('backup-add-city'), safetyPayload.backup_mailing_address.city); + await userEvent.selectOptions(getByTestId('backup-add-state'), [safetyPayload.backup_mailing_address.state]); + await userEvent.type(getByTestId('backup-add-zip'), safetyPayload.backup_mailing_address.postalCode); + + await userEvent.type(getByLabelText('Name'), safetyPayload.backup_contact.name); + await userEvent.type(getByRole('textbox', { name: 'Email' }), safetyPayload.backup_contact.email); + await userEvent.type(getByRole('textbox', { name: 'Phone' }), safetyPayload.backup_contact.telephone); + + const saveBtn = await screen.findByRole('button', { name: 'Save' }); + expect(saveBtn).toBeInTheDocument(); + + await waitFor(() => { + expect(saveBtn).toBeEnabled(); + }); + await userEvent.click(saveBtn); + + await waitFor(() => { + expect(createCustomerWithOktaOption).toHaveBeenCalled(); + expect(mockNavigate).toHaveBeenCalledWith(ordersPath, { + state: { + isSafetyMoveSelected: true, + }, + }); + }); + }, 10000); + it('submits the form and tests for unsupported state validation', async () => { createCustomerWithOktaOption.mockImplementation(() => Promise.resolve(fakeResponse)); @@ -350,6 +529,7 @@ describe('CreateCustomerForm', () => { expect(saveBtn).toBeInTheDocument(); await user.selectOptions(getByLabelText('Branch of service'), [fakePayload.affiliation]); + await userEvent.type(getByTestId('edipiInput'), fakePayload.edipi); await user.type(getByLabelText('First name'), fakePayload.first_name); await user.type(getByLabelText('Last name'), fakePayload.last_name); diff --git a/src/pages/Office/HeadquartersQueues/HeadquartersQueues.jsx b/src/pages/Office/HeadquartersQueues/HeadquartersQueues.jsx index 9c6f613cc7e..8ba085fc464 100644 --- a/src/pages/Office/HeadquartersQueues/HeadquartersQueues.jsx +++ b/src/pages/Office/HeadquartersQueues/HeadquartersQueues.jsx @@ -247,6 +247,7 @@ const HeadquartersQueue = () => { csvExportQueueFetcher={getMovesQueue} csvExportQueueFetcherKey="queueMoves" sessionStorageKey={queueType} + isHeadquartersUser />
); @@ -273,6 +274,7 @@ const HeadquartersQueue = () => { csvExportQueueFetcher={getPaymentRequestsQueue} csvExportQueueFetcherKey="queuePaymentRequests" sessionStorageKey={queueType} + isHeadquartersUser />
); @@ -299,6 +301,7 @@ const HeadquartersQueue = () => { csvExportQueueFetcher={getServicesCounselingPPMQueue} csvExportQueueFetcherKey="queueMoves" sessionStorageKey={queueType} + isHeadquartersUser />
); @@ -326,6 +329,7 @@ const HeadquartersQueue = () => { csvExportQueueFetcher={getServicesCounselingQueue} csvExportQueueFetcherKey="queueMoves" sessionStorageKey={queueType} + isHeadquartersUser />
); diff --git a/src/pages/Office/MoveDetails/MoveDetails.jsx b/src/pages/Office/MoveDetails/MoveDetails.jsx index 8d6a4c359ac..e2fb69d7450 100644 --- a/src/pages/Office/MoveDetails/MoveDetails.jsx +++ b/src/pages/Office/MoveDetails/MoveDetails.jsx @@ -57,6 +57,8 @@ const MoveDetails = ({ setExcessWeightRiskCount, setUnapprovedSITExtensionCount, setShipmentsWithDeliveryAddressUpdateRequestedCount, + missingOrdersInfoCount, + setMissingOrdersInfoCount, isMoveLocked, }) => { const { moveCode } = useParams(); @@ -238,6 +240,28 @@ const MoveDetails = ({ setShipmentMissingRequiredInformation(shipmentIsMissingInformation); }, [mtoShipments]); + // using useMemo here due to this being used in a useEffect + // using useMemo prevents the useEffect from being rendered on ever render by memoizing the object + // so that it only recognizes the change when the orders object changes + const requiredOrdersInfo = useMemo( + () => ({ + ordersNumber: order?.order_number || '', + ordersType: order?.order_type || '', + ordersTypeDetail: order?.order_type_detail || '', + tacMDC: order?.tac || '', + departmentIndicator: order?.department_indicator || '', + }), + [order], + ); + + // Keep num of missing orders info synced up + useEffect(() => { + const ordersInfoCount = Object.values(requiredOrdersInfo).reduce((count, value) => { + return !value ? count + 1 : count; + }, 0); + setMissingOrdersInfoCount(ordersInfoCount); + }, [order, requiredOrdersInfo, setMissingOrdersInfoCount]); + if (isLoading) return ; if (isError) return ; @@ -284,7 +308,7 @@ const MoveDetails = ({ const customerInfo = { name: formattedCustomerName(customer.last_name, customer.first_name, customer.suffix, customer.middle_name), agency: customer.agency, - dodId: customer.dodID, + edipi: customer.edipi, emplid: customer.emplid, phone: customer.phone, altPhone: customer.secondaryTelephone, @@ -294,13 +318,6 @@ const MoveDetails = ({ backupContact: customer.backup_contact, }; - const requiredOrdersInfo = { - ordersNumber: order.order_number, - ordersType: order.order_type, - ordersTypeDetail: order.order_type_detail, - tacMDC: order.tac, - }; - const hasMissingOrdersRequiredInfo = Object.values(requiredOrdersInfo).some((value) => !value || value === ''); const hasAmendedOrders = ordersInfo.uploadedAmendedOrderID && !ordersInfo.amendedOrdersAcknowledgedAt; const hasDestinationAddressUpdate = @@ -311,12 +328,12 @@ const MoveDetails = ({
- + {missingOrdersInfoCount} ({ ...jest.requireActual('react-router-dom'), @@ -360,6 +361,7 @@ const requestedMoveDetailsAmendedOrdersQuery = { }, order: { id: '1', + department_indicator: 'ARMY', originDutyLocation: { address: { streetAddress1: '', @@ -783,6 +785,8 @@ describe('MoveDetails page', () => { setUnapprovedServiceItemCount={setUnapprovedServiceItemCount} setExcessWeightRiskCount={setExcessWeightRiskCount} setUnapprovedSITExtensionCount={setUnapprovedSITExtensionCount} + missingOrdersInfoCount={0} + setMissingOrdersInfoCount={setMissingOrdersInfoCount} /> , ); @@ -801,6 +805,8 @@ describe('MoveDetails page', () => { setUnapprovedServiceItemCount={setUnapprovedServiceItemCount} setExcessWeightRiskCount={setExcessWeightRiskCount} setUnapprovedSITExtensionCount={setUnapprovedSITExtensionCount} + missingOrdersInfoCount={0} + setMissingOrdersInfoCount={setMissingOrdersInfoCount} /> , ); @@ -819,6 +825,8 @@ describe('MoveDetails page', () => { setUnapprovedServiceItemCount={setUnapprovedServiceItemCount} setExcessWeightRiskCount={setExcessWeightRiskCount} setUnapprovedSITExtensionCount={setUnapprovedSITExtensionCount} + missingOrdersInfoCount={0} + setMissingOrdersInfoCount={setMissingOrdersInfoCount} /> , ); @@ -885,6 +893,8 @@ describe('MoveDetails page', () => { setUnapprovedServiceItemCount={setUnapprovedServiceItemCount} setExcessWeightRiskCount={setExcessWeightRiskCount} setUnapprovedSITExtensionCount={setUnapprovedSITExtensionCount} + missingOrdersInfoCount={0} + setMissingOrdersInfoCount={setMissingOrdersInfoCount} /> , ); @@ -905,6 +915,8 @@ describe('MoveDetails page', () => { setUnapprovedServiceItemCount={setUnapprovedServiceItemCount} setExcessWeightRiskCount={setExcessWeightRiskCount} setUnapprovedSITExtensionCount={setUnapprovedSITExtensionCount} + missingOrdersInfoCount={0} + setMissingOrdersInfoCount={setMissingOrdersInfoCount} /> , ); @@ -928,6 +940,8 @@ describe('MoveDetails page', () => { setUnapprovedServiceItemCount={setUnapprovedServiceItemCount} setExcessWeightRiskCount={setExcessWeightRiskCount} setUnapprovedSITExtensionCount={setUnapprovedSITExtensionCount} + missingOrdersInfoCount={0} + setMissingOrdersInfoCount={setMissingOrdersInfoCount} /> , ); @@ -967,6 +981,8 @@ describe('MoveDetails page', () => { setUnapprovedServiceItemCount={setUnapprovedServiceItemCount} setExcessWeightRiskCount={setExcessWeightRiskCount} setUnapprovedSITExtensionCount={setUnapprovedServiceItemCount} + missingOrdersInfoCount={0} + setMissingOrdersInfoCount={setMissingOrdersInfoCount} /> , ); @@ -986,12 +1002,15 @@ describe('MoveDetails page', () => { setUnapprovedServiceItemCount={setUnapprovedServiceItemCount} setExcessWeightRiskCount={setExcessWeightRiskCount} setUnapprovedSITExtensionCount={setUnapprovedSITExtensionCount} + missingOrdersInfoCount={2} + setMissingOrdersInfoCount={setMissingOrdersInfoCount} /> , ); it('renders an error indicator in the sidebar', () => { expect(wrapper.find('a[href="#orders"] span[data-testid="tag"]').exists()).toBe(true); + expect(wrapper.find('a[href="#orders"] span[data-testid="tag"]').text()).toBe('2'); }); }); @@ -1006,6 +1025,8 @@ describe('MoveDetails page', () => { setUnapprovedServiceItemCount={setUnapprovedServiceItemCount} setExcessWeightRiskCount={setExcessWeightRiskCount} setUnapprovedSITExtensionCount={setUnapprovedSITExtensionCount} + missingOrdersInfoCount={0} + setMissingOrdersInfoCount={setMissingOrdersInfoCount} /> , ); @@ -1025,6 +1046,8 @@ describe('MoveDetails page', () => { setUnapprovedServiceItemCount={setUnapprovedServiceItemCount} setExcessWeightRiskCount={setExcessWeightRiskCount} setUnapprovedSITExtensionCount={setUnapprovedSITExtensionCount} + missingOrdersInfoCount={0} + setMissingOrdersInfoCount={setMissingOrdersInfoCount} /> , ); @@ -1039,6 +1062,7 @@ describe('MoveDetails page', () => { setUnapprovedServiceItemCount, setExcessWeightRiskCount, setUnapprovedSITExtensionCount, + setMissingOrdersInfoCount, }; it('renders the financial review flag button when user has permission', async () => { @@ -1150,6 +1174,8 @@ describe('MoveDetails page', () => { setUnapprovedServiceItemCount={setUnapprovedServiceItemCount} setExcessWeightRiskCount={setExcessWeightRiskCount} setUnapprovedSITExtensionCount={setUnapprovedSITExtensionCount} + missingOrdersInfoCount={0} + setMissingOrdersInfoCount={setMissingOrdersInfoCount} /> , ); diff --git a/src/pages/Office/MoveQueue/MoveQueue.jsx b/src/pages/Office/MoveQueue/MoveQueue.jsx index d485648b3fe..acfceb62439 100644 --- a/src/pages/Office/MoveQueue/MoveQueue.jsx +++ b/src/pages/Office/MoveQueue/MoveQueue.jsx @@ -10,7 +10,7 @@ import { getMovesQueue } from 'services/ghcApi'; import { formatDateFromIso, serviceMemberAgencyLabel } from 'utils/formatters'; import MultiSelectCheckBoxFilter from 'components/Table/Filters/MultiSelectCheckBoxFilter'; import SelectFilter from 'components/Table/Filters/SelectFilter'; -import { BRANCH_OPTIONS, MOVE_STATUS_OPTIONS, GBLOC, MOVE_STATUS_LABELS } from 'constants/queues'; +import { MOVE_STATUS_OPTIONS, GBLOC, MOVE_STATUS_LABELS, BRANCH_OPTIONS } from 'constants/queues'; import TableQueue from 'components/Table/TableQueue'; import LoadingPlaceholder from 'shared/LoadingPlaceholder'; import SomethingWentWrong from 'shared/SomethingWentWrong'; diff --git a/src/pages/Office/MoveQueue/MoveQueue.test.jsx b/src/pages/Office/MoveQueue/MoveQueue.test.jsx index f62a6b75c05..b8017b59fdb 100644 --- a/src/pages/Office/MoveQueue/MoveQueue.test.jsx +++ b/src/pages/Office/MoveQueue/MoveQueue.test.jsx @@ -7,7 +7,7 @@ import { render, screen, waitFor } from '@testing-library/react'; import MoveQueue from './MoveQueue'; import { MockProviders } from 'testUtils'; -import { MOVE_STATUS_OPTIONS } from 'constants/queues'; +import { MOVE_STATUS_OPTIONS, BRANCH_OPTIONS } from 'constants/queues'; import { generalRoutes, tooRoutes } from 'constants/routes'; import { isBooleanFlagEnabled } from 'utils/featureFlags'; @@ -23,6 +23,71 @@ jest.mock('utils/featureFlags', () => ({ isBooleanFlagEnabled: jest.fn().mockImplementation(() => Promise.resolve()), })); +const moveData = [ + { + id: 'move1', + customer: { + agency: 'AIR_FORCE', + first_name: 'test first', + last_name: 'test last', + dodID: '555555555', + }, + locator: 'AB5P', + departmentIndicator: 'ARMY', + shipmentsCount: 2, + status: 'SUBMITTED', + originDutyLocation: { + name: 'Area 51', + }, + originGBLOC: 'EEEE', + requestedMoveDate: '2023-02-10', + appearedInTooAt: '2023-02-10T00:00:00.000Z', + lockExpiresAt: '2099-02-10T00:00:00.000Z', + lockedByOfficeUserID: '2744435d-7ba8-4cc5-bae5-f302c72c966e', + }, + { + id: 'move2', + customer: { + agency: 'COAST_GUARD', + first_name: 'test another first', + last_name: 'test another last', + dodID: '4444444444', + emplid: '4589652', + }, + locator: 'T12A', + departmentIndicator: 'COAST_GUARD', + shipmentsCount: 1, + status: 'APPROVED', + originDutyLocation: { + name: 'Los Alamos', + }, + originGBLOC: 'EEEE', + requestedMoveDate: '2023-02-12', + appearedInTooAt: '2023-02-12T00:00:00.000Z', + }, + { + id: 'move3', + customer: { + agency: 'Marine Corps', + first_name: 'will', + last_name: 'robinson', + dodID: '6666666666', + }, + locator: 'PREP', + departmentIndicator: 'MARINES', + shipmentsCount: 1, + status: 'SUBMITTED', + originDutyLocation: { + name: 'Area 52', + }, + originGBLOC: 'EEEE', + requestedMoveDate: '2023-03-12', + appearedInTooAt: '2023-03-12T00:00:00.000Z', + lockExpiresAt: '2099-03-12T00:00:00.000Z', + lockedByOfficeUserID: '2744435d-7ba8-4cc5-bae5-f302c72c966e', + }, +]; + jest.mock('hooks/queries', () => ({ useUserQueries: () => { return { @@ -38,50 +103,8 @@ jest.mock('hooks/queries', () => ({ isLoading: false, isError: false, queueResult: { - totalCount: 2, - data: [ - { - id: 'move1', - customer: { - agency: 'AIR_FORCE', - first_name: 'test first', - last_name: 'test last', - dodID: '555555555', - }, - locator: 'AB5P', - departmentIndicator: 'ARMY', - shipmentsCount: 2, - status: 'SUBMITTED', - originDutyLocation: { - name: 'Area 51', - }, - originGBLOC: 'EEEE', - requestedMoveDate: '2023-02-10', - appearedInTooAt: '2023-02-10T00:00:00.000Z', - lockExpiresAt: '2099-02-10T00:00:00.000Z', - lockedByOfficeUserID: '2744435d-7ba8-4cc5-bae5-f302c72c966e', - }, - { - id: 'move2', - customer: { - agency: 'COAST_GUARD', - first_name: 'test another first', - last_name: 'test another last', - dodID: '4444444444', - emplid: '4589652', - }, - locator: 'T12A', - departmentIndicator: 'COAST_GUARD', - shipmentsCount: 1, - status: 'APPROVED', - originDutyLocation: { - name: 'Los Alamos', - }, - originGBLOC: 'EEEE', - requestedMoveDate: '2023-02-12', - appearedInTooAt: '2023-02-12T00:00:00.000Z', - }, - ], + totalCount: 3, + data: moveData, }, }; }, @@ -103,7 +126,7 @@ describe('MoveQueue', () => { }); it('should render the h1', () => { - expect(GetMountedComponent(tooRoutes.MOVE_QUEUE).find('h1').text()).toBe('All moves (2)'); + expect(GetMountedComponent(tooRoutes.MOVE_QUEUE).find('h1').text()).toBe('All moves (3)'); }); it('should render the table', () => { @@ -111,32 +134,89 @@ describe('MoveQueue', () => { }); it('should format the column data', () => { + let currentIndex = 0; + let currentMove; const moves = GetMountedComponent(tooRoutes.MOVE_QUEUE).find('tbody tr'); - const firstMove = moves.at(0); - expect(firstMove.find({ 'data-testid': 'lastName-0' }).text()).toBe('test last, test first'); - expect(firstMove.find({ 'data-testid': 'dodID-0' }).text()).toBe('555555555'); - expect(firstMove.find({ 'data-testid': 'status-0' }).text()).toBe('New move'); - expect(firstMove.find({ 'data-testid': 'locator-0' }).text()).toBe('AB5P'); - expect(firstMove.find({ 'data-testid': 'branch-0' }).text()).toBe('Air Force'); - expect(firstMove.find({ 'data-testid': 'shipmentsCount-0' }).text()).toBe('2'); - expect(firstMove.find({ 'data-testid': 'originDutyLocation-0' }).text()).toBe('Area 51'); - expect(firstMove.find({ 'data-testid': 'originGBLOC-0' }).text()).toBe('EEEE'); - expect(firstMove.find({ 'data-testid': 'requestedMoveDate-0' }).text()).toBe('10 Feb 2023'); - expect(firstMove.find({ 'data-testid': 'appearedInTooAt-0' }).text()).toBe('10 Feb 2023'); + currentMove = moves.at(currentIndex); + expect(currentMove.find({ 'data-testid': `lastName-${currentIndex}` }).text()).toBe( + `${moveData[currentIndex].customer.last_name}, ${moveData[currentIndex].customer.first_name}`, + ); + expect(currentMove.find({ 'data-testid': `dodID-${currentIndex}` }).text()).toBe( + moveData[currentIndex].customer.dodID, + ); + expect(currentMove.find({ 'data-testid': `status-${currentIndex}` }).text()).toBe('New move'); + expect(currentMove.find({ 'data-testid': `locator-${currentIndex}` }).text()).toBe(moveData[currentIndex].locator); + expect(currentMove.find({ 'data-testid': `branch-${currentIndex}` }).text()).toBe( + BRANCH_OPTIONS.find((value) => value.value === moveData[currentIndex].customer.agency).label, + ); + expect(currentMove.find({ 'data-testid': `shipmentsCount-${currentIndex}` }).text()).toBe( + moveData[currentIndex].shipmentsCount.toString(), + ); + expect(currentMove.find({ 'data-testid': `originDutyLocation-${currentIndex}` }).text()).toBe( + moveData[currentIndex].originDutyLocation.name, + ); + expect(currentMove.find({ 'data-testid': `originGBLOC-${currentIndex}` }).text()).toBe( + moveData[currentIndex].originGBLOC, + ); + expect(currentMove.find({ 'data-testid': `requestedMoveDate-${currentIndex}` }).text()).toBe('10 Feb 2023'); + expect(currentMove.find({ 'data-testid': `appearedInTooAt-${currentIndex}` }).text()).toBe('10 Feb 2023'); + + currentIndex += 1; + currentMove = moves.at(currentIndex); + expect(currentMove.find({ 'data-testid': `lastName-${currentIndex}` }).text()).toBe( + 'test another last, test another first', + ); + expect(currentMove.find({ 'data-testid': `lastName-${currentIndex}` }).text()).toBe( + `${moveData[currentIndex].customer.last_name}, ${moveData[currentIndex].customer.first_name}`, + ); + expect(currentMove.find({ 'data-testid': `dodID-${currentIndex}` }).text()).toBe( + moveData[currentIndex].customer.dodID, + ); + expect(currentMove.find({ 'data-testid': `emplid-${currentIndex}` }).text()).toBe( + moveData[currentIndex].customer.emplid, + ); + expect(currentMove.find({ 'data-testid': `status-${currentIndex}` }).text()).toBe('Move approved'); + expect(currentMove.find({ 'data-testid': `locator-${currentIndex}` }).text()).toBe(moveData[currentIndex].locator); + expect(currentMove.find({ 'data-testid': `branch-${currentIndex}` }).text()).toBe( + BRANCH_OPTIONS.find((value) => value.value === moveData[currentIndex].customer.agency).label, + ); + expect(currentMove.find({ 'data-testid': `shipmentsCount-${currentIndex}` }).text()).toBe( + moveData[currentIndex].shipmentsCount.toString(), + ); + expect(currentMove.find({ 'data-testid': `originDutyLocation-${currentIndex}` }).text()).toBe( + moveData[currentIndex].originDutyLocation.name, + ); + expect(currentMove.find({ 'data-testid': `originGBLOC-${currentIndex}` }).text()).toBe( + moveData[currentIndex].originGBLOC, + ); + expect(currentMove.find({ 'data-testid': `requestedMoveDate-${currentIndex}` }).text()).toBe('12 Feb 2023'); + expect(currentMove.find({ 'data-testid': `appearedInTooAt-${currentIndex}` }).text()).toBe('12 Feb 2023'); - const secondMove = moves.at(1); - expect(secondMove.find({ 'data-testid': 'lastName-1' }).text()).toBe('test another last, test another first'); - expect(secondMove.find({ 'data-testid': 'dodID-1' }).text()).toBe('4444444444'); - expect(secondMove.find({ 'data-testid': 'emplid-1' }).text()).toBe('4589652'); - expect(secondMove.find({ 'data-testid': 'status-1' }).text()).toBe('Move approved'); - expect(secondMove.find({ 'data-testid': 'locator-1' }).text()).toBe('T12A'); - expect(secondMove.find({ 'data-testid': 'branch-1' }).text()).toBe('Coast Guard'); - expect(secondMove.find({ 'data-testid': 'shipmentsCount-1' }).text()).toBe('1'); - expect(secondMove.find({ 'data-testid': 'originDutyLocation-1' }).text()).toBe('Los Alamos'); - expect(secondMove.find({ 'data-testid': 'originGBLOC-1' }).text()).toBe('EEEE'); - expect(secondMove.find({ 'data-testid': 'requestedMoveDate-1' }).text()).toBe('12 Feb 2023'); - expect(secondMove.find({ 'data-testid': 'appearedInTooAt-1' }).text()).toBe('12 Feb 2023'); + currentIndex += 1; + currentMove = moves.at(currentIndex); + expect(currentMove.find({ 'data-testid': `lastName-${currentIndex}` }).text()).toBe( + `${moveData[currentIndex].customer.last_name}, ${moveData[currentIndex].customer.first_name}`, + ); + expect(currentMove.find({ 'data-testid': `dodID-${currentIndex}` }).text()).toBe( + moveData[currentIndex].customer.dodID, + ); + expect(currentMove.find({ 'data-testid': `status-${currentIndex}` }).text()).toBe('New move'); + expect(currentMove.find({ 'data-testid': `locator-${currentIndex}` }).text()).toBe(moveData[currentIndex].locator); + expect(currentMove.find({ 'data-testid': `branch-${currentIndex}` }).text()).toBe( + moveData[currentIndex].customer.agency.toString(), + ); + expect(currentMove.find({ 'data-testid': `shipmentsCount-${currentIndex}` }).text()).toBe( + moveData[currentIndex].shipmentsCount.toString(), + ); + expect(currentMove.find({ 'data-testid': `originDutyLocation-${currentIndex}` }).text()).toBe( + moveData[currentIndex].originDutyLocation.name, + ); + expect(currentMove.find({ 'data-testid': `originGBLOC-${currentIndex}` }).text()).toBe( + moveData[currentIndex].originGBLOC, + ); + expect(currentMove.find({ 'data-testid': `requestedMoveDate-${currentIndex}` }).text()).toBe('12 Mar 2023'); + expect(currentMove.find({ 'data-testid': `appearedInTooAt-${currentIndex}` }).text()).toBe('12 Mar 2023'); }); it('should render the pagination component', () => { @@ -249,7 +329,7 @@ describe('MoveQueue', () => { , ); await waitFor(() => { - const lockIcon = screen.queryByTestId('lock-icon'); + const lockIcon = screen.queryAllByTestId('lock-icon')[0]; expect(lockIcon).toBeInTheDocument(); }); }); diff --git a/src/pages/Office/PaymentRequestReview/PaymentRequestReview.jsx b/src/pages/Office/PaymentRequestReview/PaymentRequestReview.jsx index e4a3f2c576f..e20af2c740d 100644 --- a/src/pages/Office/PaymentRequestReview/PaymentRequestReview.jsx +++ b/src/pages/Office/PaymentRequestReview/PaymentRequestReview.jsx @@ -175,7 +175,11 @@ export const PaymentRequestReview = ({ order }) => { return (
- {uploads.length > 0 ? :

No documents provided

} + {uploads.length > 0 ? ( + + ) : ( +

No documents provided

+ )}
{ expect(reviewServiceItems.prop('serviceItemCards')).toEqual(expectedServiceItemCards); }); }); + describe('clicking the next button', () => { describe('with pending requests', () => { beforeEach(async () => { diff --git a/src/pages/Office/ServicesCounselingEditShipmentDetails/ServicesCounselingEditShipmentDetails.test.jsx b/src/pages/Office/ServicesCounselingEditShipmentDetails/ServicesCounselingEditShipmentDetails.test.jsx index de1ac1b0ead..26b06a51d85 100644 --- a/src/pages/Office/ServicesCounselingEditShipmentDetails/ServicesCounselingEditShipmentDetails.test.jsx +++ b/src/pages/Office/ServicesCounselingEditShipmentDetails/ServicesCounselingEditShipmentDetails.test.jsx @@ -328,7 +328,7 @@ describe('ServicesCounselingEditShipmentDetails component', () => { expect( screen.getByText('Something went wrong, and your changes were not saved. Please try again.'), ).toBeVisible(); - }); + }, 10000); }); it('routes to the move details page when the cancel button is clicked', async () => { diff --git a/src/pages/Office/ServicesCounselingMoveDetails/ServicesCounselingMoveDetails.jsx b/src/pages/Office/ServicesCounselingMoveDetails/ServicesCounselingMoveDetails.jsx index 6c065a37083..104aac73b43 100644 --- a/src/pages/Office/ServicesCounselingMoveDetails/ServicesCounselingMoveDetails.jsx +++ b/src/pages/Office/ServicesCounselingMoveDetails/ServicesCounselingMoveDetails.jsx @@ -1,7 +1,6 @@ import React, { useState, useEffect, useMemo } from 'react'; import { Link, useParams, useNavigate, generatePath } from 'react-router-dom'; import { useQueryClient, useMutation } from '@tanstack/react-query'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { func } from 'prop-types'; import classnames from 'classnames'; import 'styles/office.scss'; @@ -42,7 +41,13 @@ import { objectIsMissingFieldWithCondition } from 'utils/displayFlags'; import { ReviewButton } from 'components/form/IconButtons'; import { calculateWeightRequested } from 'hooks/custom'; -const ServicesCounselingMoveDetails = ({ infoSavedAlert, setUnapprovedShipmentCount, isMoveLocked }) => { +const ServicesCounselingMoveDetails = ({ + infoSavedAlert, + setUnapprovedShipmentCount, + isMoveLocked, + missingOrdersInfoCount, + setMissingOrdersInfoCount, +}) => { const { moveCode } = useParams(); const navigate = useNavigate(); const [alertMessage, setAlertMessage] = useState(null); @@ -345,6 +350,20 @@ const ServicesCounselingMoveDetails = ({ infoSavedAlert, setUnapprovedShipmentCo ntsSac: order.ntsSac, }; + // using useMemo here due to this being used in a useEffect + // using useMemo prevents the useEffect from being rendered on ever render by memoizing the object + // so that it only recognizes the change when the orders object changes + const requiredOrdersInfo = useMemo( + () => ({ + ordersNumber: order?.order_number || '', + ordersType: order?.order_type || '', + ordersTypeDetail: order?.order_type_detail || '', + tacMDC: order?.tac || '', + departmentIndicator: order?.department_indicator || '', + }), + [order], + ); + const handleButtonDropdownChange = (e) => { const selectedOption = e.target.value; @@ -412,6 +431,14 @@ const ServicesCounselingMoveDetails = ({ infoSavedAlert, setUnapprovedShipmentCo setUnapprovedShipmentCount, ]); + // Keep num of missing orders info synced up + useEffect(() => { + const ordersInfoCount = Object.values(requiredOrdersInfo).reduce((count, value) => { + return !value ? count + 1 : count; + }, 0); + setMissingOrdersInfoCount(ordersInfoCount); + }, [order, requiredOrdersInfo, setMissingOrdersInfoCount]); + if (isLoading) return ; if (isError) return ; @@ -454,13 +481,6 @@ const ServicesCounselingMoveDetails = ({ infoSavedAlert, setUnapprovedShipmentCo return false; }; - const requiredOrdersInfo = { - ordersNumber: order.order_number, - ordersType: order.order_type, - ordersTypeDetail: order.order_type_detail, - tacMDC: order.tac, - }; - const allShipmentsDeleted = mtoShipments.every((shipment) => !!shipment.deletedAt); const hasMissingOrdersRequiredInfo = Object.values(requiredOrdersInfo).some((value) => !value || value === ''); const hasAmendedOrders = ordersInfo.uploadedAmendedOrderID && !ordersInfo.amendedOrdersAcknowledgedAt; @@ -477,12 +497,12 @@ const ServicesCounselingMoveDetails = ({ infoSavedAlert, setUnapprovedShipmentCo {shipmentConcernCount} - + {missingOrdersInfoCount} { return render( - + , ); }; @@ -570,6 +638,23 @@ describe('MoveDetails page', () => { expect(mockSetUpapprovedShipmentCount).toHaveBeenCalledWith(3); }); + it('shares the number of missing orders information', () => { + const moveDetailsQuery = { + ...newMoveDetailsQuery, + order: orderMissingRequiredInfo, + }; + + useMoveDetailsQueries.mockReturnValue(moveDetailsQuery); + useOrdersDocumentQueries.mockReturnValue(moveDetailsQuery); + + const mockSetMissingOrdersInfoCount = jest.fn(); + renderComponent({ setMissingOrdersInfoCount: mockSetMissingOrdersInfoCount }); + + // Should have called `setMissingOrdersInfoCount` with 4 missing fields + expect(mockSetMissingOrdersInfoCount).toHaveBeenCalledTimes(1); + expect(mockSetMissingOrdersInfoCount).toHaveBeenCalledWith(4); + }); + /* eslint-disable camelcase */ it('renders shipments info', async () => { useMoveDetailsQueries.mockReturnValue(newMoveDetailsQuery); @@ -836,7 +921,11 @@ describe('MoveDetails page', () => { permissions={[permissionTypes.updateShipment, permissionTypes.updateCustomer]} {...mockRoutingOptions} > - + , ); @@ -933,7 +1022,11 @@ describe('MoveDetails page', () => { path={servicesCounselingRoutes.BASE_SHIPMENT_ADD_PATH} params={{ moveCode: mockRequestedMoveCode, shipmentType }} > - , + + , , ); @@ -1031,7 +1124,10 @@ describe('MoveDetails page', () => { it('renders the financial review flag button when user has permission', async () => { render( - + , ); diff --git a/src/pages/Office/ServicesCounselingMoveInfo/ServicesCounselingMoveInfo.jsx b/src/pages/Office/ServicesCounselingMoveInfo/ServicesCounselingMoveInfo.jsx index 8646b252640..eaedf6eca8a 100644 --- a/src/pages/Office/ServicesCounselingMoveInfo/ServicesCounselingMoveInfo.jsx +++ b/src/pages/Office/ServicesCounselingMoveInfo/ServicesCounselingMoveInfo.jsx @@ -42,6 +42,7 @@ const ServicesCounselingMoveInfo = () => { const [unapprovedServiceItemCount, setUnapprovedServiceItemCount] = React.useState(0); const [excessWeightRiskCount, setExcessWeightRiskCount] = React.useState(0); const [unapprovedSITExtensionCount, setUnApprovedSITExtensionCount] = React.useState(0); + const [missingOrdersInfoCount, setMissingOrdersInfoCount] = useState(0); const [infoSavedAlert, setInfoSavedAlert] = useState(null); const { hasRecentError, traceId } = useSelector((state) => state.interceptor); const [moveLockFlag, setMoveLockFlag] = useState(false); @@ -195,6 +196,7 @@ const ServicesCounselingMoveInfo = () => { unapprovedServiceItemCount={unapprovedServiceItemCount} excessWeightRiskCount={excessWeightRiskCount} unapprovedSITExtensionCount={unapprovedSITExtensionCount} + missingOrdersInfoCount={missingOrdersInfoCount} /> )} @@ -214,6 +216,8 @@ const ServicesCounselingMoveInfo = () => { } diff --git a/src/pages/Office/ServicesCounselingQueue/ServicesCounselingQueue.jsx b/src/pages/Office/ServicesCounselingQueue/ServicesCounselingQueue.jsx index 398bc9fe860..dc2e4421932 100644 --- a/src/pages/Office/ServicesCounselingQueue/ServicesCounselingQueue.jsx +++ b/src/pages/Office/ServicesCounselingQueue/ServicesCounselingQueue.jsx @@ -10,7 +10,7 @@ import SelectFilter from 'components/Table/Filters/SelectFilter'; import DateSelectFilter from 'components/Table/Filters/DateSelectFilter'; import TableQueue from 'components/Table/TableQueue'; import { - BRANCH_OPTIONS_WITH_MARINE_CORPS, + BRANCH_OPTIONS, SERVICE_COUNSELING_MOVE_STATUS_LABELS, SERVICE_COUNSELING_PPM_TYPE_OPTIONS, SERVICE_COUNSELING_PPM_TYPE_LABELS, @@ -148,7 +148,7 @@ export const counselingColumns = (moveLockFlag, originLocationList, supervisor) isFilterable: true, Filter: (props) => ( // eslint-disable-next-line react/jsx-props-no-spreading - + ), }, ), @@ -252,7 +252,7 @@ export const closeoutColumns = (moveLockFlag, ppmCloseoutGBLOC, ppmCloseoutOrigi isFilterable: true, Filter: (props) => ( // eslint-disable-next-line react/jsx-props-no-spreading - + ), }, ), diff --git a/src/pages/Office/TXOMoveInfo/TXOMoveInfo.jsx b/src/pages/Office/TXOMoveInfo/TXOMoveInfo.jsx index a931a110286..bbe6c9c4f09 100644 --- a/src/pages/Office/TXOMoveInfo/TXOMoveInfo.jsx +++ b/src/pages/Office/TXOMoveInfo/TXOMoveInfo.jsx @@ -39,6 +39,7 @@ const TXOMoveInfo = () => { const [excessWeightRiskCount, setExcessWeightRiskCount] = React.useState(0); const [pendingPaymentRequestCount, setPendingPaymentRequestCount] = React.useState(0); const [unapprovedSITExtensionCount, setUnApprovedSITExtensionCount] = React.useState(0); + const [missingOrdersInfoCount, setMissingOrdersInfoCount] = useState(0); const [moveLockFlag, setMoveLockFlag] = useState(false); const [isMoveLocked, setIsMoveLocked] = useState(false); @@ -150,6 +151,7 @@ const TXOMoveInfo = () => { excessWeightRiskCount={excessWeightRiskCount} pendingPaymentRequestCount={pendingPaymentRequestCount} unapprovedSITExtensionCount={unapprovedSITExtensionCount} + missingOrdersInfoCount={missingOrdersInfoCount} moveCode={moveCode} reportId={reportId} order={order} @@ -177,6 +179,8 @@ const TXOMoveInfo = () => { } setExcessWeightRiskCount={setExcessWeightRiskCount} setUnapprovedSITExtensionCount={setUnApprovedSITExtensionCount} + missingOrdersInfoCount={missingOrdersInfoCount} + setMissingOrdersInfoCount={setMissingOrdersInfoCount} isMoveLocked={isMoveLocked} /> } diff --git a/src/pages/PrimeUI/Shipment/PrimeUIShipmentUpdate.jsx b/src/pages/PrimeUI/Shipment/PrimeUIShipmentUpdate.jsx index 699700e7ee8..bb408d21f55 100644 --- a/src/pages/PrimeUI/Shipment/PrimeUIShipmentUpdate.jsx +++ b/src/pages/PrimeUI/Shipment/PrimeUIShipmentUpdate.jsx @@ -20,7 +20,12 @@ import formStyles from 'styles/form.module.scss'; import WizardNavigation from 'components/Customer/WizardNavigation/WizardNavigation'; import { requiredAddressSchema, addressSchema } from 'utils/validation'; import { isEmpty, isValidWeight } from 'shared/utils'; -import { formatAddressForPrimeAPI, formatSwaggerDate, fromPrimeAPIAddressFormat } from 'utils/formatters'; +import { + formatAddressForPrimeAPI, + formatExtraAddressForPrimeAPI, + formatSwaggerDate, + fromPrimeAPIAddressFormat, +} from 'utils/formatters'; import PrimeUIShipmentUpdateForm from 'pages/PrimeUI/Shipment/PrimeUIShipmentUpdateForm'; import PrimeUIShipmentUpdatePPMForm from 'pages/PrimeUI/Shipment/PrimeUIShipmentUpdatePPMForm'; import { setFlashMessage as setFlashMessageAction } from 'store/flash/actions'; @@ -128,7 +133,11 @@ const PrimeUIShipmentUpdate = ({ setFlashMessage }) => { const reformatPrimeApiSecondaryDeliveryAddress = fromPrimeAPIAddressFormat(shipment.secondaryDeliveryAddress); const reformatPrimeApiTertiaryDeliveryAddress = fromPrimeAPIAddressFormat(shipment.tertiaryDeliveryAddress); const editablePickupAddress = isEmpty(reformatPrimeApiPickupAddress); + const editableSecondaryPickupAddress = isEmpty(reformatPrimeApiSecondaryPickupAddress); + const editableTertiaryPickupAddress = isEmpty(reformatPrimeApiTertiaryPickupAddress); const editableDestinationAddress = isEmpty(reformatPrimeApiDestinationAddress); + const editableSecondaryDeliveryAddress = isEmpty(reformatPrimeApiSecondaryDeliveryAddress); + const editableTertiaryDeliveryAddress = isEmpty(reformatPrimeApiTertiaryDeliveryAddress); const onCancelShipmentClick = () => { mutateMTOShipmentStatus({ mtoShipmentID: shipmentId, ifMatchETag: shipment.eTag }).then(() => { @@ -144,8 +153,10 @@ const PrimeUIShipmentUpdate = ({ setFlashMessage }) => { expectedDepartureDate, pickupAddress, secondaryPickupAddress, + tertiaryPickupAddress, destinationAddress, secondaryDestinationAddress, + tertiaryDestinationAddress, sitExpected, sitLocation, sitEstimatedWeight, @@ -156,7 +167,9 @@ const PrimeUIShipmentUpdate = ({ setFlashMessage }) => { proGearWeight, spouseProGearWeight, hasSecondaryPickupAddress, + hasTertiaryPickupAddress, hasSecondaryDestinationAddress, + hasTertiaryDestinationAddress, }, counselorRemarks, } = values; @@ -167,10 +180,16 @@ const PrimeUIShipmentUpdate = ({ setFlashMessage }) => { secondaryPickupAddress: isEmpty(secondaryPickupAddress) ? emptyAddress : formatAddressForPrimeAPI(secondaryPickupAddress), + tertiaryPickupAddress: isEmpty(tertiaryPickupAddress) + ? emptyAddress + : formatAddressForPrimeAPI(tertiaryPickupAddress), destinationAddress: isEmpty(destinationAddress) ? null : formatAddressForPrimeAPI(destinationAddress), secondaryDestinationAddress: isEmpty(secondaryDestinationAddress) ? emptyAddress : formatAddressForPrimeAPI(secondaryDestinationAddress), + tertiaryDestinationAddress: isEmpty(tertiaryDestinationAddress) + ? emptyAddress + : formatAddressForPrimeAPI(tertiaryDestinationAddress), sitExpected, ...(sitExpected && { sitLocation: sitLocation || null, @@ -185,7 +204,9 @@ const PrimeUIShipmentUpdate = ({ setFlashMessage }) => { spouseProGearWeight: spouseProGearWeight ? parseInt(spouseProGearWeight, 10) : null, }), hasSecondaryPickupAddress: hasSecondaryPickupAddress === 'true', + hasTertiaryPickupAddress: hasTertiaryPickupAddress === 'true', hasSecondaryDestinationAddress: hasSecondaryDestinationAddress === 'true', + hasTertiaryDestinationAddress: hasTertiaryDestinationAddress === 'true', }, counselorRemarks: counselorRemarks || null, }; @@ -200,7 +221,11 @@ const PrimeUIShipmentUpdate = ({ setFlashMessage }) => { actualDeliveryDate, scheduledDeliveryDate, pickupAddress, + secondaryPickupAddress, + tertiaryPickupAddress, destinationAddress, + secondaryDeliveryAddress, + tertiaryDeliveryAddress, destinationType, diversion, } = values; @@ -215,7 +240,19 @@ const PrimeUIShipmentUpdate = ({ setFlashMessage }) => { scheduledDeliveryDate: scheduledDeliveryDate ? formatSwaggerDate(scheduledDeliveryDate) : null, actualDeliveryDate: actualDeliveryDate ? formatSwaggerDate(actualDeliveryDate) : null, pickupAddress: editablePickupAddress ? formatAddressForPrimeAPI(pickupAddress) : null, + secondaryPickupAddress: editableSecondaryPickupAddress + ? formatExtraAddressForPrimeAPI(secondaryPickupAddress) + : null, + tertiaryPickupAddress: editableTertiaryPickupAddress + ? formatExtraAddressForPrimeAPI(tertiaryPickupAddress) + : null, destinationAddress: editableDestinationAddress ? formatAddressForPrimeAPI(destinationAddress) : null, + secondaryDeliveryAddress: editableSecondaryDeliveryAddress + ? formatExtraAddressForPrimeAPI(secondaryDeliveryAddress) + : null, + tertiaryDeliveryAddress: editableTertiaryDeliveryAddress + ? formatExtraAddressForPrimeAPI(tertiaryDeliveryAddress) + : null, destinationType, diversion, }; @@ -237,12 +274,18 @@ const PrimeUIShipmentUpdate = ({ setFlashMessage }) => { secondaryPickupAddress: shipment.ppmShipment.secondaryPickupAddress ? formatAddressForPrimeAPI(shipment.ppmShipment.secondaryPickupAddress) : emptyAddress, + tertiaryPickupAddress: shipment.ppmShipment.tertiaryPickupAddress + ? formatAddressForPrimeAPI(shipment.ppmShipment.tertiaryPickupAddress) + : emptyAddress, destinationAddress: shipment.ppmShipment.destinationAddress ? formatAddressForPrimeAPI(shipment.ppmShipment.destinationAddress) : emptyAddress, secondaryDestinationAddress: shipment.ppmShipment.secondaryDestinationAddress ? formatAddressForPrimeAPI(shipment.ppmShipment.secondaryDestinationAddress) : emptyAddress, + tertiaryDestinationAddress: shipment.ppmShipment.tertiaryDestinationAddress + ? formatAddressForPrimeAPI(shipment.ppmShipment.tertiaryDestinationAddress) + : emptyAddress, sitExpected: shipment.ppmShipment.sitExpected, sitLocation: shipment.ppmShipment.sitLocation, sitEstimatedWeight: shipment.ppmShipment.sitEstimatedWeight?.toString(), @@ -254,7 +297,9 @@ const PrimeUIShipmentUpdate = ({ setFlashMessage }) => { proGearWeight: shipment.ppmShipment.proGearWeight?.toString(), spouseProGearWeight: shipment.ppmShipment.spouseProGearWeight?.toString(), hasSecondaryPickupAddress: shipment.ppmShipment.hasSecondaryPickupAddress ? 'true' : 'false', + hasTertiaryPickupAddress: shipment.ppmShipment.hasTertiaryPickupAddress ? 'true' : 'false', hasSecondaryDestinationAddress: shipment.ppmShipment.hasSecondaryDestinationAddress ? 'true' : 'false', + hasTertiaryDestinationAddress: shipment.ppmShipment.hasTertiaryDestinationAddress ? 'true' : 'false', }, counselorRemarks: shipment.counselorRemarks || '', }; @@ -265,8 +310,10 @@ const PrimeUIShipmentUpdate = ({ setFlashMessage }) => { .typeError('Invalid date. Must be in the format: DD MMM YYYY'), pickupAddress: requiredAddressSchema.required('Required'), secondaryPickupAddress: OptionalAddressSchema, + tertiaryPickupAddress: OptionalAddressSchema, destinationAddress: requiredAddressSchema.required('Required'), secondaryDestinationAddress: OptionalAddressSchema, + tertiaryDestinationAddress: OptionalAddressSchema, sitExpected: Yup.boolean().required('Required'), sitLocation: Yup.string().when('sitExpected', { is: true, @@ -313,11 +360,13 @@ const PrimeUIShipmentUpdate = ({ setFlashMessage }) => { scheduledDeliveryDate: shipment.scheduledDeliveryDate, actualDeliveryDate: shipment.actualDeliveryDate, pickupAddress: editablePickupAddress ? emptyAddress : reformatPrimeApiPickupAddress, - secondaryPickupAddress: reformatPrimeApiSecondaryPickupAddress, - tertiaryPickupAddress: reformatPrimeApiTertiaryPickupAddress, + secondaryPickupAddress: editableSecondaryPickupAddress ? emptyAddress : reformatPrimeApiSecondaryPickupAddress, + tertiaryPickupAddress: editableTertiaryPickupAddress ? emptyAddress : reformatPrimeApiTertiaryPickupAddress, destinationAddress: editableDestinationAddress ? emptyAddress : reformatPrimeApiDestinationAddress, - secondaryDeliveryAddress: reformatPrimeApiSecondaryDeliveryAddress, - tertiaryDeliveryAddress: reformatPrimeApiTertiaryDeliveryAddress, + secondaryDeliveryAddress: editableSecondaryDeliveryAddress + ? emptyAddress + : reformatPrimeApiSecondaryDeliveryAddress, + tertiaryDeliveryAddress: editableTertiaryDeliveryAddress ? emptyAddress : reformatPrimeApiTertiaryDeliveryAddress, destinationType: shipment.destinationType, diversion: shipment.diversion, }; @@ -365,7 +414,11 @@ const PrimeUIShipmentUpdate = ({ setFlashMessage }) => { editableProGearWeightActualField={editableProGearWeightActualField} editableSpouseProGearWeightActualField={editableSpouseProGearWeightActualField} editablePickupAddress={editablePickupAddress} + editableSecondaryPickupAddress={editableSecondaryPickupAddress} + editableTertiaryPickupAddress={editableTertiaryPickupAddress} editableDestinationAddress={editableDestinationAddress} + editableSecondaryDeliveryAddress={editableSecondaryDeliveryAddress} + editableTertiaryDeliveryAddress={editableTertiaryDeliveryAddress} estimatedWeight={initialValues.estimatedWeight} actualWeight={initialValues.actualWeight} actualProGearWeight={initialValues.actualProGearWeight} @@ -378,6 +431,7 @@ const PrimeUIShipmentUpdate = ({ setFlashMessage }) => { secondaryDeliveryAddress={initialValues.secondaryDeliveryAddress} tertiaryDeliveryAddress={initialValues.tertiaryDeliveryAddress} diversion={initialValues.diversion} + shipmentType={shipment.shipmentType} /> )}
diff --git a/src/pages/PrimeUI/Shipment/PrimeUIShipmentUpdateAddress.jsx b/src/pages/PrimeUI/Shipment/PrimeUIShipmentUpdateAddress.jsx index ee1bf418c34..0d234eb1136 100644 --- a/src/pages/PrimeUI/Shipment/PrimeUIShipmentUpdateAddress.jsx +++ b/src/pages/PrimeUI/Shipment/PrimeUIShipmentUpdateAddress.jsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { useNavigate, useParams, generatePath } from 'react-router-dom'; +import { useNavigate, useParams, generatePath, useLocation } from 'react-router-dom'; import { useQueryClient, useMutation } from '@tanstack/react-query'; import { Alert, Grid, GridContainer } from '@trussworks/react-uswds'; import * as Yup from 'yup'; @@ -11,26 +11,24 @@ import { usePrimeSimulatorGetMove } from 'hooks/queries'; import LoadingPlaceholder from 'shared/LoadingPlaceholder'; import SomethingWentWrong from 'shared/SomethingWentWrong'; import { primeSimulatorRoutes } from 'constants/routes'; -import { addressSchema } from 'utils/validation'; +import { ZIP_CODE_REGEX } from 'utils/validation'; import scrollToTop from 'shared/scrollToTop'; import { updatePrimeMTOShipmentAddress } from 'services/primeApi'; import primeStyles from 'pages/PrimeUI/Prime.module.scss'; import { isEmpty } from 'shared/utils'; import { fromPrimeAPIAddressFormat } from 'utils/formatters'; import { PRIME_SIMULATOR_MOVE } from 'constants/queryKeys'; +import { getAddressLabel } from 'shared/constants'; -const updatePickupAddressSchema = Yup.object().shape({ +const updateAddressSchema = Yup.object().shape({ addressID: Yup.string(), - pickupAddress: Yup.object().shape({ - address: addressSchema, - }), - eTag: Yup.string(), -}); - -const updateDestinationAddressSchema = Yup.object().shape({ - addressID: Yup.string(), - destinationAddress: Yup.object().shape({ - address: addressSchema, + address: Yup.object().shape({ + id: Yup.string(), + streetAddress1: Yup.string().required('Required'), + streetAddress2: Yup.string(), + city: Yup.string().required('Required'), + state: Yup.string().required('Required').length(2, 'Must use state abbreviation'), + postalCode: Yup.string().required('Required').matches(ZIP_CODE_REGEX, 'Must be valid zip code'), }), eTag: Yup.string(), }); @@ -42,6 +40,11 @@ const PrimeUIShipmentUpdateAddress = () => { const mtoShipments = moveTaskOrder?.mtoShipments; const shipment = mtoShipments?.find((mtoShipment) => mtoShipment?.id === shipmentId); const navigate = useNavigate(); + const location = useLocation(); + + const addressType = location?.state?.addressType; + const addressLabel = getAddressLabel(addressType); + const addressData = shipment ? shipment[addressType] : null; const handleClose = () => { navigate(generatePath(primeSimulatorRoutes.VIEW_MOVE_PATH, { moveCodeOrID })); @@ -52,12 +55,10 @@ const PrimeUIShipmentUpdateAddress = () => { onSuccess: (updatedMTOShipmentAddress) => { const shipmentIndex = mtoShipments.findIndex((mtoShipment) => mtoShipment.id === shipmentId); let updateQuery = false; - ['pickupAddress', 'destinationAddress'].forEach((key) => { - if (updatedMTOShipmentAddress.id === mtoShipments[shipmentIndex][key].id) { - mtoShipments[shipmentIndex][key] = updatedMTOShipmentAddress; - updateQuery = true; - } - }); + if (updatedMTOShipmentAddress.id === mtoShipments[shipmentIndex][addressType].id) { + mtoShipments[shipmentIndex][addressType] = updatedMTOShipmentAddress; + updateQuery = true; + } if (updateQuery) { moveTaskOrder.mtoShipments = mtoShipments; queryClient.setQueryData([PRIME_SIMULATOR_MOVE, moveCodeOrID], moveTaskOrder); @@ -87,19 +88,16 @@ const PrimeUIShipmentUpdateAddress = () => { if (isError) return ; const onSubmit = (values, { setSubmitting }) => { - // Choose pickupAddress or destinationAddress by the presence of the object - // by the same name. It's possible that these values are blank and set to - // `undefined` or an empty string `""`. - const address = values.pickupAddress ? values.pickupAddress.address : values.destinationAddress.address; + const { streetAddress1, streetAddress2, streetAddress3, city, state, postalCode } = values.address; const body = { id: values.addressID, - streetAddress1: address.streetAddress1, - streetAddress2: address.streetAddress2, - streetAddress3: address.streetAddress3, - city: address.city, - state: address.state, - postalCode: address.postalCode, + streetAddress1, + streetAddress2, + streetAddress3, + city, + state, + postalCode, }; // Check if the address payload contains any blank properties and remove @@ -121,24 +119,13 @@ const PrimeUIShipmentUpdateAddress = () => { }); }; - const reformatPrimeApiPickupAddress = fromPrimeAPIAddressFormat(shipment.pickupAddress); - const reformatPrimeApiDestinationAddress = fromPrimeAPIAddressFormat(shipment.destinationAddress); - const editablePickupAddress = !isEmpty(reformatPrimeApiPickupAddress); - const editableDestinationAddress = !isEmpty(reformatPrimeApiDestinationAddress); + const reformatPriApiAddress = fromPrimeAPIAddressFormat(addressData); + const editableAddress = !isEmpty(reformatPriApiAddress); - const initialValuesPickupAddress = { - addressID: shipment.pickupAddress?.id, - pickupAddress: { - address: reformatPrimeApiPickupAddress, - }, - eTag: shipment.pickupAddress?.eTag, - }; - const initialValuesDestinationAddress = { - addressID: shipment.destinationAddress?.id, - destinationAddress: { - address: reformatPrimeApiDestinationAddress, - }, - eTag: shipment.destinationAddress?.eTag, + const initialValues = { + addressID: addressData?.id, + address: reformatPriApiAddress, + eTag: addressData?.eTag, }; return ( @@ -155,23 +142,14 @@ const PrimeUIShipmentUpdateAddress = () => {
)} -

Update Existing Pickup & Destination Address

- {editablePickupAddress && ( - - )} - {editableDestinationAddress && ( +

Update Existing {`${addressLabel}`}

+ {editableAddress && ( )} diff --git a/src/pages/PrimeUI/Shipment/PrimeUIShipmentUpdateAddress.test.jsx b/src/pages/PrimeUI/Shipment/PrimeUIShipmentUpdateAddress.test.jsx index 3f0f87a8e67..5e2083a7fd7 100644 --- a/src/pages/PrimeUI/Shipment/PrimeUIShipmentUpdateAddress.test.jsx +++ b/src/pages/PrimeUI/Shipment/PrimeUIShipmentUpdateAddress.test.jsx @@ -15,6 +15,7 @@ const mockNavigate = jest.fn(); jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useNavigate: () => mockNavigate, + useLocation: () => ({ state: { addressType: 'pickupAddress' } }), })); jest.mock('hooks/queries', () => ({ @@ -72,54 +73,6 @@ const moveReturnValue = { isError: false, }; -const noDestinationAddressReturnValue = { - moveTaskOrder: { - id: '1', - moveCode: 'LN4T89', - mtoShipments: [ - { - id: '4', - shipmentType: 'HHG', - requestedPickupDate: '2021-12-01', - pickupAddress: { - id: '1', - streetAddress1: '800 Madison Avenue', - city: 'New York', - state: 'NY', - postalCode: '10002', - }, - destinationAddress: null, - }, - ], - }, - isLoading: false, - isError: false, -}; - -const noPickupAddressReturnValue = { - moveTaskOrder: { - id: '1', - moveCode: 'LN4T89', - mtoShipments: [ - { - id: '4', - shipmentType: 'HHG', - requestedPickupDate: '2021-12-01', - pickupAddress: null, - destinationAddress: { - id: '2', - streetAddress1: '100 1st Avenue', - city: 'New York', - state: 'NY', - postalCode: '10001', - }, - }, - ], - }, - isLoading: false, - isError: false, -}; - const renderComponent = () => { render( @@ -168,7 +121,7 @@ describe('PrimeUIShipmentUpdateAddress page', () => { renderComponent(); const pageHeading = await screen.getByRole('heading', { - name: 'Update Existing Pickup & Destination Address', + name: 'Update Existing Pickup Address', level: 1, }); expect(pageHeading).toBeInTheDocument(); @@ -179,74 +132,11 @@ describe('PrimeUIShipmentUpdateAddress page', () => { const shipment = moveTaskOrder.mtoShipments[shipmentIndex]; await waitFor(() => { - expect(screen.getByRole('heading', { name: /Pickup address/, level: 2 })); expect(screen.getAllByLabelText('Address 1')[0]).toHaveValue(shipment.pickupAddress.streetAddress1); expect(screen.getAllByLabelText(/Address 2/)[0]).toHaveValue(''); expect(screen.getAllByLabelText('City')[0]).toHaveValue(shipment.pickupAddress.city); expect(screen.getAllByLabelText('State')[0]).toHaveValue(shipment.pickupAddress.state); expect(screen.getAllByLabelText('ZIP')[0]).toHaveValue(shipment.pickupAddress.postalCode); - expect(screen.getByRole('heading', { name: /Destination address/, level: 2 })); - expect(screen.getAllByLabelText('Address 1')[1]).toHaveValue(shipment.destinationAddress.streetAddress1); - expect(screen.getAllByLabelText(/Address 2/)[1]).toHaveValue(''); - expect(screen.getAllByLabelText('City')[1]).toHaveValue(shipment.destinationAddress.city); - expect(screen.getAllByLabelText('State')[1]).toHaveValue(shipment.destinationAddress.state); - expect(screen.getAllByLabelText('ZIP')[1]).toHaveValue(shipment.destinationAddress.postalCode); - }); - }); - - it('displays only pickup address form', async () => { - usePrimeSimulatorGetMove.mockReturnValue(noDestinationAddressReturnValue); - - renderComponent(); - - const pageHeading = await screen.getByRole('heading', { - name: 'Update Existing Pickup & Destination Address', - level: 1, - }); - expect(pageHeading).toBeInTheDocument(); - - const shipmentIndex = noDestinationAddressReturnValue.moveTaskOrder.mtoShipments.findIndex( - (mtoShipment) => mtoShipment.id === routingParams.shipmentId, - ); - const shipment = noDestinationAddressReturnValue.moveTaskOrder.mtoShipments[shipmentIndex]; - - await waitFor(() => { - expect(screen.getByRole('heading', { name: /Pickup address/, level: 2 })); - expect(screen.getAllByLabelText('Address 1').length).toBe(1); - expect(screen.getAllByLabelText('Address 1')[0]).toHaveValue(shipment.pickupAddress.streetAddress1); - expect(screen.getAllByLabelText(/Address 2/)[0]).toHaveValue(''); - expect(screen.getAllByLabelText('City')[0]).toHaveValue(shipment.pickupAddress.city); - expect(screen.getAllByLabelText('State')[0]).toHaveValue(shipment.pickupAddress.state); - expect(screen.getAllByLabelText('ZIP')[0]).toHaveValue(shipment.pickupAddress.postalCode); - expect(shipment.destinationAddress).toBeNull(); - }); - }); - - it('displays only destination address form', async () => { - usePrimeSimulatorGetMove.mockReturnValue(noPickupAddressReturnValue); - - renderComponent(); - - const pageHeading = await screen.getByRole('heading', { - name: 'Update Existing Pickup & Destination Address', - level: 1, - }); - expect(pageHeading).toBeInTheDocument(); - - const shipmentIndex = noPickupAddressReturnValue.moveTaskOrder.mtoShipments.findIndex( - (mtoShipment) => mtoShipment.id === routingParams.shipmentId, - ); - const shipment = noPickupAddressReturnValue.moveTaskOrder.mtoShipments[shipmentIndex]; - - await waitFor(() => { - expect(shipment.pickupAddress).toBeNull(); - expect(screen.getByRole('heading', { name: /Destination address/, level: 2 })); - expect(screen.getAllByLabelText('Address 1').length).toBe(1); - expect(screen.getAllByLabelText('Address 1')[0]).toHaveValue(shipment.destinationAddress.streetAddress1); - expect(screen.getAllByLabelText(/Address 2/)[0]).toHaveValue(''); - expect(screen.getAllByLabelText('City')[0]).toHaveValue(shipment.destinationAddress.city); - expect(screen.getAllByLabelText('State')[0]).toHaveValue(shipment.destinationAddress.state); - expect(screen.getAllByLabelText('ZIP')[0]).toHaveValue(shipment.destinationAddress.postalCode); }); }); }); @@ -304,7 +194,7 @@ describe('PrimeUIShipmentUpdateAddress page', () => { renderComponent(); await act(async () => { - expect(screen.getAllByRole('button', { name: 'Save' }).length).toBe(2); + expect(screen.getAllByRole('button', { name: 'Save' }).length).toBe(1); await userEvent.click(screen.getAllByRole('button', { name: 'Save' })[0]); }); diff --git a/src/pages/PrimeUI/Shipment/PrimeUIShipmentUpdateAddressForm.jsx b/src/pages/PrimeUI/Shipment/PrimeUIShipmentUpdateAddressForm.jsx index 7045a671fb0..ec188502922 100644 --- a/src/pages/PrimeUI/Shipment/PrimeUIShipmentUpdateAddressForm.jsx +++ b/src/pages/PrimeUI/Shipment/PrimeUIShipmentUpdateAddressForm.jsx @@ -54,12 +54,7 @@ const PrimeUIShipmentUpdateAddressForm = ({ PrimeUIShipmentUpdateAddressForm.propTypes = { initialValues: PropTypes.shape({ - pickupAddress: PropTypes.shape({ - address: ResidentialAddressShape, - }), - destinationAddress: PropTypes.shape({ - address: ResidentialAddressShape, - }), + address: ResidentialAddressShape, addressID: PropTypes.string, eTag: PropTypes.string, }).isRequired, @@ -69,7 +64,7 @@ PrimeUIShipmentUpdateAddressForm.propTypes = { addressID: PropTypes.string, eTag: PropTypes.string, }).isRequired, - addressLocation: PropTypes.oneOf(['Pickup address', 'Destination address']).isRequired, + addressLocation: PropTypes.string.isRequired, name: PropTypes.string.isRequired, }; diff --git a/src/pages/PrimeUI/Shipment/PrimeUIShipmentUpdateForm.jsx b/src/pages/PrimeUI/Shipment/PrimeUIShipmentUpdateForm.jsx index 50d8bdb2a57..795e6caf525 100644 --- a/src/pages/PrimeUI/Shipment/PrimeUIShipmentUpdateForm.jsx +++ b/src/pages/PrimeUI/Shipment/PrimeUIShipmentUpdateForm.jsx @@ -11,6 +11,7 @@ import { AddressFields } from 'components/form/AddressFields/AddressFields'; import SectionWrapper from 'components/Customer/SectionWrapper'; import formStyles from 'styles/form.module.scss'; import { shipmentDestinationTypes } from 'constants/shipments'; +import { SHIPMENT_OPTIONS } from 'shared/constants'; const emptyAddressShape = { streetAddress1: '', @@ -28,7 +29,11 @@ const PrimeUIShipmentUpdateForm = ({ editableProGearWeightActualField, editableSpouseProGearWeightActualField, editablePickupAddress, + editableSecondaryPickupAddress, + editableTertiaryPickupAddress, editableDestinationAddress, + editableSecondaryDeliveryAddress, + editableTertiaryDeliveryAddress, requestedPickupDate, estimatedWeight, actualWeight, @@ -40,7 +45,10 @@ const PrimeUIShipmentUpdateForm = ({ destinationAddress, secondaryDeliveryAddress, tertiaryDeliveryAddress, + shipmentType, }) => { + const isNTS = shipmentType === SHIPMENT_OPTIONS.NTS; + const isNTSR = shipmentType === SHIPMENT_OPTIONS.NTSR; return (

Shipment Dates

@@ -146,17 +154,29 @@ const PrimeUIShipmentUpdateForm = ({
Pickup Address
{editablePickupAddress && } {!editablePickupAddress && formatAddress(pickupAddress)} -
Second Pickup Address
- {formatAddress(secondaryPickupAddress)} -
Third Pickup Address
- {formatAddress(tertiaryPickupAddress)} + {!isNTSR && ( + <> +
Second Pickup Address
+ {editableSecondaryPickupAddress && } + {!editableSecondaryPickupAddress && formatAddress(secondaryPickupAddress)} +
Third Pickup Address
+ {editableTertiaryPickupAddress && } + {!editableTertiaryPickupAddress && formatAddress(tertiaryPickupAddress)} + + )}
Destination Address
{editableDestinationAddress && } {!editableDestinationAddress && formatAddress(destinationAddress)} -
Second Destination Address
- {formatAddress(secondaryDeliveryAddress)} -
Third Destination Address
- {formatAddress(tertiaryDeliveryAddress)} + {!isNTS && ( + <> +
Second Delivery Address
+ {editableSecondaryDeliveryAddress && } + {!editableSecondaryDeliveryAddress && formatAddress(secondaryDeliveryAddress)} +
Third Delivery Address
+ {editableTertiaryDeliveryAddress && } + {!editableTertiaryDeliveryAddress && formatAddress(tertiaryDeliveryAddress)} + + )} { const { values } = useFormikContext(); - const { sitExpected, hasProGear, hasSecondaryPickupAddress, hasSecondaryDestinationAddress } = values.ppmShipment; + const { + sitExpected, + hasProGear, + hasSecondaryPickupAddress, + hasTertiaryPickupAddress, + hasSecondaryDestinationAddress, + hasTertiaryDestinationAddress, + } = values.ppmShipment; return ( @@ -62,7 +69,41 @@ const PrimeUIShipmentUpdatePPMForm = () => { />
- {hasSecondaryPickupAddress === 'true' && } + {hasSecondaryPickupAddress === 'true' && ( + <> + +

Third pickup location

+ +

+ Will the movers pick up any belongings from a third address? (Must be near the pickup address. + Subject to approval.) +

+
+ + +
+
+ {hasTertiaryPickupAddress === 'true' && } + + )} )} /> @@ -103,7 +144,41 @@ const PrimeUIShipmentUpdatePPMForm = () => {
{hasSecondaryDestinationAddress === 'true' && ( - + <> + +

Third destination location

+ +

+ Will the movers pick up any belongings from a third address? (Must be near the Destination address. + Subject to approval.) +

+
+ + +
+
+ {hasTertiaryDestinationAddress === 'true' && ( + + )} + )} )} diff --git a/src/pages/PrimeUI/Shipment/PrimeUIShipmentUpdatePPMForm.test.jsx b/src/pages/PrimeUI/Shipment/PrimeUIShipmentUpdatePPMForm.test.jsx index e6ca000f04a..be3ea189cd7 100644 --- a/src/pages/PrimeUI/Shipment/PrimeUIShipmentUpdatePPMForm.test.jsx +++ b/src/pages/PrimeUI/Shipment/PrimeUIShipmentUpdatePPMForm.test.jsx @@ -69,6 +69,14 @@ const shipment = { state: 'KY', postalCode: '42702', }, + tertiaryPickupAddress: { + streetAddress1: '123 Test Lane', + streetAddress2: '234 Test Lane', + streetAddress3: 'Test Woman', + city: 'Missoula', + state: 'MT', + postalCode: '59801', + }, destinationAddress: { streetAddress1: '222 Test Street', streetAddress2: '333 Test Street', @@ -85,8 +93,18 @@ const shipment = { state: 'KY', postalCode: '42701', }, + tertiaryDestinationAddress: { + streetAddress1: '321 Test Lane', + streetAddress2: '432 Test Lane', + streetAddress3: 'Test Woman', + city: 'Silver Spring', + state: 'MD', + postalCode: '20906', + }, hasSecondaryPickupAddress: 'true', hasSecondaryDestinationAddress: 'true', + hasTertiaryPickupAddress: 'true', + hasTertiaryDestinationAddress: 'true', submittedAt: '2022-07-01T13:41:33.252Z', updatedAt: '2022-07-01T14:23:19.780Z', }, @@ -106,6 +124,18 @@ const shipment = { state: null, streetAddress1: null, }, + tertiaryDeliveryAddress: { + city: null, + postalCode: null, + state: null, + streetAddress1: null, + }, + tertiaryPickupAddress: { + city: null, + postalCode: null, + state: null, + streetAddress1: null, + }, shipmentType: 'PPM', status: 'APPROVED', updatedAt: '2022-07-01T14:23:19.738Z', @@ -173,33 +203,63 @@ describe('PrimeUIShipmentUpdatePPMForm', () => { ); expect(await screen.getAllByLabelText('Address 1')[2]).toHaveValue( - initialValues.ppmShipment.destinationAddress.streetAddress1, + initialValues.ppmShipment.tertiaryPickupAddress.streetAddress1, ); expect(await screen.getAllByLabelText(/Address 2/)[2]).toHaveValue( - initialValues.ppmShipment.destinationAddress.streetAddress2, + initialValues.ppmShipment.tertiaryPickupAddress.streetAddress2, + ); + expect(await screen.getAllByLabelText('City')[2]).toHaveValue(initialValues.ppmShipment.tertiaryPickupAddress.city); + expect(await screen.getAllByLabelText('State')[2]).toHaveValue( + initialValues.ppmShipment.tertiaryPickupAddress.state, ); - expect(await screen.getAllByLabelText('City')[2]).toHaveValue(initialValues.ppmShipment.destinationAddress.city); - expect(await screen.getAllByLabelText('State')[2]).toHaveValue(initialValues.ppmShipment.destinationAddress.state); expect(await screen.getAllByLabelText('ZIP')[2]).toHaveValue( - initialValues.ppmShipment.destinationAddress.postalCode, + initialValues.ppmShipment.tertiaryPickupAddress.postalCode, ); expect(await screen.getAllByLabelText('Address 1')[3]).toHaveValue( - initialValues.ppmShipment.secondaryDestinationAddress.streetAddress1, + initialValues.ppmShipment.destinationAddress.streetAddress1, ); expect(await screen.getAllByLabelText(/Address 2/)[3]).toHaveValue( + initialValues.ppmShipment.destinationAddress.streetAddress2, + ); + expect(await screen.getAllByLabelText('City')[3]).toHaveValue(initialValues.ppmShipment.destinationAddress.city); + expect(await screen.getAllByLabelText('State')[3]).toHaveValue(initialValues.ppmShipment.destinationAddress.state); + expect(await screen.getAllByLabelText('ZIP')[3]).toHaveValue( + initialValues.ppmShipment.destinationAddress.postalCode, + ); + + expect(await screen.getAllByLabelText('Address 1')[4]).toHaveValue( + initialValues.ppmShipment.secondaryDestinationAddress.streetAddress1, + ); + expect(await screen.getAllByLabelText(/Address 2/)[4]).toHaveValue( initialValues.ppmShipment.secondaryDestinationAddress.streetAddress2, ); - expect(await screen.getAllByLabelText('City')[3]).toHaveValue( + expect(await screen.getAllByLabelText('City')[4]).toHaveValue( initialValues.ppmShipment.secondaryDestinationAddress.city, ); - expect(await screen.getAllByLabelText('State')[3]).toHaveValue( + expect(await screen.getAllByLabelText('State')[4]).toHaveValue( initialValues.ppmShipment.secondaryDestinationAddress.state, ); - expect(await screen.getAllByLabelText('ZIP')[3]).toHaveValue( + expect(await screen.getAllByLabelText('ZIP')[4]).toHaveValue( initialValues.ppmShipment.secondaryDestinationAddress.postalCode, ); + expect(await screen.getAllByLabelText('Address 1')[5]).toHaveValue( + initialValues.ppmShipment.tertiaryDestinationAddress.streetAddress1, + ); + expect(await screen.getAllByLabelText(/Address 2/)[5]).toHaveValue( + initialValues.ppmShipment.tertiaryDestinationAddress.streetAddress2, + ); + expect(await screen.getAllByLabelText('City')[5]).toHaveValue( + initialValues.ppmShipment.tertiaryDestinationAddress.city, + ); + expect(await screen.getAllByLabelText('State')[5]).toHaveValue( + initialValues.ppmShipment.tertiaryDestinationAddress.state, + ); + expect(await screen.getAllByLabelText('ZIP')[5]).toHaveValue( + initialValues.ppmShipment.tertiaryDestinationAddress.postalCode, + ); + expect(await screen.findByText('Storage In Transit (SIT)')).toBeInTheDocument(); expect(await screen.findByLabelText('SIT Expected')).toBeChecked(); expect(await screen.findByLabelText('SIT Location')).toHaveValue(initialValues.ppmShipment.sitLocation); diff --git a/src/services/ghcApi.js b/src/services/ghcApi.js index 2537eddf30c..0692d3e02f4 100644 --- a/src/services/ghcApi.js +++ b/src/services/ghcApi.js @@ -848,6 +848,10 @@ export async function patchPPMSIT({ ppmShipmentId, payload, eTag }) { ); } +export async function bulkDownloadPaymentRequest(paymentRequestID) { + return makeGHCRequestRaw('paymentRequests.bulkDownload', { paymentRequestID }); +} + export async function dateSelectionIsWeekendHoliday(countryCode, date) { return makeGHCRequestRaw( 'calendar.isDateWeekendHoliday', diff --git a/src/shared/constants.js b/src/shared/constants.js index 4fb0ab134a8..7852275dc1b 100644 --- a/src/shared/constants.js +++ b/src/shared/constants.js @@ -198,3 +198,23 @@ export const MOVE_DOCUMENT_TYPE = { AMENDMENTS: 'AMENDMENTS', SUPPORTING: 'SUPPORTING', }; + +export const ADDRESS_TYPES = { + PICKUP: 'pickupAddress', + SECOND_PICKUP: 'secondaryPickupAddress', + THIRD_PICKUP: 'tertiaryPickupAddress', + DESTINATION: 'destinationAddress', + SECOND_DESTINATION: 'secondaryDeliveryAddress', + THIRD_DESTINATION: 'tertiaryDeliveryAddress', +}; + +const ADDRESS_LABELS_MAP = { + [ADDRESS_TYPES.PICKUP]: 'Pickup Address', + [ADDRESS_TYPES.SECOND_PICKUP]: 'Second Pickup Address', + [ADDRESS_TYPES.THIRD_PICKUP]: 'Third Pickup Address', + [ADDRESS_TYPES.DESTINATION]: 'Destination Address', + [ADDRESS_TYPES.SECOND_DESTINATION]: 'Second Destination Address', + [ADDRESS_TYPES.THIRD_DESTINATION]: 'Third Destination Address', +}; + +export const getAddressLabel = (type) => ADDRESS_LABELS_MAP[type]; diff --git a/src/shared/styles/colors.scss b/src/shared/styles/colors.scss index 4914ff82a5b..15fb42e6b02 100644 --- a/src/shared/styles/colors.scss +++ b/src/shared/styles/colors.scss @@ -19,8 +19,8 @@ $base-darker: #3d4551; $base-darkest: #171717; //Group 4: Form State colors -$error: #d63e04; -$error-dark: #A03003; +$error: #e34b11; +$error-dark: #a03003; $error-light: #f4e3db; $warning: #ffbe2e; $warning-light: #faf3d1; @@ -38,7 +38,7 @@ $accent-ub: #f2938c; $accent-nts: #d85bef; $accent-ntsr: #8168b3; $accent-boat: #5d92ba; -$accent-pro-gear: #71767A; +$accent-pro-gear: #71767a; $accent-default: $base-light; // Group 6 Info colors diff --git a/src/utils/customer.js b/src/utils/customer.js index b8d54cf2b0a..af84bb80de7 100644 --- a/src/utils/customer.js +++ b/src/utils/customer.js @@ -23,3 +23,28 @@ export const findNextServiceMemberStep = (profileState) => { return generalRoutes.HOME_PATH; } }; + +export const generateUniqueDodid = () => { + const prefix = 'SM'; + + // Custom epoch start date (e.g., 2024-01-01), generates something like 1704067200000 + const customEpoch = new Date('2024-01-01').getTime(); + const now = Date.now(); + + // Calculate milliseconds since custom epoch, then convert to an 8-digit integer + const uniqueNumber = Math.floor((now - customEpoch) / 1000); // Dividing by 1000 to reduce to seconds + + // Convert the unique number to a string, ensuring it has 8 digits + const uniqueStr = uniqueNumber.toString().slice(0, 8).padStart(8, '0'); + + return prefix + uniqueStr; +}; + +export const generateUniqueEmplid = () => { + const prefix = 'SM'; + const customEpoch = new Date('2024-01-01').getTime(); + const now = Date.now(); + const uniqueNumber = Math.floor((now - customEpoch) / 1000) % 100000; // Modulo 100000 ensures it's 5 digits + const uniqueStr = uniqueNumber.toString().padStart(5, '0'); + return prefix + uniqueStr; +}; diff --git a/src/utils/formatters.js b/src/utils/formatters.js index 139ef5b179f..50b54459236 100644 --- a/src/utils/formatters.js +++ b/src/utils/formatters.js @@ -406,6 +406,19 @@ export const formatAddressForPrimeAPI = (address) => { }; }; +export const formatExtraAddressForPrimeAPI = (address) => { + const { streetAddress1, city, state, postalCode } = address; + if (streetAddress1 === '' || city === '' || state === '' || postalCode === '') return null; + return { + streetAddress1: address.streetAddress1, + streetAddress2: address.streetAddress2, + streetAddress3: address.streetAddress3, + city: address.city, + state: address.state, + postalCode: address.postalCode, + }; +}; + const emptyAddress = { streetAddress1: '', streetAddress2: '', diff --git a/swagger-def/definitions/ServiceItemParamName.yaml b/swagger-def/definitions/ServiceItemParamName.yaml index 066dd32d45e..c3361653fbe 100644 --- a/swagger-def/definitions/ServiceItemParamName.yaml +++ b/swagger-def/definitions/ServiceItemParamName.yaml @@ -69,3 +69,4 @@ enum: - StandaloneCrate - StandaloneCrateCap - UncappedRequestTotal + - LockedPriceCents diff --git a/swagger-def/ghc.yaml b/swagger-def/ghc.yaml index d34a2e03267..e0f9390964c 100644 --- a/swagger-def/ghc.yaml +++ b/swagger-def/ghc.yaml @@ -3141,6 +3141,36 @@ paths: summary: Updates status of a payment request by id x-permissions: - update.paymentRequest + '/payment-requests/{paymentRequestID}/bulkDownload': + parameters: + - description: the id for the payment-request with files to be downloaded + in: path + name: paymentRequestID + required: true + type: string + get: + summary: Downloads all Payment Request documents as a PDF + description: | + This endpoint downloads all uploaded payment request documentation combined into a single PDF. + operationId: bulkDownload + tags: + - paymentRequests + produces: + - application/pdf + responses: + '200': + headers: + Content-Disposition: + type: string + description: File name to download + description: Payment Request Files PDF + schema: + format: binary + type: file + '400': + $ref: '#/responses/InvalidRequest' + '500': + $ref: '#/responses/ServerError' /documents/{documentId}: get: summary: Returns a document @@ -3797,8 +3827,8 @@ paths: get: produces: - application/json - summary: Returns the transportation offices matching the search query - description: Returns the transportation offices matching the search query + summary: Returns the transportation offices matching the search query that is enabled for PPM closeout + description: Returns the transportation offices matching the search query that is enabled for PPM closeout operationId: getTransportationOffices tags: - transportationOffice @@ -4377,7 +4407,7 @@ definitions: type: string format: uuid example: c56a4180-65aa-42ec-a945-5fd21dec0538 - dodID: + edipi: type: string userID: type: string @@ -4516,8 +4546,9 @@ definitions: $ref: 'definitions/Affiliation.yaml' edipi: type: string - example: John - x-nullable: true + example: '1234567890' + maxLength: 10 + x-nullable: false emplid: type: string example: '9485155' diff --git a/swagger-def/internal.yaml b/swagger-def/internal.yaml index 679236a358b..134d05f587a 100644 --- a/swagger-def/internal.yaml +++ b/swagger-def/internal.yaml @@ -705,7 +705,7 @@ definitions: secondary_telephone: type: string format: telephone - pattern: '^[2-9]\d{2}-\d{3}-\d{4}$' + pattern: '^([2-9]\d{2}-\d{3}-\d{4})?$' example: 212-555-5555 x-nullable: true title: Secondary Phone @@ -799,7 +799,7 @@ definitions: secondary_telephone: type: string format: telephone - pattern: '^[2-9]\d{2}-\d{3}-\d{4}$' + pattern: '^([2-9]\d{2}-\d{3}-\d{4})?$' example: 212-555-5555 x-nullable: true title: Alternate phone @@ -883,7 +883,7 @@ definitions: secondary_telephone: type: string format: telephone - pattern: '^[2-9]\d{2}-\d{3}-\d{4}$' + pattern: '^([2-9]\d{2}-\d{3}-\d{4})?$' example: 212-555-5555 x-nullable: true title: Alternate Phone diff --git a/swagger-def/prime_v3.yaml b/swagger-def/prime_v3.yaml index ebefad72192..9a28d0380e2 100644 --- a/swagger-def/prime_v3.yaml +++ b/swagger-def/prime_v3.yaml @@ -561,6 +561,15 @@ definitions: An optional secondary pickup location near the origin where additional goods exist. allOf: - $ref: 'definitions/Address.yaml' + hasTertiaryPickupAddress: + type: boolean + x-omitempty: false + x-nullable: true + tertiaryPickupAddress: + description: > + An optional third pickup location near the origin where additional goods exist. + allOf: + - $ref: 'definitions/Address.yaml' destinationAddress: description: > The address of the destination location where goods are being delivered to. @@ -575,6 +584,15 @@ definitions: An optional secondary address near the destination where goods will be dropped off. allOf: - $ref: 'definitions/Address.yaml' + hasTertiaryDestinationAddress: + type: boolean + x-omitempty: false + x-nullable: true + tertiaryDestinationAddress: + description: > + An optional third address near the destination where goods will be dropped off. + allOf: + - $ref: 'definitions/Address.yaml' sitExpected: description: | Captures whether some or all of the PPM shipment will require temporary storage at the origin or destination. @@ -709,6 +727,14 @@ definitions: description: A second delivery address for this shipment, if the customer entered one. An optional field. allOf: - $ref: 'definitions/Address.yaml' + tertiaryPickupAddress: + description: A third pickup address for this shipment, if the customer entered one. An optional field. + allOf: + - $ref: 'definitions/Address.yaml' + tertiaryDeliveryAddress: + description: A third delivery address for this shipment, if the customer entered one. An optional field. + allOf: + - $ref: 'definitions/Address.yaml' storageFacility: allOf: - x-nullable: true diff --git a/swagger/ghc.yaml b/swagger/ghc.yaml index 506675a611f..b234a879f0f 100644 --- a/swagger/ghc.yaml +++ b/swagger/ghc.yaml @@ -3254,6 +3254,37 @@ paths: summary: Updates status of a payment request by id x-permissions: - update.paymentRequest + /payment-requests/{paymentRequestID}/bulkDownload: + parameters: + - description: the id for the payment-request with files to be downloaded + in: path + name: paymentRequestID + required: true + type: string + get: + summary: Downloads all Payment Request documents as a PDF + description: > + This endpoint downloads all uploaded payment request documentation + combined into a single PDF. + operationId: bulkDownload + tags: + - paymentRequests + produces: + - application/pdf + responses: + '200': + headers: + Content-Disposition: + type: string + description: File name to download + description: Payment Request Files PDF + schema: + format: binary + type: file + '400': + $ref: '#/responses/InvalidRequest' + '500': + $ref: '#/responses/ServerError' /documents/{documentId}: get: summary: Returns a document @@ -3972,8 +4003,12 @@ paths: get: produces: - application/json - summary: Returns the transportation offices matching the search query - description: Returns the transportation offices matching the search query + summary: >- + Returns the transportation offices matching the search query that is + enabled for PPM closeout + description: >- + Returns the transportation offices matching the search query that is + enabled for PPM closeout operationId: getTransportationOffices tags: - transportationOffice @@ -4568,7 +4603,7 @@ definitions: type: string format: uuid example: c56a4180-65aa-42ec-a945-5fd21dec0538 - dodID: + edipi: type: string userID: type: string @@ -4707,8 +4742,9 @@ definitions: $ref: '#/definitions/Affiliation' edipi: type: string - example: John - x-nullable: true + example: '1234567890' + maxLength: 10 + x-nullable: false emplid: type: string example: '9485155' @@ -10284,6 +10320,7 @@ definitions: - StandaloneCrate - StandaloneCrateCap - UncappedRequestTotal + - LockedPriceCents ServiceItemParamType: type: string enum: diff --git a/swagger/internal.yaml b/swagger/internal.yaml index cc75bf8e2fa..80c0dd139ed 100644 --- a/swagger/internal.yaml +++ b/swagger/internal.yaml @@ -726,7 +726,7 @@ definitions: secondary_telephone: type: string format: telephone - pattern: ^[2-9]\d{2}-\d{3}-\d{4}$ + pattern: ^([2-9]\d{2}-\d{3}-\d{4})?$ example: 212-555-5555 x-nullable: true title: Secondary Phone @@ -820,7 +820,7 @@ definitions: secondary_telephone: type: string format: telephone - pattern: ^[2-9]\d{2}-\d{3}-\d{4}$ + pattern: ^([2-9]\d{2}-\d{3}-\d{4})?$ example: 212-555-5555 x-nullable: true title: Alternate phone @@ -904,7 +904,7 @@ definitions: secondary_telephone: type: string format: telephone - pattern: ^[2-9]\d{2}-\d{3}-\d{4}$ + pattern: ^([2-9]\d{2}-\d{3}-\d{4})?$ example: 212-555-5555 x-nullable: true title: Alternate Phone diff --git a/swagger/prime.yaml b/swagger/prime.yaml index f7f587b8624..6da8e27e845 100644 --- a/swagger/prime.yaml +++ b/swagger/prime.yaml @@ -3126,6 +3126,7 @@ definitions: - StandaloneCrate - StandaloneCrateCap - UncappedRequestTotal + - LockedPriceCents ServiceItemParamType: type: string enum: diff --git a/swagger/prime_v2.yaml b/swagger/prime_v2.yaml index 7e1bf6c0acc..def5f5626c3 100644 --- a/swagger/prime_v2.yaml +++ b/swagger/prime_v2.yaml @@ -1711,6 +1711,7 @@ definitions: - StandaloneCrate - StandaloneCrateCap - UncappedRequestTotal + - LockedPriceCents ServiceItemParamType: type: string enum: diff --git a/swagger/prime_v3.yaml b/swagger/prime_v3.yaml index 6b59d77d897..d13c94b496f 100644 --- a/swagger/prime_v3.yaml +++ b/swagger/prime_v3.yaml @@ -864,6 +864,16 @@ definitions: goods exist. allOf: - $ref: '#/definitions/Address' + hasTertiaryPickupAddress: + type: boolean + x-omitempty: false + x-nullable: true + tertiaryPickupAddress: + description: > + An optional third pickup location near the origin where additional + goods exist. + allOf: + - $ref: '#/definitions/Address' destinationAddress: description: > The address of the destination location where goods are being @@ -880,6 +890,16 @@ definitions: dropped off. allOf: - $ref: '#/definitions/Address' + hasTertiaryDestinationAddress: + type: boolean + x-omitempty: false + x-nullable: true + tertiaryDestinationAddress: + description: > + An optional third address near the destination where goods will be + dropped off. + allOf: + - $ref: '#/definitions/Address' sitExpected: description: > Captures whether some or all of the PPM shipment will require @@ -1047,6 +1067,18 @@ definitions: one. An optional field. allOf: - $ref: '#/definitions/Address' + tertiaryPickupAddress: + description: >- + A third pickup address for this shipment, if the customer entered one. + An optional field. + allOf: + - $ref: '#/definitions/Address' + tertiaryDeliveryAddress: + description: >- + A third delivery address for this shipment, if the customer entered + one. An optional field. + allOf: + - $ref: '#/definitions/Address' storageFacility: allOf: - x-nullable: true @@ -1735,6 +1767,7 @@ definitions: - StandaloneCrate - StandaloneCrateCap - UncappedRequestTotal + - LockedPriceCents ServiceItemParamType: type: string enum: