diff --git a/go.mod b/go.mod index 70c906490f8..c2e5591e886 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/aws/aws-sdk-go-v2/credentials v1.17.32 github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.4.17 github.com/aws/aws-sdk-go-v2/service/cloudwatchevents v1.23.6 - github.com/aws/aws-sdk-go-v2/service/ecr v1.32.1 + github.com/aws/aws-sdk-go-v2/service/ecr v1.32.4 github.com/aws/aws-sdk-go-v2/service/ecs v1.44.3 github.com/aws/aws-sdk-go-v2/service/rds v1.78.2 github.com/aws/aws-sdk-go-v2/service/s3 v1.59.0 diff --git a/go.sum b/go.sum index d88c78b2c35..93e7ee0989e 100644 --- a/go.sum +++ b/go.sum @@ -64,8 +64,8 @@ github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.16 h1:mimdLQkIX1zr8GIPY1ZtALdBQGx github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.16/go.mod h1:YHk6owoSwrIsok+cAH9PENCOGoH5PU2EllX4vLtSrsY= github.com/aws/aws-sdk-go-v2/service/cloudwatchevents v1.23.6 h1:s9TaIFJJ1zVLiKoxvclHNXjcZ+9+JnHvVgWfQgN9srQ= github.com/aws/aws-sdk-go-v2/service/cloudwatchevents v1.23.6/go.mod h1:9h+vJwhl865wjyC7AQOBX4LVQxgntBYrf3p5WtzOSi0= -github.com/aws/aws-sdk-go-v2/service/ecr v1.32.1 h1:PxM8EHsv1sd9eWGamMQCvqBEjxytK5kAwjrxlfG3tac= -github.com/aws/aws-sdk-go-v2/service/ecr v1.32.1/go.mod h1:kdk+WJbHcGVbIlRQfSrKyuKkbWDdD8I9NScyS5vZ8eQ= +github.com/aws/aws-sdk-go-v2/service/ecr v1.32.4 h1:nQAU2Yr+afkAvIV39mg7LrNYFNQP7ShwbmiJqx2fUKA= +github.com/aws/aws-sdk-go-v2/service/ecr v1.32.4/go.mod h1:keOS9j4fv5ASh7dV29lIpGw2QgoJwGFAyMU0uPvfax4= github.com/aws/aws-sdk-go-v2/service/ecs v1.44.3 h1:JkVDQ9mfUSwMOGWIEmyB74mIznjKnHykJSq3uwusBBs= github.com/aws/aws-sdk-go-v2/service/ecs v1.44.3/go.mod h1:MsQWy/90Xwn3cy5u+eiiXqC521xIm21wOODIweLo4hs= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 h1:KypMCbLPPHEmf9DgMGw51jMj77VfGPAN2Kv4cfhlfgI= diff --git a/pkg/gen/ghcapi/configure_mymove.go b/pkg/gen/ghcapi/configure_mymove.go index 96f74c298b8..af39abd3a62 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 b049ef44fa2..bc87561910d 100644 --- a/pkg/gen/ghcapi/embedded_spec.go +++ b/pkg/gen/ghcapi/embedded_spec.go @@ -3325,6 +3325,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", @@ -18267,6 +18310,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", diff --git a/pkg/gen/ghcapi/ghcoperations/mymove_api.go b/pkg/gen/ghcapi/ghcoperations/mymove_api.go index ebac35ede1c..bec29e262f6 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") }), @@ -425,6 +428,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 @@ -713,6 +718,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") } @@ -1110,6 +1118,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/primev3api/embedded_spec.go b/pkg/gen/primev3api/embedded_spec.go index f568708fa66..6ea3004f846 100644 --- a/pkg/gen/primev3api/embedded_spec.go +++ b/pkg/gen/primev3api/embedded_spec.go @@ -1818,20 +1818,10 @@ func init() { "x-omitempty": false }, "secondaryDeliveryAddress": { - "description": "A second delivery address for this shipment, if the customer entered one. An optional field.", - "allOf": [ - { - "$ref": "#/definitions/Address" - } - ] + "$ref": "#/definitions/Address" }, "secondaryPickupAddress": { - "description": "A second pickup address for this shipment, if the customer entered one. An optional field.", - "allOf": [ - { - "$ref": "#/definitions/Address" - } - ] + "$ref": "#/definitions/Address" }, "shipmentType": { "$ref": "#/definitions/MTOShipmentType" @@ -1862,6 +1852,12 @@ func init() { } ] }, + "tertiaryDeliveryAddress": { + "$ref": "#/definitions/Address" + }, + "tertiaryPickupAddress": { + "$ref": "#/definitions/Address" + }, "updatedAt": { "type": "string", "format": "date-time", @@ -2209,6 +2205,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 + }, "id": { "description": "The primary unique identifier of this PPM shipment", "type": "string", @@ -2306,6 +2312,12 @@ func init() { "x-nullable": true, "x-omitempty": false }, + "tertiaryDestinationAddress": { + "$ref": "#/definitions/Address" + }, + "tertiaryPickupAddress": { + "$ref": "#/definitions/Address" + }, "updatedAt": { "description": "The timestamp of when a property of this object was last updated (UTC)", "type": "string", @@ -5415,20 +5427,10 @@ func init() { "x-omitempty": false }, "secondaryDeliveryAddress": { - "description": "A second delivery address for this shipment, if the customer entered one. An optional field.", - "allOf": [ - { - "$ref": "#/definitions/Address" - } - ] + "$ref": "#/definitions/Address" }, "secondaryPickupAddress": { - "description": "A second pickup address for this shipment, if the customer entered one. An optional field.", - "allOf": [ - { - "$ref": "#/definitions/Address" - } - ] + "$ref": "#/definitions/Address" }, "shipmentType": { "$ref": "#/definitions/MTOShipmentType" @@ -5459,6 +5461,12 @@ func init() { } ] }, + "tertiaryDeliveryAddress": { + "$ref": "#/definitions/Address" + }, + "tertiaryPickupAddress": { + "$ref": "#/definitions/Address" + }, "updatedAt": { "type": "string", "format": "date-time", @@ -5806,6 +5814,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 + }, "id": { "description": "The primary unique identifier of this PPM shipment", "type": "string", @@ -5903,6 +5921,12 @@ func init() { "x-nullable": true, "x-omitempty": false }, + "tertiaryDestinationAddress": { + "$ref": "#/definitions/Address" + }, + "tertiaryPickupAddress": { + "$ref": "#/definitions/Address" + }, "updatedAt": { "description": "The timestamp of when a property of this object was last updated (UTC)", "type": "string", diff --git a/pkg/gen/primev3messages/m_t_o_shipment_without_service_items.go b/pkg/gen/primev3messages/m_t_o_shipment_without_service_items.go index 442992cb427..6411c29497b 100644 --- a/pkg/gen/primev3messages/m_t_o_shipment_without_service_items.go +++ b/pkg/gen/primev3messages/m_t_o_shipment_without_service_items.go @@ -191,15 +191,11 @@ type MTOShipmentWithoutServiceItems struct { // Format: date ScheduledPickupDate *strfmt.Date `json:"scheduledPickupDate"` - // A second delivery address for this shipment, if the customer entered one. An optional field. - SecondaryDeliveryAddress struct { - Address - } `json:"secondaryDeliveryAddress,omitempty"` + // secondary delivery address + SecondaryDeliveryAddress *Address `json:"secondaryDeliveryAddress,omitempty"` - // A second pickup address for this shipment, if the customer entered one. An optional field. - SecondaryPickupAddress struct { - Address - } `json:"secondaryPickupAddress,omitempty"` + // secondary pickup address + SecondaryPickupAddress *Address `json:"secondaryPickupAddress,omitempty"` // shipment type ShipmentType MTOShipmentType `json:"shipmentType,omitempty"` @@ -216,6 +212,12 @@ type MTOShipmentWithoutServiceItems struct { // storage facility StorageFacility *StorageFacility `json:"storageFacility,omitempty"` + // tertiary delivery address + TertiaryDeliveryAddress *Address `json:"tertiaryDeliveryAddress,omitempty"` + + // tertiary pickup address + TertiaryPickupAddress *Address `json:"tertiaryPickupAddress,omitempty"` + // updated at // Read Only: true // Format: date-time @@ -350,6 +352,14 @@ func (m *MTOShipmentWithoutServiceItems) 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 err := m.validateUpdatedAt(formats); err != nil { res = append(res, err) } @@ -697,6 +707,17 @@ func (m *MTOShipmentWithoutServiceItems) validateSecondaryDeliveryAddress(format return nil } + if m.SecondaryDeliveryAddress != nil { + if err := m.SecondaryDeliveryAddress.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("secondaryDeliveryAddress") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("secondaryDeliveryAddress") + } + return err + } + } + return nil } @@ -705,6 +726,17 @@ func (m *MTOShipmentWithoutServiceItems) validateSecondaryPickupAddress(formats return nil } + if m.SecondaryPickupAddress != nil { + if err := m.SecondaryPickupAddress.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("secondaryPickupAddress") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("secondaryPickupAddress") + } + return err + } + } + return nil } @@ -815,6 +847,44 @@ func (m *MTOShipmentWithoutServiceItems) validateStorageFacility(formats strfmt. return nil } +func (m *MTOShipmentWithoutServiceItems) validateTertiaryDeliveryAddress(formats strfmt.Registry) error { + if swag.IsZero(m.TertiaryDeliveryAddress) { // not required + return nil + } + + if m.TertiaryDeliveryAddress != nil { + if err := m.TertiaryDeliveryAddress.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("tertiaryDeliveryAddress") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("tertiaryDeliveryAddress") + } + return err + } + } + + return nil +} + +func (m *MTOShipmentWithoutServiceItems) validateTertiaryPickupAddress(formats strfmt.Registry) error { + if swag.IsZero(m.TertiaryPickupAddress) { // not required + return nil + } + + if m.TertiaryPickupAddress != nil { + if err := m.TertiaryPickupAddress.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("tertiaryPickupAddress") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("tertiaryPickupAddress") + } + return err + } + } + + return nil +} + func (m *MTOShipmentWithoutServiceItems) validateUpdatedAt(formats strfmt.Registry) error { if swag.IsZero(m.UpdatedAt) { // not required return nil @@ -935,6 +1005,14 @@ func (m *MTOShipmentWithoutServiceItems) ContextValidate(ctx context.Context, fo 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 err := m.contextValidateUpdatedAt(ctx, formats); err != nil { res = append(res, err) } @@ -1184,11 +1262,43 @@ func (m *MTOShipmentWithoutServiceItems) contextValidateReweigh(ctx context.Cont func (m *MTOShipmentWithoutServiceItems) contextValidateSecondaryDeliveryAddress(ctx context.Context, formats strfmt.Registry) error { + if m.SecondaryDeliveryAddress != nil { + + if swag.IsZero(m.SecondaryDeliveryAddress) { // not required + return nil + } + + if err := m.SecondaryDeliveryAddress.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("secondaryDeliveryAddress") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("secondaryDeliveryAddress") + } + return err + } + } + return nil } func (m *MTOShipmentWithoutServiceItems) contextValidateSecondaryPickupAddress(ctx context.Context, formats strfmt.Registry) error { + if m.SecondaryPickupAddress != nil { + + if swag.IsZero(m.SecondaryPickupAddress) { // not required + return nil + } + + if err := m.SecondaryPickupAddress.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("secondaryPickupAddress") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("secondaryPickupAddress") + } + return err + } + } + return nil } @@ -1254,6 +1364,48 @@ func (m *MTOShipmentWithoutServiceItems) contextValidateStorageFacility(ctx cont return nil } +func (m *MTOShipmentWithoutServiceItems) contextValidateTertiaryDeliveryAddress(ctx context.Context, formats strfmt.Registry) error { + + if m.TertiaryDeliveryAddress != nil { + + if swag.IsZero(m.TertiaryDeliveryAddress) { // not required + return nil + } + + if err := m.TertiaryDeliveryAddress.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("tertiaryDeliveryAddress") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("tertiaryDeliveryAddress") + } + return err + } + } + + return nil +} + +func (m *MTOShipmentWithoutServiceItems) contextValidateTertiaryPickupAddress(ctx context.Context, formats strfmt.Registry) error { + + if m.TertiaryPickupAddress != nil { + + if swag.IsZero(m.TertiaryPickupAddress) { // not required + return nil + } + + if err := m.TertiaryPickupAddress.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("tertiaryPickupAddress") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("tertiaryPickupAddress") + } + return err + } + } + + return nil +} + func (m *MTOShipmentWithoutServiceItems) contextValidateUpdatedAt(ctx context.Context, formats strfmt.Registry) error { if err := validate.ReadOnly(ctx, "updatedAt", "body", strfmt.DateTime(m.UpdatedAt)); err != nil { diff --git a/pkg/gen/primev3messages/p_p_m_shipment.go b/pkg/gen/primev3messages/p_p_m_shipment.go index bb1acd53881..99a834a1303 100644 --- a/pkg/gen/primev3messages/p_p_m_shipment.go +++ b/pkg/gen/primev3messages/p_p_m_shipment.go @@ -97,6 +97,12 @@ type PPMShipment 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 primary unique identifier of this PPM shipment // Example: 1f2270c7-7166-40ae-981e-b200ebdf3054 // Required: true @@ -164,6 +170,12 @@ type PPMShipment struct { // Format: date-time SubmittedAt *strfmt.DateTime `json:"submittedAt"` + // tertiary destination address + TertiaryDestinationAddress *Address `json:"tertiaryDestinationAddress,omitempty"` + + // tertiary pickup address + TertiaryPickupAddress *Address `json:"tertiaryPickupAddress,omitempty"` + // The timestamp of when a property of this object was last updated (UTC) // Read Only: true // Format: date-time @@ -254,6 +266,14 @@ func (m *PPMShipment) 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 err := m.validateUpdatedAt(formats); err != nil { res = append(res, err) } @@ -545,6 +565,44 @@ func (m *PPMShipment) validateSubmittedAt(formats strfmt.Registry) error { return nil } +func (m *PPMShipment) validateTertiaryDestinationAddress(formats strfmt.Registry) error { + if swag.IsZero(m.TertiaryDestinationAddress) { // not required + return nil + } + + if m.TertiaryDestinationAddress != nil { + if err := m.TertiaryDestinationAddress.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("tertiaryDestinationAddress") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("tertiaryDestinationAddress") + } + return err + } + } + + return nil +} + +func (m *PPMShipment) validateTertiaryPickupAddress(formats strfmt.Registry) error { + if swag.IsZero(m.TertiaryPickupAddress) { // not required + return nil + } + + if m.TertiaryPickupAddress != nil { + if err := m.TertiaryPickupAddress.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("tertiaryPickupAddress") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("tertiaryPickupAddress") + } + return err + } + } + + return nil +} + func (m *PPMShipment) validateUpdatedAt(formats strfmt.Registry) error { if swag.IsZero(m.UpdatedAt) { // not required return nil @@ -601,6 +659,14 @@ func (m *PPMShipment) ContextValidate(ctx context.Context, formats strfmt.Regist 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 err := m.contextValidateUpdatedAt(ctx, formats); err != nil { res = append(res, err) } @@ -758,6 +824,48 @@ func (m *PPMShipment) contextValidateStatus(ctx context.Context, formats strfmt. return nil } +func (m *PPMShipment) contextValidateTertiaryDestinationAddress(ctx context.Context, formats strfmt.Registry) error { + + if m.TertiaryDestinationAddress != nil { + + if swag.IsZero(m.TertiaryDestinationAddress) { // not required + return nil + } + + if err := m.TertiaryDestinationAddress.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("tertiaryDestinationAddress") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("tertiaryDestinationAddress") + } + return err + } + } + + return nil +} + +func (m *PPMShipment) contextValidateTertiaryPickupAddress(ctx context.Context, formats strfmt.Registry) error { + + if m.TertiaryPickupAddress != nil { + + if swag.IsZero(m.TertiaryPickupAddress) { // not required + return nil + } + + if err := m.TertiaryPickupAddress.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("tertiaryPickupAddress") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("tertiaryPickupAddress") + } + return err + } + } + + return nil +} + func (m *PPMShipment) contextValidateUpdatedAt(ctx context.Context, formats strfmt.Registry) error { if err := validate.ReadOnly(ctx, "updatedAt", "body", strfmt.DateTime(m.UpdatedAt)); err != nil { diff --git a/pkg/handlers/ghcapi/api.go b/pkg/handlers/ghcapi/api.go index ccdc0147292..96167bbecfe 100644 --- a/pkg/handlers/ghcapi/api.go +++ b/pkg/handlers/ghcapi/api.go @@ -672,6 +672,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/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/primeapi/payment_request.go b/pkg/handlers/primeapi/payment_request.go index d66f831331b..7a0150e28c9 100644 --- a/pkg/handlers/primeapi/payment_request.go +++ b/pkg/handlers/primeapi/payment_request.go @@ -127,6 +127,14 @@ func (h CreatePaymentRequestHandler) Handle(params paymentrequestop.CreatePaymen zap.Any("payload", payload)) return paymentrequestop.NewCreatePaymentRequestBadRequest().WithPayload(payload), err default: + if e.Error() == "cannot have payment date earlier than or equal to SIT Entry date" { + payload := payloads.ClientError(handlers.ConflictErrMessage, err.Error(), h.GetTraceIDFromRequest(params.HTTPRequest)) + + appCtx.Logger().Error("Payment Request", + zap.Any("payload", payload)) + return paymentrequestop.NewCreatePaymentRequestConflict().WithPayload(payload), err + } + appCtx.Logger().Error("Payment Request", zap.Any("payload", payload)) return paymentrequestop.NewCreatePaymentRequestInternalServerError().WithPayload( diff --git a/pkg/handlers/primeapiv3/move_task_order_test.go b/pkg/handlers/primeapiv3/move_task_order_test.go index abcd48a9741..e0587fd4bbd 100644 --- a/pkg/handlers/primeapiv3/move_task_order_test.go +++ b/pkg/handlers/primeapiv3/move_task_order_test.go @@ -301,6 +301,8 @@ func (suite *HandlerSuite) TestGetMoveTaskOrder() { destinationType := models.DestinationTypeHomeOfRecord secondaryDeliveryAddress := factory.BuildAddress(suite.DB(), nil, nil) secondaryPickupAddress := factory.BuildAddress(suite.DB(), nil, nil) + tertiaryDeliveryAddress := factory.BuildAddress(suite.DB(), nil, nil) + tertiaryPickupAddress := factory.BuildAddress(suite.DB(), nil, nil) now := time.Now() nowDate := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC) yesterDate := nowDate.AddDate(0, 0, -1) @@ -331,6 +333,16 @@ func (suite *HandlerSuite) TestGetMoveTaskOrder() { Type: &factory.Addresses.SecondaryPickupAddress, LinkOnly: true, }, + { + Model: tertiaryDeliveryAddress, + Type: &factory.Addresses.TertiaryDeliveryAddress, + LinkOnly: true, + }, + { + Model: tertiaryPickupAddress, + Type: &factory.Addresses.TertiaryPickupAddress, + LinkOnly: true, + }, { Model: successMove, LinkOnly: true, @@ -389,9 +401,11 @@ func (suite *HandlerSuite) TestGetMoveTaskOrder() { suite.Equal(successShipment.ScheduledDeliveryDate.Format(time.RFC3339), handlers.FmtDatePtrToPop(shipment.ScheduledDeliveryDate).Format(time.RFC3339)) suite.Equal(successShipment.ScheduledPickupDate.Format(time.RFC3339), handlers.FmtDatePtrToPop(shipment.ScheduledPickupDate).Format(time.RFC3339)) - verifyAddressFields(successShipment.SecondaryDeliveryAddress, &shipment.SecondaryDeliveryAddress.Address) - verifyAddressFields(successShipment.SecondaryPickupAddress, &shipment.SecondaryPickupAddress.Address) + verifyAddressFields(successShipment.SecondaryDeliveryAddress, shipment.SecondaryDeliveryAddress) + verifyAddressFields(successShipment.SecondaryPickupAddress, shipment.SecondaryPickupAddress) + verifyAddressFields(successShipment.TertiaryDeliveryAddress, shipment.TertiaryDeliveryAddress) + verifyAddressFields(successShipment.TertiaryPickupAddress, shipment.TertiaryPickupAddress) suite.Equal(string(successShipment.ShipmentType), string(shipment.ShipmentType)) suite.Equal(string(successShipment.Status), shipment.Status) diff --git a/pkg/handlers/primeapiv3/payloads/model_to_payload.go b/pkg/handlers/primeapiv3/payloads/model_to_payload.go index 7827f6dfa93..e6488380943 100644 --- a/pkg/handlers/primeapiv3/payloads/model_to_payload.go +++ b/pkg/handlers/primeapiv3/payloads/model_to_payload.go @@ -520,6 +520,10 @@ func MTOShipmentWithoutServiceItems(mtoShipment *models.MTOShipment) *primev3mes ETag: etag.GenerateEtag(mtoShipment.UpdatedAt), OriginSitAuthEndDate: (*strfmt.Date)(mtoShipment.OriginSITAuthEndDate), DestinationSitAuthEndDate: (*strfmt.Date)(mtoShipment.DestinationSITAuthEndDate), + SecondaryDeliveryAddress: Address(mtoShipment.SecondaryDeliveryAddress), + SecondaryPickupAddress: Address(mtoShipment.SecondaryPickupAddress), + TertiaryDeliveryAddress: Address(mtoShipment.TertiaryDeliveryAddress), + TertiaryPickupAddress: Address(mtoShipment.TertiaryPickupAddress), } // Set up address payloads @@ -533,12 +537,6 @@ func MTOShipmentWithoutServiceItems(mtoShipment *models.MTOShipment) *primev3mes destinationType := primev3messages.DestinationType(*mtoShipment.DestinationType) payload.DestinationType = &destinationType } - if mtoShipment.SecondaryPickupAddress != nil { - payload.SecondaryPickupAddress.Address = *Address(mtoShipment.SecondaryPickupAddress) - } - if mtoShipment.SecondaryDeliveryAddress != nil { - payload.SecondaryDeliveryAddress.Address = *Address(mtoShipment.SecondaryDeliveryAddress) - } if mtoShipment.StorageFacility != nil { payload.StorageFacility = StorageFacility(mtoShipment.StorageFacility) @@ -563,14 +561,22 @@ func MTOShipmentWithoutServiceItems(mtoShipment *models.MTOShipment) *primev3mes if mtoShipment.PPMShipment.SecondaryPickupAddress != nil { payload.PpmShipment.SecondaryPickupAddress = Address(mtoShipment.PPMShipment.SecondaryPickupAddress) } + if mtoShipment.PPMShipment.TertiaryPickupAddress != nil { + payload.PpmShipment.TertiaryPickupAddress = Address(mtoShipment.PPMShipment.TertiaryPickupAddress) + } if mtoShipment.PPMShipment.DestinationAddress != nil { payload.PpmShipment.DestinationAddress = Address(mtoShipment.PPMShipment.DestinationAddress) } if mtoShipment.PPMShipment.SecondaryDestinationAddress != nil { payload.PpmShipment.SecondaryDestinationAddress = Address(mtoShipment.PPMShipment.SecondaryDestinationAddress) } + if mtoShipment.PPMShipment.TertiaryDestinationAddress != nil { + payload.PpmShipment.TertiaryDestinationAddress = Address(mtoShipment.PPMShipment.TertiaryDestinationAddress) + } payload.PpmShipment.HasSecondaryPickupAddress = mtoShipment.PPMShipment.HasSecondaryPickupAddress payload.PpmShipment.HasSecondaryDestinationAddress = mtoShipment.PPMShipment.HasSecondaryDestinationAddress + payload.PpmShipment.HasTertiaryPickupAddress = mtoShipment.PPMShipment.HasTertiaryPickupAddress + payload.PpmShipment.HasTertiaryDestinationAddress = mtoShipment.PPMShipment.HasTertiaryDestinationAddress } return payload 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 8b0eddea933..775c3207696 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/services/move/move_router.go b/pkg/services/move/move_router.go index 9b3ccc44d49..9894a841df4 100644 --- a/pkg/services/move/move_router.go +++ b/pkg/services/move/move_router.go @@ -207,8 +207,10 @@ func (router moveRouter) sendToServiceCounselor(appCtx appcontext.AppContext, mo return apperror.NewInvalidInputError(move.MTOShipments[i].PPMShipment.ID, err, verrs, msg) } } - // update status for boat shipment - if move.MTOShipments[i].ShipmentType == models.MTOShipmentTypeBoatHaulAway || move.MTOShipments[i].ShipmentType == models.MTOShipmentTypeBoatTowAway { + // update status for boat or mobile home shipment + if move.MTOShipments[i].ShipmentType == models.MTOShipmentTypeBoatHaulAway || + move.MTOShipments[i].ShipmentType == models.MTOShipmentTypeBoatTowAway || + move.MTOShipments[i].ShipmentType == models.MTOShipmentTypeMobileHome { move.MTOShipments[i].Status = models.MTOShipmentStatusSubmitted if verrs, err := appCtx.DB().ValidateAndUpdate(&move.MTOShipments[i]); verrs.HasAny() || err != nil { diff --git a/pkg/services/move_task_order/move_task_order_fetcher.go b/pkg/services/move_task_order/move_task_order_fetcher.go index ad37708f4ce..49ec5273db9 100644 --- a/pkg/services/move_task_order/move_task_order_fetcher.go +++ b/pkg/services/move_task_order/move_task_order_fetcher.go @@ -135,6 +135,8 @@ func (f moveTaskOrderFetcher) FetchMoveTaskOrder(appCtx appcontext.AppContext, s "MTOShipments.PickupAddress", "MTOShipments.SecondaryDeliveryAddress", "MTOShipments.SecondaryPickupAddress", + "MTOShipments.TertiaryDeliveryAddress", + "MTOShipments.TertiaryPickupAddress", "MTOShipments.MTOAgents", "MTOShipments.SITDurationUpdates", "MTOShipments.StorageFacility", diff --git a/pkg/services/move_task_order/move_task_order_fetcher_test.go b/pkg/services/move_task_order/move_task_order_fetcher_test.go index 1c32f1145c1..fe0562db685 100644 --- a/pkg/services/move_task_order/move_task_order_fetcher_test.go +++ b/pkg/services/move_task_order/move_task_order_fetcher_test.go @@ -128,6 +128,48 @@ func (suite *MoveTaskOrderServiceSuite) TestMoveTaskOrderFetcher() { suite.Equal(expectedAddressUpdate.OriginalAddress.Country, actualAddressUpdate.OriginalAddress.Country) }) + suite.Run("Success with fetching a MTO with a Shipment Address Update that has a customized Original Address and three addresses", func() { + traits := []factory.Trait{factory.GetTraitShipmentAddressUpdateApproved} + + expectedAddressUpdate := factory.BuildShipmentAddressUpdate(suite.DB(), []factory.Customization{ + { + Model: models.Address{ + StreetAddress1: "123 Main St", + StreetAddress2: models.StringPointer("Apt 2"), + StreetAddress3: models.StringPointer("Suite 200"), + City: "New York", + State: "NY", + PostalCode: "10001", + Country: models.StringPointer("US"), + }, + Type: &factory.Addresses.OriginalAddress, + }, + }, traits) + + searchParams := services.MoveTaskOrderFetcherParams{ + IncludeHidden: false, + IsAvailableToPrime: true, + MoveTaskOrderID: expectedAddressUpdate.Shipment.MoveTaskOrder.ID, + } + + actualMTO, err := mtoFetcher.FetchMoveTaskOrder(suite.AppContextForTest(), &searchParams) + suite.NoError(err) + + actualAddressUpdate := actualMTO.MTOShipments[0].DeliveryAddressUpdate + + // Validate MTO was fetched that includes expected shipment address update with customized original address + suite.Equal(expectedAddressUpdate.ShipmentID, actualAddressUpdate.ShipmentID) + suite.Equal(expectedAddressUpdate.Status, actualAddressUpdate.Status) + suite.ElementsMatch(expectedAddressUpdate.OriginalAddressID, actualAddressUpdate.OriginalAddressID) + suite.Equal(expectedAddressUpdate.OriginalAddress.StreetAddress1, actualAddressUpdate.OriginalAddress.StreetAddress1) + suite.Equal(expectedAddressUpdate.OriginalAddress.StreetAddress2, actualAddressUpdate.OriginalAddress.StreetAddress2) + suite.Equal(expectedAddressUpdate.OriginalAddress.StreetAddress3, actualAddressUpdate.OriginalAddress.StreetAddress3) + suite.Equal(expectedAddressUpdate.OriginalAddress.City, actualAddressUpdate.OriginalAddress.City) + suite.Equal(expectedAddressUpdate.OriginalAddress.State, actualAddressUpdate.OriginalAddress.State) + suite.Equal(expectedAddressUpdate.OriginalAddress.PostalCode, actualAddressUpdate.OriginalAddress.PostalCode) + suite.Equal(expectedAddressUpdate.OriginalAddress.Country, actualAddressUpdate.OriginalAddress.Country) + }) + suite.Run("Success with Prime-available move by ID, fetch all non-deleted shipments", func() { expectedMTO, _ := setupTestData() searchParams := services.MoveTaskOrderFetcherParams{ 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_creator.go b/pkg/services/payment_request/payment_request_creator.go index 648f911ae1f..0301e8dd3d6 100644 --- a/pkg/services/payment_request/payment_request_creator.go +++ b/pkg/services/payment_request/payment_request_creator.go @@ -150,9 +150,8 @@ func (p *paymentRequestCreator) CreatePaymentRequest(appCtx appcontext.AppContex return apperror.NewBadDataError(errStr) } - if paymentServiceItem.MTOServiceItem.SITEntryDate != nil && paymentServiceItem.MTOServiceItem.SITEntryDate.After(paymentDate) { + if paymentServiceItem.MTOServiceItem.SITEntryDate != nil && !paymentDate.After(*paymentServiceItem.MTOServiceItem.SITEntryDate) { return apperror.NewConflictError(paymentRequestArg.ID, "cannot have payment date earlier than or equal to SIT Entry date") - } } diff --git a/pkg/services/shipment_summary_worksheet/shipment_summary_worksheet.go b/pkg/services/shipment_summary_worksheet/shipment_summary_worksheet.go index 3b259dd96ab..7d3dd2c421c 100644 --- a/pkg/services/shipment_summary_worksheet/shipment_summary_worksheet.go +++ b/pkg/services/shipment_summary_worksheet/shipment_summary_worksheet.go @@ -867,15 +867,21 @@ func FormatSITDaysInStorage(entryDate *time.Time, departureDate *time.Time) stri func formatDisbursement(expensesMap map[string]float64, ppmRemainingEntitlement float64) string { disbursementGTCC := expensesMap["TotalGTCCPaid"] + expensesMap["StorageGTCCPaid"] disbursementGTCCB := ppmRemainingEntitlement + expensesMap["StorageMemberPaid"] + var disbursementMember float64 // Disbursement GTCC is the lowest value of the above 2 calculations if disbursementGTCCB < disbursementGTCC { disbursementGTCC = disbursementGTCCB } - // Disbursement Member is remaining entitlement plus member SIT minus GTCC Disbursement, not less than 0. - disbursementMember := ppmRemainingEntitlement + expensesMap["StorageMemberPaid"] - disbursementGTCC - if disbursementMember < 0 { - disbursementMember = 0 + if disbursementGTCC < 0 { + // The only way this can happen is if the member overdrafted on their advance, resulting in negative GTCCB. In this case, the + // disbursement member value will be liable for the negative difference, meaning they owe this money to the govt. + disbursementMember = disbursementGTCC + disbursementGTCC = 0 + } else { + // Disbursement Member is remaining entitlement plus member SIT minus GTCC Disbursement, not less than 0. + disbursementMember = ppmRemainingEntitlement + expensesMap["StorageMemberPaid"] - disbursementGTCC } + // Return formatted values in string disbursementString := "GTCC: " + FormatDollars(disbursementGTCC) + "\nMember: " + FormatDollars(disbursementMember) return disbursementString diff --git a/pkg/services/shipment_summary_worksheet/shipment_summary_worksheet_test.go b/pkg/services/shipment_summary_worksheet/shipment_summary_worksheet_test.go index f5f1e0d184e..9b48e8273e2 100644 --- a/pkg/services/shipment_summary_worksheet/shipment_summary_worksheet_test.go +++ b/pkg/services/shipment_summary_worksheet/shipment_summary_worksheet_test.go @@ -1414,6 +1414,17 @@ func (suite *ShipmentSummaryWorksheetServiceSuite) TestFormatDisbursement() { expensesMap["StorageMemberPaid"] = 200.00 result = formatDisbursement(expensesMap, ppmRemainingEntitlement) suite.Equal(result, expectedResult) + + // Test case 3: GTCC calculation is less than 0 + expectedResult = "GTCC: " + FormatDollars(0) + "\nMember: " + FormatDollars(-250.00) + expensesMap = make(map[string]float64) + expensesMap["TotalGTCCPaid"] = 0 + expensesMap["StorageGTCCPaid"] = 0 + ppmRemainingEntitlement = -300.00 + expensesMap["StorageMemberPaid"] = 50.00 + result = formatDisbursement(expensesMap, ppmRemainingEntitlement) + suite.Equal(result, expectedResult) + } func (suite *ShipmentSummaryWorksheetServiceSuite) TestFormatAdditionalShipments() { diff --git a/pkg/testdatagen/scenario/shared.go b/pkg/testdatagen/scenario/shared.go index 6c96de1c028..6e8a2fcc5ef 100644 --- a/pkg/testdatagen/scenario/shared.go +++ b/pkg/testdatagen/scenario/shared.go @@ -5214,24 +5214,30 @@ func createHHGWithPaymentServiceItems( // An origin and destination SIT would normally not be on the same payment request so the TIO totals will appear // off. Refer to the PARSIT move to see a reviewed and pending payment request with origin and destination SIT. + doaPaymentStartDate := originEntryDate.Add(15 * 24 * time.Hour) + doaPaymentEndDate := originDepartureDate.Add(15 * 24 * time.Hour) + + ddaPaymentStartDate := destEntryDate.Add(15 * 24 * time.Hour) + daaPaymentEndDate := destDepartureDate.Add(15 * 24 * time.Hour) + doasitPaymentParams := []models.PaymentServiceItemParam{ { IncomingKey: models.ServiceItemParamNameSITPaymentRequestStart.String(), - Value: originEntryDate.Format("2006-01-02"), + Value: doaPaymentStartDate.Format("2006-01-02"), }, { IncomingKey: models.ServiceItemParamNameSITPaymentRequestEnd.String(), - Value: originDepartureDate.Format("2006-01-02"), + Value: doaPaymentEndDate.Format("2006-01-02"), }} ddasitPaymentParams := []models.PaymentServiceItemParam{ { IncomingKey: models.ServiceItemParamNameSITPaymentRequestStart.String(), - Value: destEntryDate.Format("2006-01-02"), + Value: ddaPaymentStartDate.Format("2006-01-02"), }, { IncomingKey: models.ServiceItemParamNameSITPaymentRequestEnd.String(), - Value: destDepartureDate.Format("2006-01-02"), + Value: daaPaymentEndDate.Format("2006-01-02"), }} // Ordering the service items based on approved date to ensure the DDFSIT is after the DOASIT. diff --git a/playwright/tests/my/mymove/mobileHome.spec.js b/playwright/tests/my/mymove/mobileHome.spec.js new file mode 100644 index 00000000000..78200820323 --- /dev/null +++ b/playwright/tests/my/mymove/mobileHome.spec.js @@ -0,0 +1,142 @@ +// @ts-check +import { test, expect } from '../../utils/my/customerTest'; + +const multiMoveEnabled = process.env.FEATURE_FLAG_MULTI_MOVE; + +test.describe('Mobile Home shipment', () => { + test.skip(multiMoveEnabled === 'true', 'Skip if MultiMove workflow is enabled.'); + + test('A customer can create a Mobile Home shipment', async ({ page, customerPage }) => { + // Generate a new onboarded user with orders and log in + const move = await customerPage.testHarness.buildMoveWithOrders(); + const userId = move.Orders.ServiceMember.user_id; + await customerPage.signInAsExistingCustomer(userId); + + // Navigate to create a new shipment + await customerPage.waitForPage.home(); + await page.getByTestId('shipment-selection-btn').click(); + await customerPage.waitForPage.aboutShipments(); + await customerPage.navigateForward(); + await customerPage.waitForPage.selectShipmentType(); + + // Create an Mobile Home shipment + await page.getByText('Move a Mobile Home').click(); + await customerPage.navigateForward(); + + // Fill in form to create Mobile Home shipment + await customerPage.waitForPage.mobileHomeShipment(); + await page.getByLabel('Year').fill('2022'); + await page.getByLabel('Make').fill('make'); + await page.getByLabel('Model').fill('model'); + await page.getByTestId('lengthFeet').fill('22'); + await page.getByTestId('widthFeet').fill('22'); + await page.getByTestId('heightFeet').fill('22'); + await page.getByRole('button', { name: 'Continue' }).click(); + + await expect(page.getByTestId('tag')).toContainText('Mobile Home'); + + await expect(page.getByText('Pickup info')).toBeVisible(); + await page.getByLabel('Preferred pickup date').fill('25 Dec 2022'); + await page.getByLabel('Preferred pickup date').blur(); + await page.getByText('Use my current address').click(); + await page.getByLabel('Preferred delivery date').fill('25 Dec 2022'); + await page.getByLabel('Preferred delivery date').blur(); + await page.getByRole('button', { name: 'Save & Continue' }).click(); + await customerPage.waitForPage.reviewShipments(); + }); +}); + +test.describe('(MultiMove) Mobile Home shipment', () => { + test.skip(multiMoveEnabled === 'false', 'Skip if MultiMove workflow is not enabled.'); + + test('A customer can create a Mobile Home shipment', async ({ page, customerPage }) => { + // Generate a new onboarded user with orders and log in + const move = await customerPage.testHarness.buildMoveWithOrders(); + const userId = move.Orders.ServiceMember.user_id; + await customerPage.signInAsExistingCustomer(userId); + + // Navigate from MM Dashboard to Move + await customerPage.navigateFromMMDashboardToMove(move); + + // Navigate to create a new shipment + await customerPage.waitForPage.home(); + await page.getByTestId('shipment-selection-btn').click(); + await customerPage.waitForPage.aboutShipments(); + await customerPage.navigateForward(); + await customerPage.waitForPage.selectShipmentType(); + + // Create an Mobile Home shipment + await page.getByText('Move a mobile home').click(); + await customerPage.navigateForward(); + + // Fill in form to create Mobile Home shipment + await customerPage.waitForPage.mobileHomeShipment(); + await page.getByLabel('Year').fill('2022'); + await page.getByLabel('Make').fill('make'); + await page.getByLabel('Model').fill('model'); + await page.getByTestId('lengthFeet').fill('22'); + await page.getByTestId('widthFeet').fill('22'); + await page.getByTestId('heightFeet').fill('22'); + await page.getByRole('button', { name: 'Continue' }).click(); + + await expect(page.getByTestId('tag')).toContainText('Mobile Home'); + + await expect(page.getByText('Pickup info')).toBeVisible(); + await page.getByLabel('Preferred pickup date').fill('25 Dec 2022'); + await page.getByLabel('Preferred pickup date').blur(); + await page.getByText('Use my current address').click(); + await page.getByLabel('Preferred delivery date').fill('25 Dec 2022'); + await page.getByLabel('Preferred delivery date').blur(); + await page.getByRole('button', { name: 'Save & Continue' }).click(); + await customerPage.waitForPage.reviewShipments(); + }); + + test('Is able to delete a Mobile Home shipment', async ({ page, customerPage }) => { + // Generate a new onboarded user with orders and log in + const move = await customerPage.testHarness.buildMoveWithOrders(); + const userId = move.Orders.ServiceMember.user_id; + await customerPage.signInAsExistingCustomer(userId); + + // Navigate from MM Dashboard to Move + await customerPage.navigateFromMMDashboardToMove(move); + + // Navigate to create a new shipment + await customerPage.waitForPage.home(); + await page.getByTestId('shipment-selection-btn').click(); + await customerPage.waitForPage.aboutShipments(); + await customerPage.navigateForward(); + await customerPage.waitForPage.selectShipmentType(); + + // Create an Mobile Home shipment + await page.getByText('Move a mobile home').click(); + await customerPage.navigateForward(); + + // Fill in form to create Mobile Home shipment + await customerPage.waitForPage.mobileHomeShipment(); + await page.getByLabel('Year').fill('2022'); + await page.getByLabel('Make').fill('make'); + await page.getByLabel('Model').fill('model'); + await page.getByTestId('lengthFeet').fill('22'); + await page.getByTestId('widthFeet').fill('22'); + await page.getByTestId('heightFeet').fill('22'); + await page.getByRole('button', { name: 'Continue' }).click(); + + await expect(page.getByTestId('tag')).toContainText('Mobile Home'); + + await expect(page.getByText('Pickup info')).toBeVisible(); + await page.getByLabel('Preferred pickup date').fill('25 Dec 2022'); + await page.getByLabel('Preferred pickup date').blur(); + await page.getByText('Use my current address').click(); + await page.getByLabel('Preferred delivery date').fill('25 Dec 2022'); + await page.getByLabel('Preferred delivery date').blur(); + await page.getByRole('button', { name: 'Save & Continue' }).click(); + await customerPage.waitForPage.reviewShipments(); + + await expect(page.getByRole('heading', { name: 'Mobile Home 1' })).toBeVisible(); + await page.getByTestId('deleteShipmentButton').click(); + await expect(page.getByRole('heading', { name: 'Delete this?' })).toBeVisible(); + await page.getByText('Yes, Delete').click(); + + await expect(page.getByRole('heading', { name: 'Mobile Home 1' })).not.toBeVisible(); + }); +}); diff --git a/playwright/tests/utils/my/waitForCustomerPage.js b/playwright/tests/utils/my/waitForCustomerPage.js index 18d76ac5d95..20fa227d9b0 100644 --- a/playwright/tests/utils/my/waitForCustomerPage.js +++ b/playwright/tests/utils/my/waitForCustomerPage.js @@ -193,6 +193,15 @@ export class WaitForCustomerPage extends WaitForPage { await this.runAccessibilityAudit(); } + /** + * @returns {Promise} + */ + async mobileHomeShipment() { + await this.runAccessibilityAudit(); + await base.expect(this.page.getByRole('heading', { level: 1 })).toHaveText('Mobile Home details and measurements'); + await this.runAccessibilityAudit(); + } + /** * @returns {Promise} */ diff --git a/src/components/Customer/MobileHomeShipment/MobileHomeShipmentForm/MobileHomeShipmentForm.jsx b/src/components/Customer/MobileHomeShipment/MobileHomeShipmentForm/MobileHomeShipmentForm.jsx index 4972d74b387..51364fc0d3a 100644 --- a/src/components/Customer/MobileHomeShipment/MobileHomeShipmentForm/MobileHomeShipmentForm.jsx +++ b/src/components/Customer/MobileHomeShipment/MobileHomeShipmentForm/MobileHomeShipmentForm.jsx @@ -110,7 +110,7 @@ const MobileHomeShipmentForm = ({ mtoShipment, onBack, onSubmit }) => {
-

Mobile home Information

+

Mobile Home Information

{

Mobile Home Dimensions

-

Enter all of the dimensions of the mobile home.

+

Enter the total outside dimensions (in Feet and Inches) of the Mobile Home.

@@ -268,14 +268,12 @@ const MobileHomeShipmentForm = ({ mtoShipment, onBack, onSubmit }) => { - Examples + Example
  • - Dimensions of the mobile home on the trailer are significantly different than one would expect - given their individual dimensions + Is there additional information you feel is pertinent to the processing of your mobile home + shipment?(e.g., ‘wrecker service requested’ and ‘crane service needed’).
  • - -
  • Access info for your origin or destination address/marina
diff --git a/src/components/Customer/MtoShipmentForm/MtoShipmentForm.jsx b/src/components/Customer/MtoShipmentForm/MtoShipmentForm.jsx index 20c62106ef0..efb155918d2 100644 --- a/src/components/Customer/MtoShipmentForm/MtoShipmentForm.jsx +++ b/src/components/Customer/MtoShipmentForm/MtoShipmentForm.jsx @@ -628,7 +628,7 @@ class MtoShipmentForm extends Component { )} - {!isBoat && ( + {!isBoat && !isMobileHome && (
Remarks
}>
); @@ -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/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/PrimeUI/Shipment/PrimeUIShipmentUpdate.jsx b/src/pages/PrimeUI/Shipment/PrimeUIShipmentUpdate.jsx index e581487702c..699700e7ee8 100644 --- a/src/pages/PrimeUI/Shipment/PrimeUIShipmentUpdate.jsx +++ b/src/pages/PrimeUI/Shipment/PrimeUIShipmentUpdate.jsx @@ -122,7 +122,11 @@ const PrimeUIShipmentUpdate = ({ setFlashMessage }) => { const editableProGearWeightActualField = true; const editableSpouseProGearWeightActualField = true; const reformatPrimeApiPickupAddress = fromPrimeAPIAddressFormat(shipment.pickupAddress); + const reformatPrimeApiSecondaryPickupAddress = fromPrimeAPIAddressFormat(shipment.secondaryPickupAddress); + const reformatPrimeApiTertiaryPickupAddress = fromPrimeAPIAddressFormat(shipment.tertiaryPickupAddress); const reformatPrimeApiDestinationAddress = fromPrimeAPIAddressFormat(shipment.destinationAddress); + const reformatPrimeApiSecondaryDeliveryAddress = fromPrimeAPIAddressFormat(shipment.secondaryDeliveryAddress); + const reformatPrimeApiTertiaryDeliveryAddress = fromPrimeAPIAddressFormat(shipment.tertiaryDeliveryAddress); const editablePickupAddress = isEmpty(reformatPrimeApiPickupAddress); const editableDestinationAddress = isEmpty(reformatPrimeApiDestinationAddress); @@ -309,7 +313,11 @@ const PrimeUIShipmentUpdate = ({ setFlashMessage }) => { scheduledDeliveryDate: shipment.scheduledDeliveryDate, actualDeliveryDate: shipment.actualDeliveryDate, pickupAddress: editablePickupAddress ? emptyAddress : reformatPrimeApiPickupAddress, + secondaryPickupAddress: reformatPrimeApiSecondaryPickupAddress, + tertiaryPickupAddress: reformatPrimeApiTertiaryPickupAddress, destinationAddress: editableDestinationAddress ? emptyAddress : reformatPrimeApiDestinationAddress, + secondaryDeliveryAddress: reformatPrimeApiSecondaryDeliveryAddress, + tertiaryDeliveryAddress: reformatPrimeApiTertiaryDeliveryAddress, destinationType: shipment.destinationType, diversion: shipment.diversion, }; @@ -364,7 +372,11 @@ const PrimeUIShipmentUpdate = ({ setFlashMessage }) => { actualSpouseProGearWeight={initialValues.actualSpouseProGearWeight} requestedPickupDate={initialValues.requestedPickupDate} pickupAddress={initialValues.pickupAddress} + secondaryPickupAddress={initialValues.secondaryPickupAddress} + tertiaryPickupAddress={initialValues.tertiaryPickupAddress} destinationAddress={initialValues.destinationAddress} + secondaryDeliveryAddress={initialValues.secondaryDeliveryAddress} + tertiaryDeliveryAddress={initialValues.tertiaryDeliveryAddress} diversion={initialValues.diversion} /> )} diff --git a/src/pages/PrimeUI/Shipment/PrimeUIShipmentUpdateForm.jsx b/src/pages/PrimeUI/Shipment/PrimeUIShipmentUpdateForm.jsx index 0c71eb62549..50d8bdb2a57 100644 --- a/src/pages/PrimeUI/Shipment/PrimeUIShipmentUpdateForm.jsx +++ b/src/pages/PrimeUI/Shipment/PrimeUIShipmentUpdateForm.jsx @@ -35,7 +35,11 @@ const PrimeUIShipmentUpdateForm = ({ actualProGearWeight, actualSpouseProGearWeight, pickupAddress, + secondaryPickupAddress, + tertiaryPickupAddress, destinationAddress, + secondaryDeliveryAddress, + tertiaryDeliveryAddress, }) => { return ( @@ -142,9 +146,17 @@ const PrimeUIShipmentUpdateForm = ({
Pickup Address
{editablePickupAddress && } {!editablePickupAddress && formatAddress(pickupAddress)} +
Second Pickup Address
+ {formatAddress(secondaryPickupAddress)} +
Third Pickup Address
+ {formatAddress(tertiaryPickupAddress)}
Destination Address
{editableDestinationAddress && } {!editableDestinationAddress && formatAddress(destinationAddress)} +
Second Destination Address
+ {formatAddress(secondaryDeliveryAddress)} +
Third Destination Address
+ {formatAddress(tertiaryDeliveryAddress)} The actual postal code where the PPM shipment started. To be filled once the customer has moved the shipment. @@ -80,6 +86,12 @@ properties: type: boolean x-omitempty: false x-nullable: true + tertiaryDestinationAddress: + $ref: '../../Address.yaml' + hasTertiaryDestinationAddress: + type: boolean + x-omitempty: false + x-nullable: true actualDestinationPostalCode: description: > The actual postal code where the PPM shipment ended. To be filled once the customer has moved the shipment. diff --git a/swagger-def/ghc.yaml b/swagger-def/ghc.yaml index fa17b7af928..b3536c4d712 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 diff --git a/swagger/ghc.yaml b/swagger/ghc.yaml index 45c41820126..1517f2c9abc 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 diff --git a/swagger/prime_v3.yaml b/swagger/prime_v3.yaml index a99d6c03207..41ae3c1afb2 100644 --- a/swagger/prime_v3.yaml +++ b/swagger/prime_v3.yaml @@ -2364,6 +2364,12 @@ definitions: type: boolean x-omitempty: false x-nullable: true + tertiaryPickupAddress: + $ref: '#/definitions/Address' + hasTertiaryPickupAddress: + type: boolean + x-omitempty: false + x-nullable: true actualPickupPostalCode: description: > The actual postal code where the PPM shipment started. To be filled @@ -2383,6 +2389,12 @@ definitions: type: boolean x-omitempty: false x-nullable: true + tertiaryDestinationAddress: + $ref: '#/definitions/Address' + hasTertiaryDestinationAddress: + type: boolean + x-omitempty: false + x-nullable: true actualDestinationPostalCode: description: > The actual postal code where the PPM shipment ended. To be filled once @@ -2860,17 +2872,13 @@ definitions: destinationType: $ref: '#/definitions/DestinationType' secondaryPickupAddress: - description: >- - A second pickup address for this shipment, if the customer entered - one. An optional field. - allOf: - - $ref: '#/definitions/Address' + $ref: '#/definitions/Address' secondaryDeliveryAddress: - description: >- - A second delivery address for this shipment, if the customer entered - one. An optional field. - allOf: - - $ref: '#/definitions/Address' + $ref: '#/definitions/Address' + tertiaryPickupAddress: + $ref: '#/definitions/Address' + tertiaryDeliveryAddress: + $ref: '#/definitions/Address' storageFacility: allOf: - x-nullable: true