diff --git a/.envrc b/.envrc index d0fb8d2781d..cf126f87261 100644 --- a/.envrc +++ b/.envrc @@ -144,6 +144,7 @@ export DB_SSL_MODE=disable # Multi Move feature flag export FEATURE_FLAG_MULTI_MOVE=true export FEATURE_FLAG_COUNSELOR_MOVE_CREATE=true +export FEATURE_FLAG_CUSTOMER_REGISTRATION=false export FEATURE_FLAG_MOVE_LOCK=false export FEATURE_FLAG_OKTA_DODID_INPUT=false diff --git a/config/env/demo.app-client-tls.env b/config/env/demo.app-client-tls.env index 5989ccfd533..d7cfefc7794 100644 --- a/config/env/demo.app-client-tls.env +++ b/config/env/demo.app-client-tls.env @@ -52,3 +52,4 @@ FEATURE_FLAG_DODID_UNIQUE=false FEATURE_FLAG_ENABLE_ALASKA=false FEATURE_FLAG_ENABLE_HAWAII=false FEATURE_FLAG_BULK_ASSIGNMENT=false +FEATURE_FLAG_CUSTOMER_REGISTRATION=false diff --git a/config/env/demo.app.env b/config/env/demo.app.env index 2621c72fb7a..22445b891f6 100644 --- a/config/env/demo.app.env +++ b/config/env/demo.app.env @@ -57,3 +57,4 @@ FEATURE_FLAG_DODID_UNIQUE=false FEATURE_FLAG_ENABLE_ALASKA=false FEATURE_FLAG_ENABLE_HAWAII=false FEATURE_FLAG_BULK_ASSIGNMENT=false +FEATURE_FLAG_CUSTOMER_REGISTRATION=false diff --git a/config/env/exp.app-client-tls.env b/config/env/exp.app-client-tls.env index 706d0d7d7eb..6d7367ae654 100644 --- a/config/env/exp.app-client-tls.env +++ b/config/env/exp.app-client-tls.env @@ -51,4 +51,5 @@ FEATURE_FLAG_QUEUE_MANAGEMENT=false FEATURE_FLAG_DODID_UNIQUE=false FEATURE_FLAG_ENABLE_ALASKA=false FEATURE_FLAG_ENABLE_HAWAII=false -FEATURE_FLAG_BULK_ASSIGNMENT=false \ No newline at end of file +FEATURE_FLAG_BULK_ASSIGNMENT=false +FEATURE_FLAG_CUSTOMER_REGISTRATION=false \ No newline at end of file diff --git a/config/env/exp.app.env b/config/env/exp.app.env index c425328db35..21718e30fe3 100644 --- a/config/env/exp.app.env +++ b/config/env/exp.app.env @@ -56,4 +56,5 @@ FEATURE_FLAG_QUEUE_MANAGEMENT=false FEATURE_FLAG_DODID_UNIQUE=false FEATURE_FLAG_ENABLE_ALASKA=false FEATURE_FLAG_ENABLE_HAWAII=false -FEATURE_FLAG_BULK_ASSIGNMENT=false \ No newline at end of file +FEATURE_FLAG_BULK_ASSIGNMENT=false +FEATURE_FLAG_CUSTOMER_REGISTRATION=false \ No newline at end of file diff --git a/config/env/loadtest.app-client-tls.env b/config/env/loadtest.app-client-tls.env index 7f6b5d92528..bdde5c7d1b6 100644 --- a/config/env/loadtest.app-client-tls.env +++ b/config/env/loadtest.app-client-tls.env @@ -49,4 +49,5 @@ FEATURE_FLAG_QUEUE_MANAGEMENT=false FEATURE_FLAG_DODID_UNIQUE=false FEATURE_FLAG_ENABLE_ALASKA=false FEATURE_FLAG_ENABLE_HAWAII=false -FEATURE_FLAG_BULK_ASSIGNMENT=false \ No newline at end of file +FEATURE_FLAG_BULK_ASSIGNMENT=false +FEATURE_FLAG_CUSTOMER_REGISTRATION=false \ No newline at end of file diff --git a/config/env/loadtest.app.env b/config/env/loadtest.app.env index cc97c07c5e8..22d584ea4c6 100644 --- a/config/env/loadtest.app.env +++ b/config/env/loadtest.app.env @@ -54,4 +54,5 @@ FEATURE_FLAG_QUEUE_MANAGEMENT=false FEATURE_FLAG_DODID_UNIQUE=false FEATURE_FLAG_ENABLE_ALASKA=false FEATURE_FLAG_ENABLE_HAWAII=false -FEATURE_FLAG_BULK_ASSIGNMENT=false \ No newline at end of file +FEATURE_FLAG_BULK_ASSIGNMENT=false +FEATURE_FLAG_CUSTOMER_REGISTRATION=false \ No newline at end of file diff --git a/config/env/prd.app-client-tls.env b/config/env/prd.app-client-tls.env index c9d4a6228ed..1a44df90cfd 100644 --- a/config/env/prd.app-client-tls.env +++ b/config/env/prd.app-client-tls.env @@ -48,4 +48,5 @@ FEATURE_FLAG_QUEUE_MANAGEMENT=false FEATURE_FLAG_DODID_UNIQUE=false FEATURE_FLAG_ENABLE_ALASKA=false FEATURE_FLAG_ENABLE_HAWAII=false -FEATURE_FLAG_BULK_ASSIGNMENT=false \ No newline at end of file +FEATURE_FLAG_BULK_ASSIGNMENT=false +FEATURE_FLAG_CUSTOMER_REGISTRATION=false \ No newline at end of file diff --git a/config/env/prd.app.env b/config/env/prd.app.env index 8d2fbc7c7a2..22b755e062f 100644 --- a/config/env/prd.app.env +++ b/config/env/prd.app.env @@ -55,4 +55,5 @@ FEATURE_FLAG_QUEUE_MANAGEMENT=false FEATURE_FLAG_DODID_UNIQUE=false FEATURE_FLAG_ENABLE_ALASKA=false FEATURE_FLAG_ENABLE_HAWAII=false -FEATURE_FLAG_BULK_ASSIGNMENT=false \ No newline at end of file +FEATURE_FLAG_BULK_ASSIGNMENT=false +FEATURE_FLAG_CUSTOMER_REGISTRATION=false \ No newline at end of file diff --git a/config/env/stg.app-client-tls.env b/config/env/stg.app-client-tls.env index 067e1e9bdd2..024a4de2638 100644 --- a/config/env/stg.app-client-tls.env +++ b/config/env/stg.app-client-tls.env @@ -50,4 +50,5 @@ FEATURE_FLAG_QUEUE_MANAGEMENT=false FEATURE_FLAG_DODID_UNIQUE=false FEATURE_FLAG_ENABLE_ALASKA=false FEATURE_FLAG_ENABLE_HAWAII=false -FEATURE_FLAG_BULK_ASSIGNMENT=false \ No newline at end of file +FEATURE_FLAG_BULK_ASSIGNMENT=false +FEATURE_FLAG_CUSTOMER_REGISTRATION=false \ No newline at end of file diff --git a/config/flipt/storage/development.features.yaml b/config/flipt/storage/development.features.yaml index cde89f09329..55bf7a44350 100644 --- a/config/flipt/storage/development.features.yaml +++ b/config/flipt/storage/development.features.yaml @@ -227,6 +227,14 @@ flags: - segment: key: mil-app value: false + - key: customer_registration + name: Customer registration feature flag + type: BOOLEAN_FLAG_TYPE + enabled: false + rollouts: + - segment: + key: mil-app + value: false segments: - key: mil-app name: Mil App diff --git a/pkg/assets/paperwork/formtemplates/ShipmentSummaryWorksheet.pdf b/pkg/assets/paperwork/formtemplates/ShipmentSummaryWorksheet.pdf index 83a149a47d2..44f5774a753 100644 Binary files a/pkg/assets/paperwork/formtemplates/ShipmentSummaryWorksheet.pdf and b/pkg/assets/paperwork/formtemplates/ShipmentSummaryWorksheet.pdf differ diff --git a/pkg/gen/ghcapi/embedded_spec.go b/pkg/gen/ghcapi/embedded_spec.go index 60d6eb7460f..b1979ece4ae 100644 --- a/pkg/gen/ghcapi/embedded_spec.go +++ b/pkg/gen/ghcapi/embedded_spec.go @@ -8230,6 +8230,9 @@ func init() { } ] }, + "ppmType": { + "$ref": "#/definitions/PPMType" + }, "proGearWeight": { "type": "integer", "x-nullable": true @@ -12647,8 +12650,7 @@ func init() { "INCENTIVE_BASED", "ACTUAL_EXPENSE", "SMALL_PACKAGE" - ], - "readOnly": true + ] }, "PWSViolation": { "description": "A PWS violation for an evaluation report", @@ -15163,6 +15165,9 @@ func init() { } ] }, + "ppmType": { + "$ref": "#/definitions/PPMType" + }, "proGearWeight": { "type": "integer", "x-nullable": true @@ -25883,6 +25888,9 @@ func init() { } ] }, + "ppmType": { + "$ref": "#/definitions/PPMType" + }, "proGearWeight": { "type": "integer", "x-nullable": true @@ -30374,8 +30382,7 @@ func init() { "INCENTIVE_BASED", "ACTUAL_EXPENSE", "SMALL_PACKAGE" - ], - "readOnly": true + ] }, "PWSViolation": { "description": "A PWS violation for an evaluation report", @@ -32949,6 +32956,9 @@ func init() { } ] }, + "ppmType": { + "$ref": "#/definitions/PPMType" + }, "proGearWeight": { "type": "integer", "x-nullable": true diff --git a/pkg/gen/ghcmessages/create_p_p_m_shipment.go b/pkg/gen/ghcmessages/create_p_p_m_shipment.go index 87746177f68..82a671eb655 100644 --- a/pkg/gen/ghcmessages/create_p_p_m_shipment.go +++ b/pkg/gen/ghcmessages/create_p_p_m_shipment.go @@ -63,6 +63,9 @@ type CreatePPMShipment struct { Address } `json:"pickupAddress"` + // ppm type + PpmType PPMType `json:"ppmType,omitempty"` + // pro gear weight ProGearWeight *int64 `json:"proGearWeight,omitempty"` @@ -133,6 +136,10 @@ func (m *CreatePPMShipment) Validate(formats strfmt.Registry) error { res = append(res, err) } + if err := m.validatePpmType(formats); err != nil { + res = append(res, err) + } + if err := m.validateSecondaryDestinationAddress(formats); err != nil { res = append(res, err) } @@ -212,6 +219,23 @@ func (m *CreatePPMShipment) validatePickupAddress(formats strfmt.Registry) error return nil } +func (m *CreatePPMShipment) validatePpmType(formats strfmt.Registry) error { + if swag.IsZero(m.PpmType) { // not required + return nil + } + + if err := m.PpmType.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("ppmType") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("ppmType") + } + return err + } + + return nil +} + func (m *CreatePPMShipment) validateSecondaryDestinationAddress(formats strfmt.Registry) error { if swag.IsZero(m.SecondaryDestinationAddress) { // not required return nil @@ -308,6 +332,10 @@ func (m *CreatePPMShipment) ContextValidate(ctx context.Context, formats strfmt. res = append(res, err) } + if err := m.contextValidatePpmType(ctx, formats); err != nil { + res = append(res, err) + } + if err := m.contextValidateSecondaryDestinationAddress(ctx, formats); err != nil { res = append(res, err) } @@ -344,6 +372,24 @@ func (m *CreatePPMShipment) contextValidatePickupAddress(ctx context.Context, fo return nil } +func (m *CreatePPMShipment) contextValidatePpmType(ctx context.Context, formats strfmt.Registry) error { + + if swag.IsZero(m.PpmType) { // not required + return nil + } + + if err := m.PpmType.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("ppmType") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("ppmType") + } + return err + } + + return nil +} + func (m *CreatePPMShipment) contextValidateSecondaryDestinationAddress(ctx context.Context, formats strfmt.Registry) error { return nil diff --git a/pkg/gen/ghcmessages/p_p_m_type.go b/pkg/gen/ghcmessages/p_p_m_type.go index eb16d8cdaf2..0d6f94369c9 100644 --- a/pkg/gen/ghcmessages/p_p_m_type.go +++ b/pkg/gen/ghcmessages/p_p_m_type.go @@ -77,16 +77,7 @@ func (m PPMType) Validate(formats strfmt.Registry) error { return nil } -// ContextValidate validate this p p m type based on the context it is used +// ContextValidate validates this p p m type based on context it is used func (m PPMType) ContextValidate(ctx context.Context, formats strfmt.Registry) error { - var res []error - - if err := validate.ReadOnly(ctx, "", "body", PPMType(m)); err != nil { - return err - } - - if len(res) > 0 { - return errors.CompositeValidationError(res...) - } return nil } diff --git a/pkg/gen/ghcmessages/update_p_p_m_shipment.go b/pkg/gen/ghcmessages/update_p_p_m_shipment.go index 1684f99a4c9..1113dbbcfea 100644 --- a/pkg/gen/ghcmessages/update_p_p_m_shipment.go +++ b/pkg/gen/ghcmessages/update_p_p_m_shipment.go @@ -102,6 +102,9 @@ type UpdatePPMShipment struct { Address } `json:"pickupAddress,omitempty"` + // ppm type + PpmType PPMType `json:"ppmType,omitempty"` + // pro gear weight ProGearWeight *int64 `json:"proGearWeight,omitempty"` @@ -186,6 +189,10 @@ func (m *UpdatePPMShipment) Validate(formats strfmt.Registry) error { res = append(res, err) } + if err := m.validatePpmType(formats); err != nil { + res = append(res, err) + } + if err := m.validateSecondaryDestinationAddress(formats); err != nil { res = append(res, err) } @@ -319,6 +326,23 @@ func (m *UpdatePPMShipment) validatePickupAddress(formats strfmt.Registry) error return nil } +func (m *UpdatePPMShipment) validatePpmType(formats strfmt.Registry) error { + if swag.IsZero(m.PpmType) { // not required + return nil + } + + if err := m.PpmType.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("ppmType") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("ppmType") + } + return err + } + + return nil +} + func (m *UpdatePPMShipment) validateSecondaryDestinationAddress(formats strfmt.Registry) error { if swag.IsZero(m.SecondaryDestinationAddress) { // not required return nil @@ -429,6 +453,10 @@ func (m *UpdatePPMShipment) ContextValidate(ctx context.Context, formats strfmt. res = append(res, err) } + if err := m.contextValidatePpmType(ctx, formats); err != nil { + res = append(res, err) + } + if err := m.contextValidateSecondaryDestinationAddress(ctx, formats); err != nil { res = append(res, err) } @@ -490,6 +518,24 @@ func (m *UpdatePPMShipment) contextValidatePickupAddress(ctx context.Context, fo return nil } +func (m *UpdatePPMShipment) contextValidatePpmType(ctx context.Context, formats strfmt.Registry) error { + + if swag.IsZero(m.PpmType) { // not required + return nil + } + + if err := m.PpmType.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("ppmType") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("ppmType") + } + return err + } + + return nil +} + func (m *UpdatePPMShipment) contextValidateSecondaryDestinationAddress(ctx context.Context, formats strfmt.Registry) error { return nil diff --git a/pkg/gen/internalapi/configure_mymove.go b/pkg/gen/internalapi/configure_mymove.go index 3b277e0037c..06ecfa8af5c 100644 --- a/pkg/gen/internalapi/configure_mymove.go +++ b/pkg/gen/internalapi/configure_mymove.go @@ -85,6 +85,11 @@ func configureAPI(api *internaloperations.MymoveAPI) http.Handler { return middleware.NotImplemented("operation feature_flags.BooleanFeatureFlagForUser has not yet been implemented") }) } + if api.FeatureFlagsBooleanFeatureFlagUnauthenticatedHandler == nil { + api.FeatureFlagsBooleanFeatureFlagUnauthenticatedHandler = feature_flags.BooleanFeatureFlagUnauthenticatedHandlerFunc(func(params feature_flags.BooleanFeatureFlagUnauthenticatedParams) middleware.Responder { + return middleware.NotImplemented("operation feature_flags.BooleanFeatureFlagUnauthenticated has not yet been implemented") + }) + } if api.OfficeCancelMoveHandler == nil { api.OfficeCancelMoveHandler = office.CancelMoveHandlerFunc(func(params office.CancelMoveParams) middleware.Responder { return middleware.NotImplemented("operation office.CancelMove has not yet been implemented") diff --git a/pkg/gen/internalapi/embedded_spec.go b/pkg/gen/internalapi/embedded_spec.go index ed60226b151..40610e84331 100644 --- a/pkg/gen/internalapi/embedded_spec.go +++ b/pkg/gen/internalapi/embedded_spec.go @@ -1666,6 +1666,57 @@ func init() { } } }, + "/open/feature-flags/boolean/{key}": { + "post": { + "description": "Determines if a feature flag is enabled.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "featureFlags" + ], + "summary": "Determines if a feature flag is enabled. Only used for unauthenticated users.", + "operationId": "booleanFeatureFlagUnauthenticated", + "parameters": [ + { + "type": "string", + "description": "Feature Flag Key", + "name": "key", + "in": "path", + "required": true + }, + { + "description": "context for the feature flag request", + "name": "flagContext", + "in": "body", + "required": true, + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + ], + "responses": { + "200": { + "description": "Boolean Feature Flag Status", + "schema": { + "$ref": "#/definitions/FeatureFlagBoolean" + } + }, + "401": { + "description": "request requires user authentication" + }, + "500": { + "description": "internal server error" + } + } + } + }, "/orders": { "post": { "description": "Creates an instance of orders tied to a service member", @@ -3920,6 +3971,9 @@ func init() { "pickupAddress": { "$ref": "#/definitions/Address" }, + "ppmType": { + "$ref": "#/definitions/PPMType" + }, "secondaryDestinationAddress": { "$ref": "#/definitions/Address" }, @@ -6705,8 +6759,7 @@ func init() { "INCENTIVE_BASED", "ACTUAL_EXPENSE", "SMALL_PACKAGE" - ], - "readOnly": true + ] }, "PatchMovePayload": { "type": "object", @@ -7861,6 +7914,9 @@ func init() { "pickupAddress": { "$ref": "#/definitions/Address" }, + "ppmType": { + "$ref": "#/definitions/PPMType" + }, "proGearWeight": { "type": "integer", "x-nullable": true @@ -10471,6 +10527,57 @@ func init() { } } }, + "/open/feature-flags/boolean/{key}": { + "post": { + "description": "Determines if a feature flag is enabled.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "featureFlags" + ], + "summary": "Determines if a feature flag is enabled. Only used for unauthenticated users.", + "operationId": "booleanFeatureFlagUnauthenticated", + "parameters": [ + { + "type": "string", + "description": "Feature Flag Key", + "name": "key", + "in": "path", + "required": true + }, + { + "description": "context for the feature flag request", + "name": "flagContext", + "in": "body", + "required": true, + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + ], + "responses": { + "200": { + "description": "Boolean Feature Flag Status", + "schema": { + "$ref": "#/definitions/FeatureFlagBoolean" + } + }, + "401": { + "description": "request requires user authentication" + }, + "500": { + "description": "internal server error" + } + } + } + }, "/orders": { "post": { "description": "Creates an instance of orders tied to a service member", @@ -13081,6 +13188,9 @@ func init() { "pickupAddress": { "$ref": "#/definitions/Address" }, + "ppmType": { + "$ref": "#/definitions/PPMType" + }, "secondaryDestinationAddress": { "$ref": "#/definitions/Address" }, @@ -15871,8 +15981,7 @@ func init() { "INCENTIVE_BASED", "ACTUAL_EXPENSE", "SMALL_PACKAGE" - ], - "readOnly": true + ] }, "PatchMovePayload": { "type": "object", @@ -17029,6 +17138,9 @@ func init() { "pickupAddress": { "$ref": "#/definitions/Address" }, + "ppmType": { + "$ref": "#/definitions/PPMType" + }, "proGearWeight": { "type": "integer", "x-nullable": true diff --git a/pkg/gen/internalapi/internaloperations/feature_flags/boolean_feature_flag_unauthenticated.go b/pkg/gen/internalapi/internaloperations/feature_flags/boolean_feature_flag_unauthenticated.go new file mode 100644 index 00000000000..e36dc8f224c --- /dev/null +++ b/pkg/gen/internalapi/internaloperations/feature_flags/boolean_feature_flag_unauthenticated.go @@ -0,0 +1,58 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package feature_flags + +// 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" +) + +// BooleanFeatureFlagUnauthenticatedHandlerFunc turns a function with the right signature into a boolean feature flag unauthenticated handler +type BooleanFeatureFlagUnauthenticatedHandlerFunc func(BooleanFeatureFlagUnauthenticatedParams) middleware.Responder + +// Handle executing the request and returning a response +func (fn BooleanFeatureFlagUnauthenticatedHandlerFunc) Handle(params BooleanFeatureFlagUnauthenticatedParams) middleware.Responder { + return fn(params) +} + +// BooleanFeatureFlagUnauthenticatedHandler interface for that can handle valid boolean feature flag unauthenticated params +type BooleanFeatureFlagUnauthenticatedHandler interface { + Handle(BooleanFeatureFlagUnauthenticatedParams) middleware.Responder +} + +// NewBooleanFeatureFlagUnauthenticated creates a new http.Handler for the boolean feature flag unauthenticated operation +func NewBooleanFeatureFlagUnauthenticated(ctx *middleware.Context, handler BooleanFeatureFlagUnauthenticatedHandler) *BooleanFeatureFlagUnauthenticated { + return &BooleanFeatureFlagUnauthenticated{Context: ctx, Handler: handler} +} + +/* + BooleanFeatureFlagUnauthenticated swagger:route POST /open/feature-flags/boolean/{key} featureFlags booleanFeatureFlagUnauthenticated + +Determines if a feature flag is enabled. Only used for unauthenticated users. + +Determines if a feature flag is enabled. +*/ +type BooleanFeatureFlagUnauthenticated struct { + Context *middleware.Context + Handler BooleanFeatureFlagUnauthenticatedHandler +} + +func (o *BooleanFeatureFlagUnauthenticated) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + route, rCtx, _ := o.Context.RouteInfo(r) + if rCtx != nil { + *r = *rCtx + } + var Params = NewBooleanFeatureFlagUnauthenticatedParams() + 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/internalapi/internaloperations/feature_flags/boolean_feature_flag_unauthenticated_parameters.go b/pkg/gen/internalapi/internaloperations/feature_flags/boolean_feature_flag_unauthenticated_parameters.go new file mode 100644 index 00000000000..10e7184e8e5 --- /dev/null +++ b/pkg/gen/internalapi/internaloperations/feature_flags/boolean_feature_flag_unauthenticated_parameters.go @@ -0,0 +1,95 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package feature_flags + +// 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/errors" + "github.com/go-openapi/runtime" + "github.com/go-openapi/runtime/middleware" + "github.com/go-openapi/strfmt" +) + +// NewBooleanFeatureFlagUnauthenticatedParams creates a new BooleanFeatureFlagUnauthenticatedParams object +// +// There are no default values defined in the spec. +func NewBooleanFeatureFlagUnauthenticatedParams() BooleanFeatureFlagUnauthenticatedParams { + + return BooleanFeatureFlagUnauthenticatedParams{} +} + +// BooleanFeatureFlagUnauthenticatedParams contains all the bound params for the boolean feature flag unauthenticated operation +// typically these are obtained from a http.Request +// +// swagger:parameters booleanFeatureFlagUnauthenticated +type BooleanFeatureFlagUnauthenticatedParams struct { + + // HTTP Request Object + HTTPRequest *http.Request `json:"-"` + + /*context for the feature flag request + Required: true + In: body + */ + FlagContext map[string]string + /*Feature Flag Key + Required: true + In: path + */ + Key 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 NewBooleanFeatureFlagUnauthenticatedParams() beforehand. +func (o *BooleanFeatureFlagUnauthenticatedParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error { + var res []error + + o.HTTPRequest = r + + if runtime.HasBody(r) { + defer r.Body.Close() + var body map[string]string + if err := route.Consumer.Consume(r.Body, &body); err != nil { + if err == io.EOF { + res = append(res, errors.Required("flagContext", "body", "")) + } else { + res = append(res, errors.NewParseError("flagContext", "body", "", err)) + } + } else { + // no validation required on inline body + o.FlagContext = body + } + } else { + res = append(res, errors.Required("flagContext", "body", "")) + } + + rKey, rhkKey, _ := route.Params.GetOK("key") + if err := o.bindKey(rKey, rhkKey, route.Formats); err != nil { + res = append(res, err) + } + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +// bindKey binds and validates parameter Key from path. +func (o *BooleanFeatureFlagUnauthenticatedParams) bindKey(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.Key = raw + + return nil +} diff --git a/pkg/gen/internalapi/internaloperations/feature_flags/boolean_feature_flag_unauthenticated_responses.go b/pkg/gen/internalapi/internaloperations/feature_flags/boolean_feature_flag_unauthenticated_responses.go new file mode 100644 index 00000000000..f69a6ae0702 --- /dev/null +++ b/pkg/gen/internalapi/internaloperations/feature_flags/boolean_feature_flag_unauthenticated_responses.go @@ -0,0 +1,109 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package feature_flags + +// 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/runtime" + + "github.com/transcom/mymove/pkg/gen/internalmessages" +) + +// BooleanFeatureFlagUnauthenticatedOKCode is the HTTP code returned for type BooleanFeatureFlagUnauthenticatedOK +const BooleanFeatureFlagUnauthenticatedOKCode int = 200 + +/* +BooleanFeatureFlagUnauthenticatedOK Boolean Feature Flag Status + +swagger:response booleanFeatureFlagUnauthenticatedOK +*/ +type BooleanFeatureFlagUnauthenticatedOK struct { + + /* + In: Body + */ + Payload *internalmessages.FeatureFlagBoolean `json:"body,omitempty"` +} + +// NewBooleanFeatureFlagUnauthenticatedOK creates BooleanFeatureFlagUnauthenticatedOK with default headers values +func NewBooleanFeatureFlagUnauthenticatedOK() *BooleanFeatureFlagUnauthenticatedOK { + + return &BooleanFeatureFlagUnauthenticatedOK{} +} + +// WithPayload adds the payload to the boolean feature flag unauthenticated o k response +func (o *BooleanFeatureFlagUnauthenticatedOK) WithPayload(payload *internalmessages.FeatureFlagBoolean) *BooleanFeatureFlagUnauthenticatedOK { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the boolean feature flag unauthenticated o k response +func (o *BooleanFeatureFlagUnauthenticatedOK) SetPayload(payload *internalmessages.FeatureFlagBoolean) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *BooleanFeatureFlagUnauthenticatedOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(200) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} + +// BooleanFeatureFlagUnauthenticatedUnauthorizedCode is the HTTP code returned for type BooleanFeatureFlagUnauthenticatedUnauthorized +const BooleanFeatureFlagUnauthenticatedUnauthorizedCode int = 401 + +/* +BooleanFeatureFlagUnauthenticatedUnauthorized request requires user authentication + +swagger:response booleanFeatureFlagUnauthenticatedUnauthorized +*/ +type BooleanFeatureFlagUnauthenticatedUnauthorized struct { +} + +// NewBooleanFeatureFlagUnauthenticatedUnauthorized creates BooleanFeatureFlagUnauthenticatedUnauthorized with default headers values +func NewBooleanFeatureFlagUnauthenticatedUnauthorized() *BooleanFeatureFlagUnauthenticatedUnauthorized { + + return &BooleanFeatureFlagUnauthenticatedUnauthorized{} +} + +// WriteResponse to the client +func (o *BooleanFeatureFlagUnauthenticatedUnauthorized) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.Header().Del(runtime.HeaderContentType) //Remove Content-Type on empty responses + + rw.WriteHeader(401) +} + +// BooleanFeatureFlagUnauthenticatedInternalServerErrorCode is the HTTP code returned for type BooleanFeatureFlagUnauthenticatedInternalServerError +const BooleanFeatureFlagUnauthenticatedInternalServerErrorCode int = 500 + +/* +BooleanFeatureFlagUnauthenticatedInternalServerError internal server error + +swagger:response booleanFeatureFlagUnauthenticatedInternalServerError +*/ +type BooleanFeatureFlagUnauthenticatedInternalServerError struct { +} + +// NewBooleanFeatureFlagUnauthenticatedInternalServerError creates BooleanFeatureFlagUnauthenticatedInternalServerError with default headers values +func NewBooleanFeatureFlagUnauthenticatedInternalServerError() *BooleanFeatureFlagUnauthenticatedInternalServerError { + + return &BooleanFeatureFlagUnauthenticatedInternalServerError{} +} + +// WriteResponse to the client +func (o *BooleanFeatureFlagUnauthenticatedInternalServerError) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.Header().Del(runtime.HeaderContentType) //Remove Content-Type on empty responses + + rw.WriteHeader(500) +} diff --git a/pkg/gen/internalapi/internaloperations/feature_flags/boolean_feature_flag_unauthenticated_urlbuilder.go b/pkg/gen/internalapi/internaloperations/feature_flags/boolean_feature_flag_unauthenticated_urlbuilder.go new file mode 100644 index 00000000000..544b0762f69 --- /dev/null +++ b/pkg/gen/internalapi/internaloperations/feature_flags/boolean_feature_flag_unauthenticated_urlbuilder.go @@ -0,0 +1,99 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package feature_flags + +// 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" +) + +// BooleanFeatureFlagUnauthenticatedURL generates an URL for the boolean feature flag unauthenticated operation +type BooleanFeatureFlagUnauthenticatedURL struct { + Key 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 *BooleanFeatureFlagUnauthenticatedURL) WithBasePath(bp string) *BooleanFeatureFlagUnauthenticatedURL { + 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 *BooleanFeatureFlagUnauthenticatedURL) SetBasePath(bp string) { + o._basePath = bp +} + +// Build a url path and query string +func (o *BooleanFeatureFlagUnauthenticatedURL) Build() (*url.URL, error) { + var _result url.URL + + var _path = "/open/feature-flags/boolean/{key}" + + key := o.Key + if key != "" { + _path = strings.Replace(_path, "{key}", key, -1) + } else { + return nil, errors.New("key is required on BooleanFeatureFlagUnauthenticatedURL") + } + + _basePath := o._basePath + if _basePath == "" { + _basePath = "/internal" + } + _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 *BooleanFeatureFlagUnauthenticatedURL) 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 *BooleanFeatureFlagUnauthenticatedURL) String() string { + return o.Must(o.Build()).String() +} + +// BuildFull builds a full url with scheme, host, path and query string +func (o *BooleanFeatureFlagUnauthenticatedURL) BuildFull(scheme, host string) (*url.URL, error) { + if scheme == "" { + return nil, errors.New("scheme is required for a full url on BooleanFeatureFlagUnauthenticatedURL") + } + if host == "" { + return nil, errors.New("host is required for a full url on BooleanFeatureFlagUnauthenticatedURL") + } + + 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 *BooleanFeatureFlagUnauthenticatedURL) StringFull(scheme, host string) string { + return o.Must(o.BuildFull(scheme, host)).String() +} diff --git a/pkg/gen/internalapi/internaloperations/mymove_api.go b/pkg/gen/internalapi/internaloperations/mymove_api.go index b1ba4e1ac47..fb2ed827df3 100644 --- a/pkg/gen/internalapi/internaloperations/mymove_api.go +++ b/pkg/gen/internalapi/internaloperations/mymove_api.go @@ -76,6 +76,9 @@ func NewMymoveAPI(spec *loads.Document) *MymoveAPI { FeatureFlagsBooleanFeatureFlagForUserHandler: feature_flags.BooleanFeatureFlagForUserHandlerFunc(func(params feature_flags.BooleanFeatureFlagForUserParams) middleware.Responder { return middleware.NotImplemented("operation feature_flags.BooleanFeatureFlagForUser has not yet been implemented") }), + FeatureFlagsBooleanFeatureFlagUnauthenticatedHandler: feature_flags.BooleanFeatureFlagUnauthenticatedHandlerFunc(func(params feature_flags.BooleanFeatureFlagUnauthenticatedParams) middleware.Responder { + return middleware.NotImplemented("operation feature_flags.BooleanFeatureFlagUnauthenticated has not yet been implemented") + }), OfficeCancelMoveHandler: office.CancelMoveHandlerFunc(func(params office.CancelMoveParams) middleware.Responder { return middleware.NotImplemented("operation office.CancelMove has not yet been implemented") }), @@ -330,6 +333,8 @@ type MymoveAPI struct { OfficeApproveReimbursementHandler office.ApproveReimbursementHandler // FeatureFlagsBooleanFeatureFlagForUserHandler sets the operation handler for the boolean feature flag for user operation FeatureFlagsBooleanFeatureFlagForUserHandler feature_flags.BooleanFeatureFlagForUserHandler + // FeatureFlagsBooleanFeatureFlagUnauthenticatedHandler sets the operation handler for the boolean feature flag unauthenticated operation + FeatureFlagsBooleanFeatureFlagUnauthenticatedHandler feature_flags.BooleanFeatureFlagUnauthenticatedHandler // OfficeCancelMoveHandler sets the operation handler for the cancel move operation OfficeCancelMoveHandler office.CancelMoveHandler // DocumentsCreateDocumentHandler sets the operation handler for the create document operation @@ -556,6 +561,9 @@ func (o *MymoveAPI) Validate() error { if o.FeatureFlagsBooleanFeatureFlagForUserHandler == nil { unregistered = append(unregistered, "feature_flags.BooleanFeatureFlagForUserHandler") } + if o.FeatureFlagsBooleanFeatureFlagUnauthenticatedHandler == nil { + unregistered = append(unregistered, "feature_flags.BooleanFeatureFlagUnauthenticatedHandler") + } if o.OfficeCancelMoveHandler == nil { unregistered = append(unregistered, "office.CancelMoveHandler") } @@ -864,6 +872,10 @@ func (o *MymoveAPI) initHandlerCache() { if o.handlers["POST"] == nil { o.handlers["POST"] = make(map[string]http.Handler) } + o.handlers["POST"]["/open/feature-flags/boolean/{key}"] = feature_flags.NewBooleanFeatureFlagUnauthenticated(o.context, o.FeatureFlagsBooleanFeatureFlagUnauthenticatedHandler) + if o.handlers["POST"] == nil { + o.handlers["POST"] = make(map[string]http.Handler) + } o.handlers["POST"]["/moves/{moveId}/cancel"] = office.NewCancelMove(o.context, o.OfficeCancelMoveHandler) if o.handlers["POST"] == nil { o.handlers["POST"] = make(map[string]http.Handler) diff --git a/pkg/gen/internalmessages/create_p_p_m_shipment.go b/pkg/gen/internalmessages/create_p_p_m_shipment.go index 98c9983390d..a8b0993bd45 100644 --- a/pkg/gen/internalmessages/create_p_p_m_shipment.go +++ b/pkg/gen/internalmessages/create_p_p_m_shipment.go @@ -43,6 +43,9 @@ type CreatePPMShipment struct { // Required: true PickupAddress *Address `json:"pickupAddress"` + // ppm type + PpmType PPMType `json:"ppmType,omitempty"` + // secondary destination address SecondaryDestinationAddress *Address `json:"secondaryDestinationAddress,omitempty"` @@ -76,6 +79,10 @@ func (m *CreatePPMShipment) Validate(formats strfmt.Registry) error { res = append(res, err) } + if err := m.validatePpmType(formats); err != nil { + res = append(res, err) + } + if err := m.validateSecondaryDestinationAddress(formats); err != nil { res = append(res, err) } @@ -155,6 +162,23 @@ func (m *CreatePPMShipment) validatePickupAddress(formats strfmt.Registry) error return nil } +func (m *CreatePPMShipment) validatePpmType(formats strfmt.Registry) error { + if swag.IsZero(m.PpmType) { // not required + return nil + } + + if err := m.PpmType.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("ppmType") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("ppmType") + } + return err + } + + return nil +} + func (m *CreatePPMShipment) validateSecondaryDestinationAddress(formats strfmt.Registry) error { if swag.IsZero(m.SecondaryDestinationAddress) { // not required return nil @@ -252,6 +276,10 @@ func (m *CreatePPMShipment) ContextValidate(ctx context.Context, formats strfmt. res = append(res, err) } + if err := m.contextValidatePpmType(ctx, formats); err != nil { + res = append(res, err) + } + if err := m.contextValidateSecondaryDestinationAddress(ctx, formats); err != nil { res = append(res, err) } @@ -308,6 +336,24 @@ func (m *CreatePPMShipment) contextValidatePickupAddress(ctx context.Context, fo return nil } +func (m *CreatePPMShipment) contextValidatePpmType(ctx context.Context, formats strfmt.Registry) error { + + if swag.IsZero(m.PpmType) { // not required + return nil + } + + if err := m.PpmType.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("ppmType") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("ppmType") + } + return err + } + + return nil +} + func (m *CreatePPMShipment) contextValidateSecondaryDestinationAddress(ctx context.Context, formats strfmt.Registry) error { if m.SecondaryDestinationAddress != nil { diff --git a/pkg/gen/internalmessages/p_p_m_type.go b/pkg/gen/internalmessages/p_p_m_type.go index 4ba102b4a09..769a94b1c1b 100644 --- a/pkg/gen/internalmessages/p_p_m_type.go +++ b/pkg/gen/internalmessages/p_p_m_type.go @@ -77,16 +77,7 @@ func (m PPMType) Validate(formats strfmt.Registry) error { return nil } -// ContextValidate validate this p p m type based on the context it is used +// ContextValidate validates this p p m type based on context it is used func (m PPMType) ContextValidate(ctx context.Context, formats strfmt.Registry) error { - var res []error - - if err := validate.ReadOnly(ctx, "", "body", PPMType(m)); err != nil { - return err - } - - if len(res) > 0 { - return errors.CompositeValidationError(res...) - } return nil } diff --git a/pkg/gen/internalmessages/update_p_p_m_shipment.go b/pkg/gen/internalmessages/update_p_p_m_shipment.go index 2e1be3bd9cb..5b9402d8d2d 100644 --- a/pkg/gen/internalmessages/update_p_p_m_shipment.go +++ b/pkg/gen/internalmessages/update_p_p_m_shipment.go @@ -95,6 +95,9 @@ type UpdatePPMShipment struct { // pickup address PickupAddress *Address `json:"pickupAddress,omitempty"` + // ppm type + PpmType PPMType `json:"ppmType,omitempty"` + // pro gear weight ProGearWeight *int64 `json:"proGearWeight,omitempty"` @@ -148,6 +151,10 @@ func (m *UpdatePPMShipment) Validate(formats strfmt.Registry) error { res = append(res, err) } + if err := m.validatePpmType(formats); err != nil { + res = append(res, err) + } + if err := m.validateSecondaryDestinationAddress(formats); err != nil { res = append(res, err) } @@ -260,6 +267,23 @@ func (m *UpdatePPMShipment) validatePickupAddress(formats strfmt.Registry) error return nil } +func (m *UpdatePPMShipment) validatePpmType(formats strfmt.Registry) error { + if swag.IsZero(m.PpmType) { // not required + return nil + } + + if err := m.PpmType.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("ppmType") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("ppmType") + } + return err + } + + return nil +} + func (m *UpdatePPMShipment) validateSecondaryDestinationAddress(formats strfmt.Registry) error { if swag.IsZero(m.SecondaryDestinationAddress) { // not required return nil @@ -371,6 +395,10 @@ func (m *UpdatePPMShipment) ContextValidate(ctx context.Context, formats strfmt. res = append(res, err) } + if err := m.contextValidatePpmType(ctx, formats); err != nil { + res = append(res, err) + } + if err := m.contextValidateSecondaryDestinationAddress(ctx, formats); err != nil { res = append(res, err) } @@ -448,6 +476,24 @@ func (m *UpdatePPMShipment) contextValidatePickupAddress(ctx context.Context, fo return nil } +func (m *UpdatePPMShipment) contextValidatePpmType(ctx context.Context, formats strfmt.Registry) error { + + if swag.IsZero(m.PpmType) { // not required + return nil + } + + if err := m.PpmType.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("ppmType") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("ppmType") + } + return err + } + + return nil +} + func (m *UpdatePPMShipment) contextValidateSecondaryDestinationAddress(ctx context.Context, formats strfmt.Registry) error { if m.SecondaryDestinationAddress != nil { diff --git a/pkg/handlers/ghcapi/internal/payloads/model_to_payload.go b/pkg/handlers/ghcapi/internal/payloads/model_to_payload.go index a874911a3b0..cf5bd394b46 100644 --- a/pkg/handlers/ghcapi/internal/payloads/model_to_payload.go +++ b/pkg/handlers/ghcapi/internal/payloads/model_to_payload.go @@ -987,6 +987,7 @@ func PPMShipment(_ storage.FileStorer, ppmShipment *models.PPMShipment) *ghcmess payloadPPMShipment := &ghcmessages.PPMShipment{ ID: *handlers.FmtUUID(ppmShipment.ID), + PpmType: ghcmessages.PPMType(ppmShipment.PPMType), ShipmentID: *handlers.FmtUUID(ppmShipment.ShipmentID), CreatedAt: strfmt.DateTime(ppmShipment.CreatedAt), UpdatedAt: strfmt.DateTime(ppmShipment.UpdatedAt), diff --git a/pkg/handlers/ghcapi/internal/payloads/payload_to_model.go b/pkg/handlers/ghcapi/internal/payloads/payload_to_model.go index bb05138dec6..4ce9d4c08db 100644 --- a/pkg/handlers/ghcapi/internal/payloads/payload_to_model.go +++ b/pkg/handlers/ghcapi/internal/payloads/payload_to_model.go @@ -320,6 +320,7 @@ func PPMShipmentModelFromCreate(ppmShipment *ghcmessages.CreatePPMShipment) *mod } model := &models.PPMShipment{ + PPMType: models.PPMType(ppmShipment.PpmType), Status: models.PPMShipmentStatusSubmitted, SITExpected: ppmShipment.SitExpected, EstimatedWeight: handlers.PoundPtrFromInt64Ptr(ppmShipment.EstimatedWeight), @@ -626,6 +627,7 @@ func PPMShipmentModelFromUpdate(ppmShipment *ghcmessages.UpdatePPMShipment) *mod return nil } model := &models.PPMShipment{ + PPMType: models.PPMType(ppmShipment.PpmType), ActualMoveDate: (*time.Time)(ppmShipment.ActualMoveDate), SITExpected: ppmShipment.SitExpected, EstimatedWeight: handlers.PoundPtrFromInt64Ptr(ppmShipment.EstimatedWeight), diff --git a/pkg/handlers/ghcapi/internal/payloads/payload_to_model_test.go b/pkg/handlers/ghcapi/internal/payloads/payload_to_model_test.go index d37aac4fb30..91ab4d66ea6 100644 --- a/pkg/handlers/ghcapi/internal/payloads/payload_to_model_test.go +++ b/pkg/handlers/ghcapi/internal/payloads/payload_to_model_test.go @@ -227,6 +227,7 @@ func (suite *PayloadsSuite) TestPPMShipmentModelWithOptionalDestinationStreet1Fr } ppmShipment := ghcmessages.CreatePPMShipment{ + PpmType: ghcmessages.PPMType(models.PPMTypeIncentiveBased), ExpectedDepartureDate: expectedDepartureDate, PickupAddress: struct{ ghcmessages.Address }{pickupAddress}, DestinationAddress: struct { @@ -239,6 +240,7 @@ func (suite *PayloadsSuite) TestPPMShipmentModelWithOptionalDestinationStreet1Fr suite.NotNil(model) suite.Equal(models.PPMShipmentStatusSubmitted, model.Status) suite.Equal(model.DestinationAddress.StreetAddress1, models.STREET_ADDRESS_1_NOT_PROVIDED) + suite.Equal(model.PPMType, models.PPMTypeIncentiveBased) suite.NotNil(model) // test when street address 1 contains white spaces diff --git a/pkg/handlers/ghcapi/mto_shipment_test.go b/pkg/handlers/ghcapi/mto_shipment_test.go index 353f4c655e0..035667e35c8 100644 --- a/pkg/handlers/ghcapi/mto_shipment_test.go +++ b/pkg/handlers/ghcapi/mto_shipment_test.go @@ -4091,6 +4091,7 @@ func (suite *HandlerSuite) TestCreateMTOShipmentHandlerUsingPPM() { MoveTaskOrderID: handlers.FmtUUID(move.ID), ShipmentType: &shipmentType, PpmShipment: &ghcmessages.CreatePPMShipment{ + PpmType: ghcmessages.PPMType(models.PPMTypeIncentiveBased), ExpectedDepartureDate: handlers.FmtDatePtr(expectedDepartureDate), PickupAddress: struct{ ghcmessages.Address }{pickupAddress}, SecondaryPickupAddress: struct{ ghcmessages.Address }{secondaryPickupAddress}, @@ -4148,6 +4149,7 @@ func (suite *HandlerSuite) TestCreateMTOShipmentHandlerUsingPPM() { if suite.NotNil(ppmPayload) { suite.NotZero(ppmPayload.ID) suite.NotEqual(uuid.Nil.String(), ppmPayload.ID.String()) + suite.NotNil(ppmPayload.PpmType) suite.EqualDatePtr(expectedDepartureDate, ppmPayload.ExpectedDepartureDate) suite.Equal(expectedPickupAddress.PostalCode, *ppmPayload.PickupAddress.PostalCode) suite.Equal(&expectedSecondaryPickupAddress.PostalCode, ppmPayload.SecondaryPickupAddress.PostalCode) diff --git a/pkg/handlers/internalapi/api.go b/pkg/handlers/internalapi/api.go index 192663228be..e64625b0e32 100644 --- a/pkg/handlers/internalapi/api.go +++ b/pkg/handlers/internalapi/api.go @@ -110,6 +110,7 @@ func NewInternalAPI(handlerConfig handlers.HandlerConfig) *internalops.MymoveAPI if err != nil { log.Fatalln(err) } + internalAPI.FeatureFlagsBooleanFeatureFlagUnauthenticatedHandler = BooleanFeatureFlagsUnauthenticatedHandler{handlerConfig} internalAPI.FeatureFlagsBooleanFeatureFlagForUserHandler = BooleanFeatureFlagsForUserHandler{handlerConfig} internalAPI.FeatureFlagsVariantFeatureFlagForUserHandler = VariantFeatureFlagsForUserHandler{handlerConfig} diff --git a/pkg/handlers/internalapi/feature_flag.go b/pkg/handlers/internalapi/feature_flag.go index 4f4d3d5d268..4cdd20716ea 100644 --- a/pkg/handlers/internalapi/feature_flag.go +++ b/pkg/handlers/internalapi/feature_flag.go @@ -4,11 +4,41 @@ import ( "github.com/go-openapi/runtime/middleware" "github.com/transcom/mymove/pkg/appcontext" + "github.com/transcom/mymove/pkg/apperror" ffop "github.com/transcom/mymove/pkg/gen/internalapi/internaloperations/feature_flags" "github.com/transcom/mymove/pkg/gen/internalmessages" "github.com/transcom/mymove/pkg/handlers" ) +// BooleanFeatureFlagsUnauthenticatedHandler handles evaluating boolean feature flags outside of authentication +type BooleanFeatureFlagsUnauthenticatedHandler struct { + handlers.HandlerConfig +} + +// Handle returns the boolean feature flag for an unauthenticated user +func (h BooleanFeatureFlagsUnauthenticatedHandler) Handle(params ffop.BooleanFeatureFlagUnauthenticatedParams) middleware.Responder { + return h.AuditableAppContextFromRequestWithErrors(params.HTTPRequest, + func(appCtx appcontext.AppContext) (middleware.Responder, error) { + // we are only allowing this to be called from the customer app + // since this is an open route outside of auth, we want to buckle down on validation here + if !appCtx.Session().IsMilApp() { + return ffop.NewBooleanFeatureFlagUnauthenticatedUnauthorized(), apperror.NewSessionError("Request is not from the customer app") + } + flag, err := h.FeatureFlagFetcher().GetBooleanFlag( + params.HTTPRequest.Context(), appCtx.Logger(), "customer", params.Key, params.FlagContext) + if err != nil { + return handlers.ResponseForError(appCtx.Logger(), err), err + } + flagPayload := internalmessages.FeatureFlagBoolean{ + Entity: &flag.Entity, + Key: ¶ms.Key, + Match: &flag.Match, + Namespace: &flag.Namespace, + } + return ffop.NewBooleanFeatureFlagUnauthenticatedOK().WithPayload(&flagPayload), nil + }) +} + // BooleanFeatureFlagsForUserHandler handles evaluating boolean feature flags for // users type BooleanFeatureFlagsForUserHandler struct { diff --git a/pkg/handlers/internalapi/feature_flag_test.go b/pkg/handlers/internalapi/feature_flag_test.go index aab6ecaeda6..438215244fd 100644 --- a/pkg/handlers/internalapi/feature_flag_test.go +++ b/pkg/handlers/internalapi/feature_flag_test.go @@ -5,11 +5,69 @@ import ( "github.com/go-openapi/strfmt" + "github.com/transcom/mymove/pkg/auth" "github.com/transcom/mymove/pkg/factory" ffop "github.com/transcom/mymove/pkg/gen/internalapi/internaloperations/feature_flags" "github.com/transcom/mymove/pkg/services" ) +func (suite *HandlerSuite) TestBooleanFeatureFlagUnauthenticatedHandler() { + suite.Run("success for unauthenticated user in the customer app", func() { + req := httptest.NewRequest("POST", "/open/feature-flags/boolean/test_ff", nil) + session := &auth.Session{ + ApplicationName: auth.MilApp, + } + ctx := auth.SetSessionInRequestContext(req, session) + + params := ffop.BooleanFeatureFlagUnauthenticatedParams{ + HTTPRequest: req.WithContext(ctx), + Key: "key", + FlagContext: map[string]string{ + "thing": "one", + }, + } + + handler := BooleanFeatureFlagsUnauthenticatedHandler{suite.HandlerConfig()} + + response := handler.Handle(params) + + okResponse, ok := response.(*ffop.BooleanFeatureFlagUnauthenticatedOK) + suite.True(ok) + suite.NoError(okResponse.Payload.Validate(strfmt.Default)) + expected := services.FeatureFlag{ + Entity: "user@example.com", + Key: params.Key, + Match: true, + Namespace: "test", + } + suite.Equal(expected.Entity, *okResponse.Payload.Entity) + suite.Equal(expected.Key, *okResponse.Payload.Key) + suite.Equal(expected.Match, *okResponse.Payload.Match) + suite.Equal(expected.Namespace, *okResponse.Payload.Namespace) + }) + suite.Run("error for unauthenticated user outside the customer app", func() { + req := httptest.NewRequest("POST", "/open/feature-flags/boolean/test_ff", nil) + session := &auth.Session{ + ApplicationName: auth.OfficeApp, + } + ctx := auth.SetSessionInRequestContext(req, session) + + params := ffop.BooleanFeatureFlagUnauthenticatedParams{ + HTTPRequest: req.WithContext(ctx), + Key: "key", + FlagContext: map[string]string{ + "thing": "one", + }, + } + + handler := BooleanFeatureFlagsUnauthenticatedHandler{suite.HandlerConfig()} + response := handler.Handle(params) + res, ok := response.(*ffop.BooleanFeatureFlagUnauthenticatedUnauthorized) + suite.True(ok) + suite.IsType(&ffop.BooleanFeatureFlagUnauthenticatedUnauthorized{}, res) + }) +} + func (suite *HandlerSuite) TestBooleanFeatureFlagForUserHandler() { user := factory.BuildDefaultUser(suite.DB()) diff --git a/pkg/handlers/internalapi/internal/payloads/model_to_payload.go b/pkg/handlers/internalapi/internal/payloads/model_to_payload.go index 26b25349e02..01db27b3a99 100644 --- a/pkg/handlers/internalapi/internal/payloads/model_to_payload.go +++ b/pkg/handlers/internalapi/internal/payloads/model_to_payload.go @@ -113,6 +113,7 @@ func PPMShipment(storer storage.FileStorer, ppmShipment *models.PPMShipment) *in payloadPPMShipment := &internalmessages.PPMShipment{ ID: *handlers.FmtUUID(ppmShipment.ID), + PpmType: internalmessages.PPMType(ppmShipment.PPMType), ShipmentID: *handlers.FmtUUID(ppmShipment.ShipmentID), CreatedAt: strfmt.DateTime(ppmShipment.CreatedAt), UpdatedAt: strfmt.DateTime(ppmShipment.UpdatedAt), diff --git a/pkg/handlers/internalapi/internal/payloads/model_to_payload_test.go b/pkg/handlers/internalapi/internal/payloads/model_to_payload_test.go index 5737d25db0d..834d3b7819f 100644 --- a/pkg/handlers/internalapi/internal/payloads/model_to_payload_test.go +++ b/pkg/handlers/internalapi/internal/payloads/model_to_payload_test.go @@ -42,6 +42,7 @@ func (suite *PayloadsSuite) TestFetchPPMShipment() { expectedPPMShipment := models.PPMShipment{ ID: ppmShipmentID, + PPMType: models.PPMTypeActualExpense, PickupAddress: &expectedAddress, DestinationAddress: &expectedAddress, IsActualExpenseReimbursement: &isActualExpenseReimbursement, @@ -69,6 +70,7 @@ func (suite *PayloadsSuite) TestFetchPPMShipment() { suite.Equal(&country.Country, returnedPPMShipment.DestinationAddress.Country) suite.Equal(&county, returnedPPMShipment.DestinationAddress.County) + suite.Equal(internalmessages.PPMType(models.PPMTypeActualExpense), returnedPPMShipment.PpmType) suite.True(*returnedPPMShipment.IsActualExpenseReimbursement) }) } diff --git a/pkg/handlers/internalapi/internal/payloads/payload_to_model.go b/pkg/handlers/internalapi/internal/payloads/payload_to_model.go index 37cd06f5811..3b2186793a5 100644 --- a/pkg/handlers/internalapi/internal/payloads/payload_to_model.go +++ b/pkg/handlers/internalapi/internal/payloads/payload_to_model.go @@ -187,6 +187,7 @@ func PPMShipmentModelFromCreate(ppmShipment *internalmessages.CreatePPMShipment) } model := &models.PPMShipment{ + PPMType: models.PPMType(ppmShipment.PpmType), SITExpected: ppmShipment.SitExpected, ExpectedDepartureDate: handlers.FmtDatePtrToPop(ppmShipment.ExpectedDepartureDate), } @@ -232,6 +233,7 @@ func UpdatePPMShipmentModel(ppmShipment *internalmessages.UpdatePPMShipment) *mo } ppmModel := &models.PPMShipment{ + PPMType: models.PPMType(ppmShipment.PpmType), ActualMoveDate: (*time.Time)(ppmShipment.ActualMoveDate), ActualPickupPostalCode: ppmShipment.ActualPickupPostalCode, ActualDestinationPostalCode: ppmShipment.ActualDestinationPostalCode, diff --git a/pkg/handlers/internalapi/internal/payloads/payload_to_model_test.go b/pkg/handlers/internalapi/internal/payloads/payload_to_model_test.go index 123a5494b46..21630911f5b 100644 --- a/pkg/handlers/internalapi/internal/payloads/payload_to_model_test.go +++ b/pkg/handlers/internalapi/internal/payloads/payload_to_model_test.go @@ -288,6 +288,7 @@ func (suite *PayloadsSuite) TestPPMShipmentModelFromUpdate() { } ppmShipment := internalmessages.UpdatePPMShipment{ + PpmType: internalmessages.PPMType(models.PPMTypeActualExpense), ExpectedDepartureDate: expectedDepartureDate, PickupAddress: &pickupAddress, SecondaryPickupAddress: &secondaryPickupAddress, @@ -317,6 +318,7 @@ func (suite *PayloadsSuite) TestPPMShipmentModelFromUpdate() { suite.Nil(model.HasTertiaryDestinationAddress) suite.True(*model.IsActualExpenseReimbursement) suite.NotNil(model) + suite.Equal(model.PPMType, models.PPMTypeActualExpense) } func (suite *PayloadsSuite) TestPPMShipmentModelWithOptionalDestinationStreet1FromCreate() { diff --git a/pkg/handlers/internalapi/orders.go b/pkg/handlers/internalapi/orders.go index 3471329d227..955743cdbe8 100644 --- a/pkg/handlers/internalapi/orders.go +++ b/pkg/handlers/internalapi/orders.go @@ -571,26 +571,47 @@ func (h UpdateOrdersHandler) Handle(params ordersop.UpdateOrdersParams) middlewa order.Entitlement = &entitlement // change actual expense reimbursement to 'true' for all PPM shipments if pay grade is civilian - if payload.Grade != nil && *payload.Grade == models.ServiceMemberGradeCIVILIANEMPLOYEE { + // if not, do the opposite and make the PPM type INCENTIVE_BASED + if payload.Grade != nil && *payload.Grade != *order.Grade { moves, fetchErr := models.FetchMovesByOrderID(appCtx.DB(), order.ID) if fetchErr != nil { appCtx.Logger().Error("failure encountered querying for move associated with the order", zap.Error(fetchErr)) } else { - move := moves[0] - for i := range move.MTOShipments { - shipment := &move.MTOShipments[i] - - if shipment.ShipmentType == models.MTOShipmentTypePPM { - if shipment.PPMShipment == nil { - appCtx.Logger().Warn("PPM shipment not found for MTO shipment", zap.String("shipmentID", shipment.ID.String())) - continue - } - // actual expense reimbursement is always true for civilian moves - shipment.PPMShipment.IsActualExpenseReimbursement = models.BoolPointer(true) + var move *models.Move + for i := range moves { + if moves[i].OrdersID == order.ID { + move = &moves[i] + break + } + } + if move == nil { + appCtx.Logger().Error("no move found matching order ID", zap.String("orderID", order.ID.String())) + } else { + // look at the values and see if the grade is CIVILIAN_EMPLOYEE + isCivilian := *payload.Grade == models.ServiceMemberGradeCIVILIANEMPLOYEE + reimbursementVal := isCivilian + var ppmType models.PPMType + // setting the default ppmType + if isCivilian { + ppmType = models.PPMTypeActualExpense + } else { + ppmType = models.PPMTypeIncentiveBased + } - if verrs, err := appCtx.DB().ValidateAndUpdate(shipment.PPMShipment); verrs.HasAny() || err != nil { - msg := "failure saving PPM shipment when updating orders" - appCtx.Logger().Error(msg, zap.Error(err)) + for i := range move.MTOShipments { + shipment := &move.MTOShipments[i] + if shipment.ShipmentType == models.MTOShipmentTypePPM { + if shipment.PPMShipment == nil { + appCtx.Logger().Warn("PPM shipment not found for MTO shipment", zap.String("shipmentID", shipment.ID.String())) + continue + } + shipment.PPMShipment.IsActualExpenseReimbursement = models.BoolPointer(reimbursementVal) + shipment.PPMShipment.PPMType = ppmType + + if verrs, err := appCtx.DB().ValidateAndUpdate(shipment.PPMShipment); verrs.HasAny() || err != nil { + msg := "failure saving PPM shipment when updating orders" + appCtx.Logger().Error(msg, zap.Error(err)) + } } } } diff --git a/pkg/handlers/internalapi/orders_test.go b/pkg/handlers/internalapi/orders_test.go index 5fa545fe4a2..77bd2247c84 100644 --- a/pkg/handlers/internalapi/orders_test.go +++ b/pkg/handlers/internalapi/orders_test.go @@ -985,6 +985,150 @@ func (suite *HandlerSuite) TestUpdateOrdersHandler() { suite.NotNil(updatedEntitlement.DependentsUnderTwelve) }) + suite.Run("Updating order grade to civilian changes PPM type to ACTUAL_EXPENSE", func() { + order := factory.BuildOrder(suite.DB(), []factory.Customization{ + { + Model: models.Order{ + Grade: models.ServiceMemberGradeE7.Pointer(), + }, + }, + }, nil) + + ppmShipment := factory.BuildPPMShipment(suite.DB(), []factory.Customization{ + { + Model: order, + LinkOnly: true, + }, + { + Model: models.PPMShipment{ + PPMType: models.PPMTypeIncentiveBased, + Status: models.PPMShipmentStatusDraft, + }, + }, + }, nil) + + newDutyLocation := factory.BuildDutyLocation(suite.DB(), nil, nil) + newOrdersType := internalmessages.OrdersTypePERMANENTCHANGEOFSTATION + newOrdersNumber := "123456" + issueDate := time.Date(2018, time.March, 10, 0, 0, 0, 0, time.UTC) + reportByDate := time.Date(2018, time.August, 1, 0, 0, 0, 0, time.UTC) + deptIndicator := internalmessages.DeptIndicatorARMY + payload := &internalmessages.CreateUpdateOrders{ + OrdersNumber: handlers.FmtString(newOrdersNumber), + OrdersType: &newOrdersType, + NewDutyLocationID: handlers.FmtUUID(newDutyLocation.ID), + OriginDutyLocationID: *handlers.FmtUUID(*order.OriginDutyLocationID), + IssueDate: handlers.FmtDate(issueDate), + ReportByDate: handlers.FmtDate(reportByDate), + DepartmentIndicator: &deptIndicator, + HasDependents: handlers.FmtBool(false), + SpouseHasProGear: handlers.FmtBool(false), + Grade: models.ServiceMemberGradeCIVILIANEMPLOYEE.Pointer(), + MoveID: *handlers.FmtUUID(ppmShipment.Shipment.MoveTaskOrderID), + CounselingOfficeID: handlers.FmtUUID(*newDutyLocation.TransportationOfficeID), + ServiceMemberID: handlers.FmtUUID(order.ServiceMemberID), + } + + path := fmt.Sprintf("/orders/%v", order.ID.String()) + req := httptest.NewRequest("PUT", path, nil) + req = suite.AuthenticateRequest(req, order.ServiceMember) + + params := ordersop.UpdateOrdersParams{ + HTTPRequest: req, + OrdersID: *handlers.FmtUUID(order.ID), + UpdateOrders: payload, + } + + fakeS3 := storageTest.NewFakeS3Storage(true) + handlerConfig := suite.HandlerConfig() + handlerConfig.SetFileStorer(fakeS3) + + handler := UpdateOrdersHandler{handlerConfig} + + response := handler.Handle(params) + + suite.IsType(&ordersop.UpdateOrdersOK{}, response) + okResponse := response.(*ordersop.UpdateOrdersOK) + suite.NoError(okResponse.Payload.Validate(strfmt.Default)) + + updatedPPM, err := models.FetchPPMShipmentByPPMShipmentID(suite.DB(), ppmShipment.ID) + suite.NoError(err) + suite.Equal(updatedPPM.PPMType, models.PPMTypeActualExpense) + suite.True(*updatedPPM.IsActualExpenseReimbursement) + }) + + suite.Run("Updating order grade FROM civilian to non-civilian changes PPM type to INCENTIVE_BASED", func() { + order := factory.BuildOrder(suite.DB(), []factory.Customization{ + { + Model: models.Order{ + Grade: models.ServiceMemberGradeCIVILIANEMPLOYEE.Pointer(), + }, + }, + }, nil) + + ppmShipment := factory.BuildPPMShipment(suite.DB(), []factory.Customization{ + { + Model: order, + LinkOnly: true, + }, + { + Model: models.PPMShipment{ + PPMType: models.PPMTypeActualExpense, + Status: models.PPMShipmentStatusDraft, + }, + }, + }, nil) + + newDutyLocation := factory.BuildDutyLocation(suite.DB(), nil, nil) + newOrdersType := internalmessages.OrdersTypePERMANENTCHANGEOFSTATION + newOrdersNumber := "123456" + issueDate := time.Date(2018, time.March, 10, 0, 0, 0, 0, time.UTC) + reportByDate := time.Date(2018, time.August, 1, 0, 0, 0, 0, time.UTC) + deptIndicator := internalmessages.DeptIndicatorARMY + payload := &internalmessages.CreateUpdateOrders{ + OrdersNumber: handlers.FmtString(newOrdersNumber), + OrdersType: &newOrdersType, + NewDutyLocationID: handlers.FmtUUID(newDutyLocation.ID), + OriginDutyLocationID: *handlers.FmtUUID(*order.OriginDutyLocationID), + IssueDate: handlers.FmtDate(issueDate), + ReportByDate: handlers.FmtDate(reportByDate), + DepartmentIndicator: &deptIndicator, + HasDependents: handlers.FmtBool(false), + SpouseHasProGear: handlers.FmtBool(false), + Grade: models.ServiceMemberGradeE7.Pointer(), + MoveID: *handlers.FmtUUID(ppmShipment.Shipment.MoveTaskOrderID), + CounselingOfficeID: handlers.FmtUUID(*newDutyLocation.TransportationOfficeID), + ServiceMemberID: handlers.FmtUUID(order.ServiceMemberID), + } + + path := fmt.Sprintf("/orders/%v", order.ID.String()) + req := httptest.NewRequest("PUT", path, nil) + req = suite.AuthenticateRequest(req, order.ServiceMember) + + params := ordersop.UpdateOrdersParams{ + HTTPRequest: req, + OrdersID: *handlers.FmtUUID(order.ID), + UpdateOrders: payload, + } + + fakeS3 := storageTest.NewFakeS3Storage(true) + handlerConfig := suite.HandlerConfig() + handlerConfig.SetFileStorer(fakeS3) + + handler := UpdateOrdersHandler{handlerConfig} + + response := handler.Handle(params) + + suite.IsType(&ordersop.UpdateOrdersOK{}, response) + okResponse := response.(*ordersop.UpdateOrdersOK) + suite.NoError(okResponse.Payload.Validate(strfmt.Default)) + + updatedPPM, err := models.FetchPPMShipmentByPPMShipmentID(suite.DB(), ppmShipment.ID) + suite.NoError(err) + suite.Equal(updatedPPM.PPMType, models.PPMTypeIncentiveBased) + suite.False(*updatedPPM.IsActualExpenseReimbursement) + }) + } func (suite *HandlerSuite) TestUpdateOrdersHandlerOriginPostalCodeAndGBLOC() { @@ -1044,6 +1188,12 @@ func (suite *HandlerSuite) TestUpdateOrdersHandlerOriginPostalCodeAndGBLOC() { }, }, nil) + factory.BuildMove(suite.DB(), []factory.Customization{ + { + Model: order, + LinkOnly: true, + }}, nil) + fetchedOrder, err := models.FetchOrder(suite.DB(), order.ID) suite.NoError(err) @@ -1187,8 +1337,6 @@ func (suite *HandlerSuite) TestUpdateOrdersHandlerWithCounselingOffice() { }, nil) newDutyLocation := factory.BuildDutyLocation(suite.DB(), nil, nil) - newTransportationOffice := factory.BuildTransportationOffice(suite.DB(), nil, nil) - newDutyLocation.TransportationOffice = newTransportationOffice newOrdersType := internalmessages.OrdersTypePERMANENTCHANGEOFSTATION newOrdersNumber := "123456" diff --git a/pkg/handlers/routing/routing_init.go b/pkg/handlers/routing/routing_init.go index 0abd6d434ca..f91fa88506a 100644 --- a/pkg/handlers/routing/routing_init.go +++ b/pkg/handlers/routing/routing_init.go @@ -495,6 +495,9 @@ func mountInternalAPI(appCtx appcontext.AppContext, routingConfig *Config, site rAuth.Use(customerAPIAuthMiddleware) tracingMiddleware := middleware.OpenAPITracing(api) rAuth.Mount("/", api.Serve(tracingMiddleware)) + r.Route("/open", func(rOpen chi.Router) { + rOpen.Mount("/", api.Serve(tracingMiddleware)) + }) }) }) } diff --git a/pkg/models/worksheet_shipment.go b/pkg/models/worksheet_shipment.go index 3fd8a5ea62c..b39a23e2455 100644 --- a/pkg/models/worksheet_shipment.go +++ b/pkg/models/worksheet_shipment.go @@ -72,4 +72,5 @@ type ShipmentSummaryFormData struct { SignedCertifications []*SignedCertification MaxSITStorageEntitlement int IsActualExpenseReimbursement bool + IsSmallPackageReimbursement bool } diff --git a/pkg/services/mto_service_item/mto_service_item_creator.go b/pkg/services/mto_service_item/mto_service_item_creator.go index f7c2fd3730b..ab587ece91f 100644 --- a/pkg/services/mto_service_item/mto_service_item_creator.go +++ b/pkg/services/mto_service_item/mto_service_item_creator.go @@ -637,7 +637,6 @@ func (o *mtoServiceItemCreator) CreateMTOServiceItem(appCtx appcontext.AppContex // if estimated weight for shipment provided by the prime, calculate the estimated prices for // DLH, DPK, DOP, DDP, DUPK - // NTS-release requested pickup dates are for handle out, their pricing is handled differently as their locations are based on storage facilities, not pickup locations if mtoShipment.PrimeEstimatedWeight != nil && mtoShipment.RequestedPickupDate != nil { serviceItemEstimatedPrice, err := o.FindEstimatedPrice(appCtx, serviceItem, mtoShipment) if serviceItemEstimatedPrice != 0 && err == nil { diff --git a/pkg/services/order/order_updater.go b/pkg/services/order/order_updater.go index 1338dc5c2e2..b9933335588 100644 --- a/pkg/services/order/order_updater.go +++ b/pkg/services/order/order_updater.go @@ -847,25 +847,46 @@ func updateOrderInTx(appCtx appcontext.AppContext, order models.Order, checks .. } // change actual expense reimbursement to 'true' for all PPM shipments if pay grade is civilian + // if not, do the opposite and make the PPM type INCENTIVE_BASED if order.Grade != nil && *order.Grade == models.ServiceMemberGradeCIVILIANEMPLOYEE { moves, fetchErr := models.FetchMovesByOrderID(appCtx.DB(), order.ID) - if fetchErr != nil || len(moves) == 0 { - appCtx.Logger().Error("failure encountered querying for move associated with the order", zap.Error(err)) + if fetchErr != nil { + appCtx.Logger().Error("failure encountered querying for move associated with the order", zap.Error(fetchErr)) } else { - move := moves[0] - for i := range move.MTOShipments { - shipment := &move.MTOShipments[i] - - if shipment.ShipmentType == models.MTOShipmentTypePPM { - if shipment.PPMShipment == nil { - appCtx.Logger().Warn("PPM shipment not found for MTO shipment", zap.String("shipmentID", shipment.ID.String())) - continue - } - shipment.PPMShipment.IsActualExpenseReimbursement = models.BoolPointer(true) + var move *models.Move + for i := range moves { + if moves[i].OrdersID == order.ID { + move = &moves[i] + break + } + } + if move == nil { + appCtx.Logger().Error("no move found matching order ID", zap.String("orderID", order.ID.String())) + } else { + // look at the values and see if the grade is CIVILIAN_EMPLOYEE + isCivilian := *order.Grade == models.ServiceMemberGradeCIVILIANEMPLOYEE + reimbursementVal := isCivilian + var ppmType models.PPMType + if isCivilian { + ppmType = models.PPMTypeActualExpense + } else { + ppmType = models.PPMTypeIncentiveBased + } - if verrs, err := appCtx.DB().ValidateAndUpdate(shipment.PPMShipment); verrs.HasAny() || err != nil { - msg := "failure saving PPM shipment when updating orders" - appCtx.Logger().Error(msg, zap.Error(err)) + for i := range move.MTOShipments { + shipment := &move.MTOShipments[i] + if shipment.ShipmentType == models.MTOShipmentTypePPM && shipment.Status == models.MTOShipmentStatusSubmitted && shipment.PPMShipment.PPMType == models.PPMTypeIncentiveBased { + if shipment.PPMShipment == nil { + appCtx.Logger().Warn("PPM shipment not found for MTO shipment", zap.String("shipmentID", shipment.ID.String())) + continue + } + shipment.PPMShipment.IsActualExpenseReimbursement = models.BoolPointer(reimbursementVal) + shipment.PPMShipment.PPMType = ppmType + + if verrs, err := appCtx.DB().ValidateAndUpdate(shipment.PPMShipment); verrs.HasAny() || err != nil { + msg := "failure saving PPM shipment when updating orders" + appCtx.Logger().Error(msg, zap.Error(err)) + } } } } diff --git a/pkg/services/order/order_updater_test.go b/pkg/services/order/order_updater_test.go index fe91426d7f6..337e7fbe1e5 100644 --- a/pkg/services/order/order_updater_test.go +++ b/pkg/services/order/order_updater_test.go @@ -485,7 +485,14 @@ func (suite *OrderServiceSuite) TestUpdateOrderAsCounselor() { moveRouter := move.NewMoveRouter(transportationoffice.NewTransportationOfficesFetcher()) orderUpdater := NewOrderUpdater(moveRouter) - ppmShipment := factory.BuildPPMShipmentThatNeedsCloseout(suite.DB(), nil, nil) + ppmShipment := factory.BuildPPMShipment(suite.DB(), []factory.Customization{ + { + Model: models.PPMShipment{ + PPMType: models.PPMTypeIncentiveBased, + Status: models.PPMShipmentStatusSubmitted, + }, + }, + }, nil) move := ppmShipment.Shipment.MoveTaskOrder order := move.Orders @@ -541,6 +548,41 @@ func (suite *OrderServiceSuite) TestUpdateOrderAsCounselor() { suite.Nil(updatedOrder) suite.IsType(apperror.InvalidInputError{}, err) }) + + suite.Run("Updating order grade to civilian changes submitted PPMs to PPM type ACTUAL_EXPENSE", func() { + moveRouter := move.NewMoveRouter(transportationoffice.NewTransportationOfficesFetcher()) + orderUpdater := NewOrderUpdater(moveRouter) + ppmShipment := factory.BuildPPMShipment(suite.DB(), []factory.Customization{ + { + Model: models.PPMShipment{ + PPMType: models.PPMTypeIncentiveBased, + Status: models.PPMShipmentStatusSubmitted, + }, + }, + }, nil) + move := ppmShipment.Shipment.MoveTaskOrder + order := move.Orders + + grade := ghcmessages.GradeCIVILIANEMPLOYEE + body := ghcmessages.CounselingUpdateOrderPayload{ + Grade: &grade, + } + eTag := etag.GenerateEtag(order.UpdatedAt) + + var moved models.Move + err := suite.DB().Find(&moved, move.ID) + suite.NoError(err) + + _, _, errs := orderUpdater.UpdateOrderAsCounselor(suite.AppContextForTest(), order.ID, body, eTag) + suite.NoError(errs) + + var updatedPPMShipment models.PPMShipment + err = suite.DB().Find(&updatedPPMShipment, ppmShipment.ID) + + suite.NoError(err) + suite.EqualValues(true, *updatedPPMShipment.IsActualExpenseReimbursement) + suite.Equal(updatedPPMShipment.PPMType, models.PPMTypeActualExpense) + }) } func (suite *OrderServiceSuite) TestUpdateAllowanceAsTOO() { diff --git a/pkg/services/ppmshipment/validation.go b/pkg/services/ppmshipment/validation.go index f2c27a597b6..17c9f20f165 100644 --- a/pkg/services/ppmshipment/validation.go +++ b/pkg/services/ppmshipment/validation.go @@ -66,6 +66,10 @@ func mergePPMShipment(newPPMShipment models.PPMShipment, oldPPMShipment *models. ppmShipment := *oldPPMShipment + if newPPMShipment.PPMType != "" { + ppmShipment.PPMType = newPPMShipment.PPMType + } + today := time.Now() if newPPMShipment.ActualMoveDate != nil && today.Before(*newPPMShipment.ActualMoveDate) { err = apperror.NewUpdateError(ppmShipment.ID, "Actual move date cannot be set to the future.") diff --git a/pkg/services/shipment_summary_worksheet.go b/pkg/services/shipment_summary_worksheet.go index d2bd339901c..85002c96611 100644 --- a/pkg/services/shipment_summary_worksheet.go +++ b/pkg/services/shipment_summary_worksheet.go @@ -56,50 +56,53 @@ type Page1Values struct { MileageTotal string MailingAddressW2 string IsActualExpenseReimbursement bool - GCCIsActualExpenseReimbursement string + IsSmallPackageReimbursement bool + GCCExpenseReimbursementType string } // Page2Values is an object representing a Shipment Summary Worksheet type Page2Values struct { - CUIBanner string - PreparationDate2 string - TAC string - SAC string - ContractedExpenseMemberPaid string - ContractedExpenseGTCCPaid string - RentalEquipmentMemberPaid string - RentalEquipmentGTCCPaid string - PackingMaterialsMemberPaid string - PackingMaterialsGTCCPaid string - WeighingFeesMemberPaid string - WeighingFeesGTCCPaid string - GasMemberPaid string - GasGTCCPaid string - TollsMemberPaid string - TollsGTCCPaid string - OilMemberPaid string - OilGTCCPaid string - OtherMemberPaid string - OtherGTCCPaid string - TotalMemberPaid string - TotalGTCCPaid string - TotalMemberPaidRepeated string - TotalGTCCPaidRepeated string - TotalPaidNonSIT string - TotalMemberPaidSIT string - TotalGTCCPaidSIT string - TotalPaidSIT string - Disbursement string - ShipmentPickupDates string - TrustedAgentName string - ServiceMemberSignature string - PPPOPPSORepresentative string - SignatureDate string - PPMRemainingEntitlement string + CUIBanner string + PreparationDate2 string + TAC string + SAC string + ContractedExpenseMemberPaid string + ContractedExpenseGTCCPaid string + RentalEquipmentMemberPaid string + RentalEquipmentGTCCPaid string + PackingMaterialsMemberPaid string + PackingMaterialsGTCCPaid string + SmallPackageExpenseMemberPaid string + SmallPackageExpenseGTCCPaid string + WeighingFeesMemberPaid string + WeighingFeesGTCCPaid string + GasMemberPaid string + GasGTCCPaid string + TollsMemberPaid string + TollsGTCCPaid string + OilMemberPaid string + OilGTCCPaid string + OtherMemberPaid string + OtherGTCCPaid string + TotalMemberPaid string + TotalGTCCPaid string + TotalMemberPaidRepeated string + TotalGTCCPaidRepeated string + TotalPaidNonSIT string + TotalMemberPaidSIT string + TotalGTCCPaidSIT string + TotalPaidSIT string + Disbursement string + ShipmentPickupDates string + TrustedAgentName string + ServiceMemberSignature string + PPPOPPSORepresentative string + SignatureDate string + PPMRemainingEntitlement string FormattedMovingExpenses FormattedOtherExpenses - IncentiveIsActualExpenseReimbursement string - HeaderIsActualExpenseReimbursement string + IncentiveExpenseReimbursementType string + HeaderExpenseReimbursementType string } // Page3Values is an object representing a Shipment Summary Worksheet @@ -117,30 +120,32 @@ type FormattedOtherExpenses struct { // FormattedMovingExpenses is an object representing the service member's moving expenses formatted for the SSW type FormattedMovingExpenses struct { - ContractedExpenseMemberPaid string - ContractedExpenseGTCCPaid string - RentalEquipmentMemberPaid string - RentalEquipmentGTCCPaid string - PackingMaterialsMemberPaid string - PackingMaterialsGTCCPaid string - WeighingFeesMemberPaid string - WeighingFeesGTCCPaid string - GasMemberPaid string - GasGTCCPaid string - TollsMemberPaid string - TollsGTCCPaid string - OilMemberPaid string - OilGTCCPaid string - OtherMemberPaid string - OtherGTCCPaid string - TotalMemberPaid string - TotalGTCCPaid string - TotalMemberPaidRepeated string - TotalGTCCPaidRepeated string - TotalPaidNonSIT string - TotalMemberPaidSIT string - TotalGTCCPaidSIT string - TotalPaidSIT string + ContractedExpenseMemberPaid string + ContractedExpenseGTCCPaid string + RentalEquipmentMemberPaid string + RentalEquipmentGTCCPaid string + PackingMaterialsMemberPaid string + PackingMaterialsGTCCPaid string + SmallPackageExpenseMemberPaid string + SmallPackageExpenseGTCCPaid string + WeighingFeesMemberPaid string + WeighingFeesGTCCPaid string + GasMemberPaid string + GasGTCCPaid string + TollsMemberPaid string + TollsGTCCPaid string + OilMemberPaid string + OilGTCCPaid string + OtherMemberPaid string + OtherGTCCPaid string + TotalMemberPaid string + TotalGTCCPaid string + TotalMemberPaidRepeated string + TotalGTCCPaidRepeated string + TotalPaidNonSIT string + TotalMemberPaidSIT string + TotalGTCCPaidSIT string + TotalPaidSIT string } //go:generate mockery --name SSWPPMComputer diff --git a/pkg/services/shipment_summary_worksheet/shipment_summary_worksheet.go b/pkg/services/shipment_summary_worksheet/shipment_summary_worksheet.go index abd25465c44..fd546e341c6 100644 --- a/pkg/services/shipment_summary_worksheet/shipment_summary_worksheet.go +++ b/pkg/services/shipment_summary_worksheet/shipment_summary_worksheet.go @@ -306,7 +306,12 @@ func (s SSWPPMComputer) FormatValuesShipmentSummaryWorksheetFormPage1(data model // Fill out form fields related to Actual Expense Reimbursement status if data.PPMShipment.IsActualExpenseReimbursement != nil && *data.PPMShipment.IsActualExpenseReimbursement { page1.IsActualExpenseReimbursement = *data.PPMShipment.IsActualExpenseReimbursement - page1.GCCIsActualExpenseReimbursement = "Actual Expense Reimbursement" + page1.GCCExpenseReimbursementType = "Actual Expense Reimbursement" + } + + if data.PPMShipment.PPMType == models.PPMTypeSmallPackage { + page1.IsSmallPackageReimbursement = true + page1.GCCExpenseReimbursementType = "Small Package Reimbursement" } page1.SITDaysInStorage = formattedSIT.DaysInStorage @@ -373,6 +378,8 @@ func (s *SSWPPMComputer) FormatValuesShipmentSummaryWorksheetFormPage2(data mode page2.WeighingFeesGTCCPaid = FormatDollars(expensesMap["WeighingFeeGTCCPaid"]) page2.RentalEquipmentMemberPaid = FormatDollars(expensesMap["RentalEquipmentMemberPaid"]) page2.RentalEquipmentGTCCPaid = FormatDollars(expensesMap["RentalEquipmentGTCCPaid"]) + page2.SmallPackageExpenseMemberPaid = FormatDollars(expensesMap["SmallPackageExpenseMemberPaid"]) + page2.SmallPackageExpenseGTCCPaid = FormatDollars(expensesMap["SmallPackageExpenseGTCCPaid"]) page2.TollsMemberPaid = FormatDollars(expensesMap["TollsMemberPaid"]) page2.TollsGTCCPaid = FormatDollars(expensesMap["TollsGTCCPaid"]) page2.OilMemberPaid = FormatDollars(expensesMap["OilMemberPaid"]) @@ -394,8 +401,14 @@ func (s *SSWPPMComputer) FormatValuesShipmentSummaryWorksheetFormPage2(data mode page2.SignatureDate = certificationInfo.DateField if data.PPMShipment.IsActualExpenseReimbursement != nil && *data.PPMShipment.IsActualExpenseReimbursement { - page2.IncentiveIsActualExpenseReimbursement = "Actual Expense Reimbursement" - page2.HeaderIsActualExpenseReimbursement = `This PPM is being processed at actual expense reimbursement for valid expenses not to exceed the + page2.IncentiveExpenseReimbursementType = "Actual Expense Reimbursement" + page2.HeaderExpenseReimbursementType = `This PPM is being processed as actual expense reimbursement for valid expenses not to exceed the + government constructed cost (GCC).` + } + + if data.PPMShipment.PPMType == models.PPMTypeSmallPackage { + page2.IncentiveExpenseReimbursementType = "Small Package Reimbursement" + page2.HeaderExpenseReimbursementType = `This PPM is being processed as small package reimbursement for valid expenses not to exceed the government constructed cost (GCC).` } @@ -1154,6 +1167,11 @@ func (SSWPPMComputer *SSWPPMComputer) FetchDataShipmentSummaryWorksheetFormData( isActualExpenseReimbursement = true } + isSmallPackageReimbursement := false + if ppmShipment.PPMType == models.PPMTypeSmallPackage { + isSmallPackageReimbursement = true + } + ssd := models.ShipmentSummaryFormData{ AllShipments: ppmShipment.Shipment.MoveTaskOrder.MTOShipments, ServiceMember: serviceMember, @@ -1170,6 +1188,7 @@ func (SSWPPMComputer *SSWPPMComputer) FetchDataShipmentSummaryWorksheetFormData( SignedCertifications: signedCertifications, MaxSITStorageEntitlement: maxSit, IsActualExpenseReimbursement: isActualExpenseReimbursement, + IsSmallPackageReimbursement: isSmallPackageReimbursement, } return &ssd, nil } @@ -1249,6 +1268,11 @@ func (SSWPPMGenerator *SSWPPMGenerator) FillSSWPDFForm(Page1Values services.Page isActualExpenseReimbursement = true } + isSmallPackageReimbursement := false + if Page1Values.IsSmallPackageReimbursement { + isSmallPackageReimbursement = true + } + var sswCheckbox = []checkbox{ { Pages: []int{2}, @@ -1266,6 +1290,14 @@ func (SSWPPMGenerator *SSWPPMGenerator) FillSSWPDFForm(Page1Values services.Page Default: isActualExpenseReimbursement, Locked: false, }, + { + Pages: []int{1}, + ID: "555", + Name: "IsSmallPackageReimbursement", + Value: true, + Default: isSmallPackageReimbursement, + Locked: false, + }, } formData := pdFData{ // This is unique to each PDF template, must be found for new templates using PDFCPU's export function used on the template (can be done through CLI) 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 a3bd17fa425..b8208c450b2 100644 --- a/pkg/services/shipment_summary_worksheet/shipment_summary_worksheet_test.go +++ b/pkg/services/shipment_summary_worksheet/shipment_summary_worksheet_test.go @@ -290,226 +290,451 @@ func (suite *ShipmentSummaryWorksheetServiceSuite) TestFetchDataShipmentSummaryW } func (suite *ShipmentSummaryWorksheetServiceSuite) TestFormatValuesShipmentSummaryWorksheetFormPage1() { - yuma := factory.FetchOrBuildCurrentDutyLocation(suite.DB()) - fortGordon := factory.FetchOrBuildOrdersDutyLocation(suite.DB()) - wtgEntitlements := models.SSWMaxWeightEntitlement{ - Entitlement: 15000, - ProGear: 2000, - SpouseProGear: 500, - TotalWeight: 17500, - } + suite.Run("PPM Type Actual Expense Reimbursement - Success", func() { + yuma := factory.FetchOrBuildCurrentDutyLocation(suite.DB()) + fortGordon := factory.FetchOrBuildOrdersDutyLocation(suite.DB()) + wtgEntitlements := models.SSWMaxWeightEntitlement{ + Entitlement: 15000, + ProGear: 2000, + SpouseProGear: 500, + TotalWeight: 17500, + } - serviceMemberID, _ := uuid.NewV4() - serviceBranch := models.AffiliationAIRFORCE - grade := models.ServiceMemberGradeE9 - serviceMember := models.ServiceMember{ - ID: serviceMemberID, - FirstName: models.StringPointer("Marcus"), - MiddleName: models.StringPointer("Joseph"), - LastName: models.StringPointer("Jenkins"), - Suffix: models.StringPointer("Jr."), - Telephone: models.StringPointer("444-555-8888"), - PersonalEmail: models.StringPointer("michael+ppm-expansion_1@truss.works"), - Edipi: models.StringPointer("1234567890"), - Affiliation: &serviceBranch, - } + serviceMemberID, _ := uuid.NewV4() + serviceBranch := models.AffiliationAIRFORCE + grade := models.ServiceMemberGradeE9 + serviceMember := models.ServiceMember{ + ID: serviceMemberID, + FirstName: models.StringPointer("Marcus"), + MiddleName: models.StringPointer("Joseph"), + LastName: models.StringPointer("Jenkins"), + Suffix: models.StringPointer("Jr."), + Telephone: models.StringPointer("444-555-8888"), + PersonalEmail: models.StringPointer("michael+ppm-expansion_1@truss.works"), + Edipi: models.StringPointer("1234567890"), + Affiliation: &serviceBranch, + } - orderIssueDate := time.Date(2018, time.December, 21, 0, 0, 0, 0, time.UTC) - order := models.Order{ - IssueDate: orderIssueDate, - OrdersType: internalmessages.OrdersTypePERMANENTCHANGEOFSTATION, - OrdersNumber: models.StringPointer("012345"), - NewDutyLocationID: fortGordon.ID, - TAC: models.StringPointer("NTA4"), - SAC: models.StringPointer("SAC"), - HasDependents: true, - SpouseHasProGear: true, - Grade: &grade, - } - expectedPickupDate := time.Date(2019, time.January, 11, 0, 0, 0, 0, time.UTC) - actualPickupDate := time.Date(2019, time.February, 11, 0, 0, 0, 0, time.UTC) - netWeight := unit.Pound(4000) - cents := unit.Cents(1000) - locator := "ABCDEF-01" - estIncentive := unit.Cents(1000000) - maxIncentive := unit.Cents(2000000) - PPMShipments := models.PPMShipment{ - ExpectedDepartureDate: expectedPickupDate, - ActualMoveDate: &actualPickupDate, - Status: models.PPMShipmentStatusWaitingOnCustomer, - EstimatedWeight: &netWeight, - AdvanceAmountRequested: ¢s, - EstimatedIncentive: &estIncentive, - MaxIncentive: &maxIncentive, - Shipment: models.MTOShipment{ - ShipmentLocator: &locator, - }, - IsActualExpenseReimbursement: models.BoolPointer(true), - } - ssd := models.ShipmentSummaryFormData{ - ServiceMember: serviceMember, - Order: order, - CurrentDutyLocation: yuma, - NewDutyLocation: fortGordon, - PPMRemainingEntitlement: 3000, - WeightAllotment: wtgEntitlements, - PreparationDate: time.Date(2019, 1, 1, 1, 1, 1, 1, time.UTC), - PPMShipment: PPMShipments, - } + orderIssueDate := time.Date(2018, time.December, 21, 0, 0, 0, 0, time.UTC) + order := models.Order{ + IssueDate: orderIssueDate, + OrdersType: internalmessages.OrdersTypePERMANENTCHANGEOFSTATION, + OrdersNumber: models.StringPointer("012345"), + NewDutyLocationID: fortGordon.ID, + TAC: models.StringPointer("NTA4"), + SAC: models.StringPointer("SAC"), + HasDependents: true, + SpouseHasProGear: true, + Grade: &grade, + } + expectedPickupDate := time.Date(2019, time.January, 11, 0, 0, 0, 0, time.UTC) + actualPickupDate := time.Date(2019, time.February, 11, 0, 0, 0, 0, time.UTC) + netWeight := unit.Pound(4000) + cents := unit.Cents(1000) + locator := "ABCDEF-01" + estIncentive := unit.Cents(1000000) + maxIncentive := unit.Cents(2000000) + PPMShipments := models.PPMShipment{ + PPMType: models.PPMTypeActualExpense, + ExpectedDepartureDate: expectedPickupDate, + ActualMoveDate: &actualPickupDate, + Status: models.PPMShipmentStatusWaitingOnCustomer, + EstimatedWeight: &netWeight, + AdvanceAmountRequested: ¢s, + EstimatedIncentive: &estIncentive, + MaxIncentive: &maxIncentive, + Shipment: models.MTOShipment{ + ShipmentLocator: &locator, + }, + IsActualExpenseReimbursement: models.BoolPointer(true), + } + ssd := models.ShipmentSummaryFormData{ + ServiceMember: serviceMember, + Order: order, + CurrentDutyLocation: yuma, + NewDutyLocation: fortGordon, + PPMRemainingEntitlement: 3000, + WeightAllotment: wtgEntitlements, + PreparationDate: time.Date(2019, 1, 1, 1, 1, 1, 1, time.UTC), + PPMShipment: PPMShipments, + } - mockPPMCloseoutFetcher := &mocks.PPMCloseoutFetcher{} - sswPPMComputer := NewSSWPPMComputer(mockPPMCloseoutFetcher) - sswPage1, err := sswPPMComputer.FormatValuesShipmentSummaryWorksheetFormPage1(ssd, false) - suite.NoError(err) - suite.Equal(FormatDate(time.Now()), sswPage1.PreparationDate1) - - suite.Equal("Jenkins Jr., Marcus Joseph", sswPage1.ServiceMemberName) - suite.Equal("E-9", sswPage1.RankGrade) - suite.Equal("Air Force", sswPage1.ServiceBranch) - suite.Equal("00 Days in SIT", sswPage1.MaxSITStorageEntitlement) - suite.Equal("Yuma AFB, IA 50309", sswPage1.AuthorizedOrigin) - suite.Equal("Fort Eisenhower, GA 30813", sswPage1.AuthorizedDestination) - suite.Equal("No", sswPage1.POVAuthorized) - suite.Equal("444-555-8888", sswPage1.PreferredPhoneNumber) - suite.Equal("michael+ppm-expansion_1@truss.works", sswPage1.PreferredEmail) - suite.Equal("1234567890", sswPage1.DODId) - suite.Equal("Air Force", sswPage1.IssuingBranchOrAgency) - suite.Equal("21-Dec-2018", sswPage1.OrdersIssueDate) - suite.Equal("PCS/012345", sswPage1.OrdersTypeAndOrdersNumber) - suite.Equal("Fort Eisenhower, GA 30813", sswPage1.NewDutyAssignment) - suite.Equal("15,000", sswPage1.WeightAllotment) - suite.Equal("2,000", sswPage1.WeightAllotmentProGear) - suite.Equal("500", sswPage1.WeightAllotmentProgearSpouse) - suite.Equal("17,500", sswPage1.TotalWeightAllotment) - - suite.Equal(locator+" PPM", sswPage1.ShipmentNumberAndTypes) - suite.Equal("11-Jan-2019", sswPage1.ShipmentPickUpDates) - suite.Equal("4,000 lbs - Estimated", sswPage1.ShipmentWeights) - suite.Equal("Waiting On Customer", sswPage1.ShipmentCurrentShipmentStatuses) - suite.Equal("17,500", sswPage1.TotalWeightAllotmentRepeat) - suite.Equal("15,000 lbs; $20,000.00", sswPage1.MaxObligationGCC100) - suite.True(sswPage1.IsActualExpenseReimbursement) - suite.Equal("Actual Expense Reimbursement", sswPage1.GCCIsActualExpenseReimbursement) - - // quick test when there is no PPM actual move date - PPMShipmentWithoutActualMoveDate := models.PPMShipment{ - Status: models.PPMShipmentStatusWaitingOnCustomer, - EstimatedWeight: &netWeight, - AdvanceAmountRequested: ¢s, - Shipment: models.MTOShipment{ - ShipmentLocator: &locator, - }, - } + mockPPMCloseoutFetcher := &mocks.PPMCloseoutFetcher{} + sswPPMComputer := NewSSWPPMComputer(mockPPMCloseoutFetcher) + sswPage1, err := sswPPMComputer.FormatValuesShipmentSummaryWorksheetFormPage1(ssd, false) + suite.NoError(err) + suite.Equal(FormatDate(time.Now()), sswPage1.PreparationDate1) + + suite.Equal("Jenkins Jr., Marcus Joseph", sswPage1.ServiceMemberName) + suite.Equal("E-9", sswPage1.RankGrade) + suite.Equal("Air Force", sswPage1.ServiceBranch) + suite.Equal("00 Days in SIT", sswPage1.MaxSITStorageEntitlement) + suite.Equal("Yuma AFB, IA 50309", sswPage1.AuthorizedOrigin) + suite.Equal("Fort Eisenhower, GA 30813", sswPage1.AuthorizedDestination) + suite.Equal("No", sswPage1.POVAuthorized) + suite.Equal("444-555-8888", sswPage1.PreferredPhoneNumber) + suite.Equal("michael+ppm-expansion_1@truss.works", sswPage1.PreferredEmail) + suite.Equal("1234567890", sswPage1.DODId) + suite.Equal("Air Force", sswPage1.IssuingBranchOrAgency) + suite.Equal("21-Dec-2018", sswPage1.OrdersIssueDate) + suite.Equal("PCS/012345", sswPage1.OrdersTypeAndOrdersNumber) + suite.Equal("Fort Eisenhower, GA 30813", sswPage1.NewDutyAssignment) + suite.Equal("15,000", sswPage1.WeightAllotment) + suite.Equal("2,000", sswPage1.WeightAllotmentProGear) + suite.Equal("500", sswPage1.WeightAllotmentProgearSpouse) + suite.Equal("17,500", sswPage1.TotalWeightAllotment) + + suite.Equal(locator+" PPM", sswPage1.ShipmentNumberAndTypes) + suite.Equal("11-Jan-2019", sswPage1.ShipmentPickUpDates) + suite.Equal("4,000 lbs - Estimated", sswPage1.ShipmentWeights) + suite.Equal("Waiting On Customer", sswPage1.ShipmentCurrentShipmentStatuses) + suite.Equal("17,500", sswPage1.TotalWeightAllotmentRepeat) + suite.Equal("15,000 lbs; $20,000.00", sswPage1.MaxObligationGCC100) + suite.True(sswPage1.IsActualExpenseReimbursement) + suite.False(sswPage1.IsSmallPackageReimbursement) + suite.Equal("Actual Expense Reimbursement", sswPage1.GCCExpenseReimbursementType) + + // quick test when there is no PPM actual move date + PPMShipmentWithoutActualMoveDate := models.PPMShipment{ + Status: models.PPMShipmentStatusWaitingOnCustomer, + EstimatedWeight: &netWeight, + AdvanceAmountRequested: ¢s, + Shipment: models.MTOShipment{ + ShipmentLocator: &locator, + }, + } - ssdWithoutPPMActualMoveDate := models.ShipmentSummaryFormData{ - ServiceMember: serviceMember, - Order: order, - CurrentDutyLocation: yuma, - NewDutyLocation: fortGordon, - PPMRemainingEntitlement: 3000, - WeightAllotment: wtgEntitlements, - PreparationDate: time.Date(2019, 1, 1, 1, 1, 1, 1, time.UTC), - PPMShipment: PPMShipmentWithoutActualMoveDate, - } - sswPage1NoActualMoveDate, err := sswPPMComputer.FormatValuesShipmentSummaryWorksheetFormPage1(ssdWithoutPPMActualMoveDate, false) - suite.NoError(err) - suite.Equal("N/A", sswPage1NoActualMoveDate.ShipmentPickUpDates) + ssdWithoutPPMActualMoveDate := models.ShipmentSummaryFormData{ + ServiceMember: serviceMember, + Order: order, + CurrentDutyLocation: yuma, + NewDutyLocation: fortGordon, + PPMRemainingEntitlement: 3000, + WeightAllotment: wtgEntitlements, + PreparationDate: time.Date(2019, 1, 1, 1, 1, 1, 1, time.UTC), + PPMShipment: PPMShipmentWithoutActualMoveDate, + } + sswPage1NoActualMoveDate, err := sswPPMComputer.FormatValuesShipmentSummaryWorksheetFormPage1(ssdWithoutPPMActualMoveDate, false) + suite.NoError(err) + suite.Equal("N/A", sswPage1NoActualMoveDate.ShipmentPickUpDates) + }) + + suite.Run("PPM Type Small Package Reimbursement - Success", func() { + yuma := factory.FetchOrBuildCurrentDutyLocation(suite.DB()) + fortGordon := factory.FetchOrBuildOrdersDutyLocation(suite.DB()) + wtgEntitlements := models.SSWMaxWeightEntitlement{ + Entitlement: 15000, + ProGear: 2000, + SpouseProGear: 500, + TotalWeight: 17500, + } + + serviceMemberID, _ := uuid.NewV4() + serviceBranch := models.AffiliationAIRFORCE + grade := models.ServiceMemberGradeE9 + serviceMember := models.ServiceMember{ + ID: serviceMemberID, + FirstName: models.StringPointer("Marcus"), + MiddleName: models.StringPointer("Joseph"), + LastName: models.StringPointer("Jenkins"), + Suffix: models.StringPointer("Jr."), + Telephone: models.StringPointer("444-555-8888"), + PersonalEmail: models.StringPointer("michael+ppm-expansion_1@truss.works"), + Edipi: models.StringPointer("1234567890"), + Affiliation: &serviceBranch, + } + + orderIssueDate := time.Date(2018, time.December, 21, 0, 0, 0, 0, time.UTC) + order := models.Order{ + IssueDate: orderIssueDate, + OrdersType: internalmessages.OrdersTypePERMANENTCHANGEOFSTATION, + OrdersNumber: models.StringPointer("012345"), + NewDutyLocationID: fortGordon.ID, + TAC: models.StringPointer("NTA4"), + SAC: models.StringPointer("SAC"), + HasDependents: true, + SpouseHasProGear: true, + Grade: &grade, + } + expectedPickupDate := time.Date(2019, time.January, 11, 0, 0, 0, 0, time.UTC) + actualPickupDate := time.Date(2019, time.February, 11, 0, 0, 0, 0, time.UTC) + netWeight := unit.Pound(4000) + cents := unit.Cents(1000) + locator := "ABCDEF-01" + estIncentive := unit.Cents(1000000) + maxIncentive := unit.Cents(2000000) + PPMShipments := models.PPMShipment{ + PPMType: models.PPMTypeSmallPackage, + ExpectedDepartureDate: expectedPickupDate, + ActualMoveDate: &actualPickupDate, + Status: models.PPMShipmentStatusWaitingOnCustomer, + EstimatedWeight: &netWeight, + AdvanceAmountRequested: ¢s, + EstimatedIncentive: &estIncentive, + MaxIncentive: &maxIncentive, + Shipment: models.MTOShipment{ + ShipmentLocator: &locator, + }, + IsActualExpenseReimbursement: models.BoolPointer(false), + } + ssd := models.ShipmentSummaryFormData{ + ServiceMember: serviceMember, + Order: order, + CurrentDutyLocation: yuma, + NewDutyLocation: fortGordon, + PPMRemainingEntitlement: 3000, + WeightAllotment: wtgEntitlements, + PreparationDate: time.Date(2019, 1, 1, 1, 1, 1, 1, time.UTC), + PPMShipment: PPMShipments, + } + + mockPPMCloseoutFetcher := &mocks.PPMCloseoutFetcher{} + sswPPMComputer := NewSSWPPMComputer(mockPPMCloseoutFetcher) + sswPage1, err := sswPPMComputer.FormatValuesShipmentSummaryWorksheetFormPage1(ssd, false) + suite.NoError(err) + suite.Equal(FormatDate(time.Now()), sswPage1.PreparationDate1) + + suite.Equal("Jenkins Jr., Marcus Joseph", sswPage1.ServiceMemberName) + suite.Equal("E-9", sswPage1.RankGrade) + suite.Equal("Air Force", sswPage1.ServiceBranch) + suite.Equal("00 Days in SIT", sswPage1.MaxSITStorageEntitlement) + suite.Equal("Yuma AFB, IA 50309", sswPage1.AuthorizedOrigin) + suite.Equal("Fort Eisenhower, GA 30813", sswPage1.AuthorizedDestination) + suite.Equal("No", sswPage1.POVAuthorized) + suite.Equal("444-555-8888", sswPage1.PreferredPhoneNumber) + suite.Equal("michael+ppm-expansion_1@truss.works", sswPage1.PreferredEmail) + suite.Equal("1234567890", sswPage1.DODId) + suite.Equal("Air Force", sswPage1.IssuingBranchOrAgency) + suite.Equal("21-Dec-2018", sswPage1.OrdersIssueDate) + suite.Equal("PCS/012345", sswPage1.OrdersTypeAndOrdersNumber) + suite.Equal("Fort Eisenhower, GA 30813", sswPage1.NewDutyAssignment) + suite.Equal("15,000", sswPage1.WeightAllotment) + suite.Equal("2,000", sswPage1.WeightAllotmentProGear) + suite.Equal("500", sswPage1.WeightAllotmentProgearSpouse) + suite.Equal("17,500", sswPage1.TotalWeightAllotment) + + suite.Equal(locator+" PPM", sswPage1.ShipmentNumberAndTypes) + suite.Equal("11-Jan-2019", sswPage1.ShipmentPickUpDates) + suite.Equal("4,000 lbs - Estimated", sswPage1.ShipmentWeights) + suite.Equal("Waiting On Customer", sswPage1.ShipmentCurrentShipmentStatuses) + suite.Equal("17,500", sswPage1.TotalWeightAllotmentRepeat) + suite.Equal("15,000 lbs; $20,000.00", sswPage1.MaxObligationGCC100) + suite.False(sswPage1.IsActualExpenseReimbursement) + suite.True(sswPage1.IsSmallPackageReimbursement) + suite.Equal("Small Package Reimbursement", sswPage1.GCCExpenseReimbursementType) + + // quick test when there is no PPM actual move date + PPMShipmentWithoutActualMoveDate := models.PPMShipment{ + Status: models.PPMShipmentStatusWaitingOnCustomer, + EstimatedWeight: &netWeight, + AdvanceAmountRequested: ¢s, + Shipment: models.MTOShipment{ + ShipmentLocator: &locator, + }, + } + + ssdWithoutPPMActualMoveDate := models.ShipmentSummaryFormData{ + ServiceMember: serviceMember, + Order: order, + CurrentDutyLocation: yuma, + NewDutyLocation: fortGordon, + PPMRemainingEntitlement: 3000, + WeightAllotment: wtgEntitlements, + PreparationDate: time.Date(2019, 1, 1, 1, 1, 1, 1, time.UTC), + PPMShipment: PPMShipmentWithoutActualMoveDate, + } + sswPage1NoActualMoveDate, err := sswPPMComputer.FormatValuesShipmentSummaryWorksheetFormPage1(ssdWithoutPPMActualMoveDate, false) + suite.NoError(err) + suite.Equal("N/A", sswPage1NoActualMoveDate.ShipmentPickUpDates) + }) } func (suite *ShipmentSummaryWorksheetServiceSuite) TestFormatValuesShipmentSummaryWorksheetFormPage2() { - fortGordon := factory.FetchOrBuildOrdersDutyLocation(suite.DB()) - orderIssueDate := time.Date(2018, time.December, 21, 0, 0, 0, 0, time.UTC) - locator := "ABCDEF-01" - shipment := models.PPMShipment{ - Shipment: models.MTOShipment{ - ShipmentLocator: &locator, - }, - IsActualExpenseReimbursement: models.BoolPointer(true), - } + suite.Run("PPM Type Actual Expense Reimbursement - Success", func() { + fortGordon := factory.FetchOrBuildOrdersDutyLocation(suite.DB()) + orderIssueDate := time.Date(2018, time.December, 21, 0, 0, 0, 0, time.UTC) + locator := "ABCDEF-01" + shipment := models.PPMShipment{ + Shipment: models.MTOShipment{ + ShipmentLocator: &locator, + }, + PPMType: models.PPMTypeActualExpense, + IsActualExpenseReimbursement: models.BoolPointer(true), + } - order := models.Order{ - IssueDate: orderIssueDate, - OrdersType: internalmessages.OrdersTypePERMANENTCHANGEOFSTATION, - OrdersNumber: models.StringPointer("012345"), - NewDutyLocationID: fortGordon.ID, - TAC: models.StringPointer("NTA4"), - SAC: models.StringPointer("SAC"), - HasDependents: true, - SpouseHasProGear: true, - } - paidWithGTCCFalse := false - paidWithGTCCTrue := true - tollExpense := models.MovingExpenseReceiptTypeTolls - oilExpense := models.MovingExpenseReceiptTypeOil - amount := unit.Cents(10000) - statusApproved := models.PPMDocumentStatusApproved - movingExpenses := models.MovingExpenses{ - { - MovingExpenseType: &tollExpense, - Amount: &amount, - PaidWithGTCC: &paidWithGTCCFalse, - Status: &statusApproved, - }, - { - MovingExpenseType: &oilExpense, - Amount: &amount, - PaidWithGTCC: &paidWithGTCCFalse, - Status: &statusApproved, - }, - { - MovingExpenseType: &oilExpense, - Amount: &amount, - PaidWithGTCC: &paidWithGTCCTrue, - Status: &statusApproved, - }, - { - MovingExpenseType: &oilExpense, - Amount: &amount, - PaidWithGTCC: &paidWithGTCCFalse, - Status: &statusApproved, - }, - { - MovingExpenseType: &tollExpense, - Amount: &amount, - PaidWithGTCC: &paidWithGTCCTrue, - Status: &statusApproved, - }, - { - MovingExpenseType: &tollExpense, - Amount: &amount, - PaidWithGTCC: &paidWithGTCCTrue, - Status: &statusApproved, - }, - { - MovingExpenseType: &tollExpense, - Amount: &amount, - PaidWithGTCC: &paidWithGTCCFalse, - Status: &statusApproved, - }, - } + order := models.Order{ + IssueDate: orderIssueDate, + OrdersType: internalmessages.OrdersTypePERMANENTCHANGEOFSTATION, + OrdersNumber: models.StringPointer("012345"), + NewDutyLocationID: fortGordon.ID, + TAC: models.StringPointer("NTA4"), + SAC: models.StringPointer("SAC"), + HasDependents: true, + SpouseHasProGear: true, + } + paidWithGTCCFalse := false + paidWithGTCCTrue := true + tollExpense := models.MovingExpenseReceiptTypeTolls + oilExpense := models.MovingExpenseReceiptTypeOil + amount := unit.Cents(10000) + statusApproved := models.PPMDocumentStatusApproved + movingExpenses := models.MovingExpenses{ + { + MovingExpenseType: &tollExpense, + Amount: &amount, + PaidWithGTCC: &paidWithGTCCFalse, + Status: &statusApproved, + }, + { + MovingExpenseType: &oilExpense, + Amount: &amount, + PaidWithGTCC: &paidWithGTCCFalse, + Status: &statusApproved, + }, + { + MovingExpenseType: &oilExpense, + Amount: &amount, + PaidWithGTCC: &paidWithGTCCTrue, + Status: &statusApproved, + }, + { + MovingExpenseType: &oilExpense, + Amount: &amount, + PaidWithGTCC: &paidWithGTCCFalse, + Status: &statusApproved, + }, + { + MovingExpenseType: &tollExpense, + Amount: &amount, + PaidWithGTCC: &paidWithGTCCTrue, + Status: &statusApproved, + }, + { + MovingExpenseType: &tollExpense, + Amount: &amount, + PaidWithGTCC: &paidWithGTCCTrue, + Status: &statusApproved, + }, + { + MovingExpenseType: &tollExpense, + Amount: &amount, + PaidWithGTCC: &paidWithGTCCFalse, + Status: &statusApproved, + }, + } - ssd := models.ShipmentSummaryFormData{ - Order: order, - MovingExpenses: movingExpenses, - PPMShipment: shipment, - } + ssd := models.ShipmentSummaryFormData{ + Order: order, + MovingExpenses: movingExpenses, + PPMShipment: shipment, + } - mockPPMCloseoutFetcher := &mocks.PPMCloseoutFetcher{} - sswPPMComputer := NewSSWPPMComputer(mockPPMCloseoutFetcher) - expensesMap := SubTotalExpenses(ssd.MovingExpenses) - sswPage2, err := sswPPMComputer.FormatValuesShipmentSummaryWorksheetFormPage2(ssd, false, expensesMap) - suite.NoError(err) - suite.Equal("$200.00", sswPage2.TollsGTCCPaid) - suite.Equal("$200.00", sswPage2.TollsMemberPaid) - suite.Equal("$200.00", sswPage2.OilMemberPaid) - suite.Equal("$100.00", sswPage2.OilGTCCPaid) - suite.Equal("$300.00", sswPage2.TotalGTCCPaid) - suite.Equal("$400.00", sswPage2.TotalMemberPaid) - suite.Equal("NTA4", sswPage2.TAC) - suite.Equal("SAC", sswPage2.SAC) - suite.Equal("Actual Expense Reimbursement", sswPage2.IncentiveIsActualExpenseReimbursement) - suite.Equal(`This PPM is being processed at actual expense reimbursement for valid expenses not to exceed the - government constructed cost (GCC).`, sswPage2.HeaderIsActualExpenseReimbursement) + mockPPMCloseoutFetcher := &mocks.PPMCloseoutFetcher{} + sswPPMComputer := NewSSWPPMComputer(mockPPMCloseoutFetcher) + expensesMap := SubTotalExpenses(ssd.MovingExpenses) + sswPage2, err := sswPPMComputer.FormatValuesShipmentSummaryWorksheetFormPage2(ssd, false, expensesMap) + suite.NoError(err) + suite.Equal("$200.00", sswPage2.TollsGTCCPaid) + suite.Equal("$200.00", sswPage2.TollsMemberPaid) + suite.Equal("$200.00", sswPage2.OilMemberPaid) + suite.Equal("$100.00", sswPage2.OilGTCCPaid) + suite.Equal("$300.00", sswPage2.TotalGTCCPaid) + suite.Equal("$400.00", sswPage2.TotalMemberPaid) + suite.Equal("NTA4", sswPage2.TAC) + suite.Equal("SAC", sswPage2.SAC) + suite.Equal("Actual Expense Reimbursement", sswPage2.IncentiveExpenseReimbursementType) + suite.Equal(`This PPM is being processed as actual expense reimbursement for valid expenses not to exceed the + government constructed cost (GCC).`, sswPage2.HeaderExpenseReimbursementType) + }) + + suite.Run("PPM Type Small Package Reimbursement - Success", func() { + fortGordon := factory.FetchOrBuildOrdersDutyLocation(suite.DB()) + orderIssueDate := time.Date(2018, time.December, 21, 0, 0, 0, 0, time.UTC) + locator := "ABCDEF-01" + shipment := models.PPMShipment{ + Shipment: models.MTOShipment{ + ShipmentLocator: &locator, + }, + PPMType: models.PPMTypeSmallPackage, + IsActualExpenseReimbursement: models.BoolPointer(false), + } + + order := models.Order{ + IssueDate: orderIssueDate, + OrdersType: internalmessages.OrdersTypePERMANENTCHANGEOFSTATION, + OrdersNumber: models.StringPointer("012345"), + NewDutyLocationID: fortGordon.ID, + TAC: models.StringPointer("NTA4"), + SAC: models.StringPointer("SAC"), + HasDependents: true, + SpouseHasProGear: true, + } + paidWithGTCCFalse := false + paidWithGTCCTrue := true + tollExpense := models.MovingExpenseReceiptTypeTolls + oilExpense := models.MovingExpenseReceiptTypeOil + amount := unit.Cents(10000) + movingExpenses := models.MovingExpenses{ + { + MovingExpenseType: &tollExpense, + Amount: &amount, + PaidWithGTCC: &paidWithGTCCFalse, + }, + { + MovingExpenseType: &oilExpense, + Amount: &amount, + PaidWithGTCC: &paidWithGTCCFalse, + }, + { + MovingExpenseType: &oilExpense, + Amount: &amount, + PaidWithGTCC: &paidWithGTCCTrue, + }, + { + MovingExpenseType: &oilExpense, + Amount: &amount, + PaidWithGTCC: &paidWithGTCCFalse, + }, + { + MovingExpenseType: &tollExpense, + Amount: &amount, + PaidWithGTCC: &paidWithGTCCTrue, + }, + { + MovingExpenseType: &tollExpense, + Amount: &amount, + PaidWithGTCC: &paidWithGTCCTrue, + }, + { + MovingExpenseType: &tollExpense, + Amount: &amount, + PaidWithGTCC: &paidWithGTCCFalse, + }, + } + + ssd := models.ShipmentSummaryFormData{ + Order: order, + MovingExpenses: movingExpenses, + PPMShipment: shipment, + } + + mockPPMCloseoutFetcher := &mocks.PPMCloseoutFetcher{} + sswPPMComputer := NewSSWPPMComputer(mockPPMCloseoutFetcher) + expensesMap := SubTotalExpenses(ssd.MovingExpenses) + sswPage2, err := sswPPMComputer.FormatValuesShipmentSummaryWorksheetFormPage2(ssd, false, expensesMap) + suite.NoError(err) + suite.Equal("$200.00", sswPage2.TollsGTCCPaid) + suite.Equal("$200.00", sswPage2.TollsMemberPaid) + suite.Equal("$200.00", sswPage2.OilMemberPaid) + suite.Equal("$100.00", sswPage2.OilGTCCPaid) + suite.Equal("$300.00", sswPage2.TotalGTCCPaid) + suite.Equal("$400.00", sswPage2.TotalMemberPaid) + suite.Equal("NTA4", sswPage2.TAC) + suite.Equal("SAC", sswPage2.SAC) + suite.Equal("Small Package Reimbursement", sswPage2.IncentiveExpenseReimbursementType) + suite.Equal(`This PPM is being processed as small package reimbursement for valid expenses not to exceed the + government constructed cost (GCC).`, sswPage2.HeaderExpenseReimbursementType) + }) } func (suite *ShipmentSummaryWorksheetServiceSuite) TestFormatValuesShipmentSummaryWorksheetFormPage2ExcludeRejectedOrExcludedExpensesFromTotal() { diff --git a/playwright/tests/office/txo/tooQueueFilters.spec.js b/playwright/tests/office/txo/tooQueueFilters.spec.js new file mode 100644 index 00000000000..908f5346c19 --- /dev/null +++ b/playwright/tests/office/txo/tooQueueFilters.spec.js @@ -0,0 +1,206 @@ +// @ts-check +import { test, expect } from '../../utils/office/officeTest'; + +import TooFlowPage from './tooTestFixture'; + +const waitForFilterInput = async (page, testId, value) => { + await page.waitForFunction( + (args) => { + /** @type {HTMLInputElement} */ + const input = document.querySelector(`[data-testid="${args.testId}"] [data-testid="TextBoxFilter"]`); + if (input && input.value === args.value) return true; + if (input) { + input.value = args.value; + const keyupEvent = new KeyboardEvent('keyup', { + code: 'Enter', + key: 'Enter', + bubbles: true, + cancelable: true, + }); + input.dispatchEvent(keyupEvent); + } + return false; + }, + { testId, value }, + { polling: 500, timeout: 5000 }, + ); +}; + +test.describe('TOO user queue filters - Move Queue', async () => { + let testMove; + let testMoveNotAssigned; + let tooFlowPage; + + test.beforeEach(async ({ officePage }) => { + testMove = await officePage.testHarness.buildMoveWithNTSShipmentsForTOO(); + testMoveNotAssigned = await officePage.testHarness.buildMoveWithNTSShipmentsForTOO(); + await officePage.signInAsNewTOOUser(); + tooFlowPage = new TooFlowPage(officePage, testMove); + await tooFlowPage.waitForLoading(); + }); + + test('filters out all moves with nonsense assigned user', async ({ page }) => { + // We should still see all moves + await expect(page.getByRole('heading', { level: 1 })).not.toContainText('All moves (0)'); + + // Add nonsense string to our filter (so now we're searching for 'abcde') + await waitForFilterInput(page, 'assignedTo', 'abcde'); + + // Now we shouldn't see any results + await expect(page.getByRole('heading', { level: 1 })).toContainText('All moves (0)'); + await expect(page.getByRole('row').getByText(testMove.locator)).not.toBeVisible(); + await expect(page.getByRole('row').getByText(testMoveNotAssigned.locator)).not.toBeVisible(); + }); + + test('filters all moves EXCEPT with assigned user, restores all moves when filter removed', async ({ page }) => { + await Promise.all([ + page.waitForResponse((res) => res.url().includes('/ghc/v1/queues/moves')), + page.getByText('KKFA moves').click(), + ]); + + await expect(page.getByRole('heading', { level: 1 })).not.toContainText('All moves (0)'); + const allMoves = await page.getByRole('heading', { level: 1 }).innerHTML(); + + // assign user to test move + await waitForFilterInput(page, 'locator', testMove.locator); + + await expect(page.getByTestId('locator').getByTestId('TextBoxFilter')).toHaveValue(testMove.locator); + await expect(page.getByRole('row').getByText(testMove.locator)).toBeVisible(); + + expect( + await page + .getByTestId('assignedTo-0') + .getByTestId('dropdown') + .getByRole('option', { selected: true }) + .textContent(), + ).toEqual('—'); + + await Promise.all([ + page.waitForResponse((res) => res.url().includes('assignOfficeUser')), + page.getByTestId('assignedTo-0').getByTestId('dropdown').selectOption({ index: 1 }), + ]); + + const assigned = ( + await page + .getByTestId('assignedTo-0') + .getByTestId('dropdown') + .getByRole('option', { selected: true }) + .textContent() + ) + .split(',') + .at(0); + + // search for moves with assigned user filter only + await waitForFilterInput(page, 'locator', ''); + + await expect(page.getByTestId('locator').getByTestId('TextBoxFilter')).toBeEmpty(); + await expect(page.getByRole('heading', { level: 1 })).toContainText(allMoves); + + await waitForFilterInput(page, 'assignedTo', assigned); + + // assigned moves for user show in queue + await expect(page.getByRole('table').getByText(testMove.locator)).toBeVisible(); + await expect(page.getByRole('heading', { level: 1 })).not.toContainText(allMoves); + await expect(page.getByRole('heading', { level: 1 })).not.toContainText('All moves (0)'); + await expect(page.getByRole('row').getByText(testMove.locator)).toBeVisible(); + await expect(page.getByRole('row').getByText(testMoveNotAssigned.locator)).not.toBeVisible(); + + // Now, remove filter and ensure retores all moves in queue + const removeFilterButton = page.getByTestId('remove-filters-assignedTo'); + await expect(removeFilterButton).toBeVisible(); + await removeFilterButton.click(); + await expect(page.getByRole('heading', { level: 1 })).toContainText(allMoves); + }); +}); + +test.describe('TOO user queue filters - Destination Requests Queue', async () => { + let testMove; + let testMoveNotAssigned; + let tooFlowPage; + + test.beforeEach(async ({ officePage }) => { + testMove = await officePage.testHarness.buildHHGMoveInSITWithPendingExtension(); + testMoveNotAssigned = await officePage.testHarness.buildHHGMoveInSITWithPendingExtension(); + await officePage.signInAsNewTOOUser(); + tooFlowPage = new TooFlowPage(officePage, testMove); + await tooFlowPage.waitForLoading(); + }); + + test('filters out all moves with nonsense assigned user', async ({ page }) => { + // We should still see all moves + await Promise.all([ + page.waitForResponse((res) => res.url().includes('/ghc/v1/queues/destination-requests')), + page.getByTestId('destination-requests-tab-link').click(), + ]); + await expect(page.getByRole('heading', { level: 1 })).not.toContainText('Destination requests (0)'); + + // Add nonsense string to our filter (so now we're searching for 'abcde') + await waitForFilterInput(page, 'assignedTo', 'abcde'); + + // Now we shouldn't see any results + await expect(page.getByRole('heading', { level: 1 })).toContainText('Destination requests (0)'); + await expect(page.getByRole('row').getByText(testMove.locator)).not.toBeVisible(); + await expect(page.getByRole('row').getByText(testMoveNotAssigned.locator)).not.toBeVisible(); + }); + + test('filters all moves EXCEPT with assigned user, restores all moves when filter removed', async ({ page }) => { + // We should still see all moves + await Promise.all([ + page.waitForResponse((res) => res.url().includes('/ghc/v1/queues/destination-requests')), + page.getByTestId('destination-requests-tab-link').click(), + ]); + + await expect(page.getByRole('heading', { level: 1 })).not.toContainText('Destination requests (0)'); + const destinationRequests = await page.getByRole('heading', { level: 1 }).innerHTML(); + + // assign user to test move + await waitForFilterInput(page, 'locator', testMove.locator); + + await expect(page.getByTestId('locator').getByTestId('TextBoxFilter')).toHaveValue(testMove.locator); + await expect(page.getByRole('row').getByText(testMove.locator)).toBeVisible(); + + expect( + await page + .getByTestId('assignedTo-0') + .getByTestId('dropdown') + .getByRole('option', { selected: true }) + .textContent(), + ).toEqual('—'); + + await Promise.all([ + page.waitForResponse((res) => res.url().includes('assignOfficeUser')), + page.getByTestId('assignedTo-0').getByTestId('dropdown').selectOption({ index: 1 }), + ]); + + const assigned = ( + await page + .getByTestId('assignedTo-0') + .getByTestId('dropdown') + .getByRole('option', { selected: true }) + .textContent() + ) + .split(',') + .at(0); + + // search for moves with assigned user filter only + await waitForFilterInput(page, 'locator', ''); + + await expect(page.getByTestId('locator').getByTestId('TextBoxFilter')).toBeEmpty(); + await expect(page.getByRole('heading', { level: 1 })).toContainText(destinationRequests); + + await waitForFilterInput(page, 'assignedTo', assigned); + + // assigned moves for user show in queue + await expect(page.getByRole('table').getByText(testMove.locator)).toBeVisible(); + await expect(page.getByRole('heading', { level: 1 })).not.toContainText(destinationRequests); + await expect(page.getByRole('heading', { level: 1 })).not.toContainText('Destination requests (0)'); + await expect(page.getByRole('row').getByText(testMove.locator)).toBeVisible(); + await expect(page.getByRole('row').getByText(testMoveNotAssigned.locator)).not.toBeVisible(); + + // Now, remove filter and ensure retores all moves in queue + const removeFilterButton = page.getByTestId('remove-filters-assignedTo'); + await expect(removeFilterButton).toBeVisible(); + await removeFilterButton.click(); + await expect(page.getByRole('heading', { level: 1 })).toContainText(destinationRequests); + }); +}); diff --git a/src/components/Office/DefinitionLists/PPMShipmentInfoList.jsx b/src/components/Office/DefinitionLists/PPMShipmentInfoList.jsx index 7032cf46561..9a78bc612e4 100644 --- a/src/components/Office/DefinitionLists/PPMShipmentInfoList.jsx +++ b/src/components/Office/DefinitionLists/PPMShipmentInfoList.jsx @@ -18,6 +18,7 @@ import { permissionTypes } from 'constants/permissions'; import Restricted from 'components/Restricted/Restricted'; import { downloadPPMAOAPacket, downloadPPMPaymentPacket } from 'services/ghcApi'; import { isBooleanFlagEnabled } from 'utils/featureFlags'; +import { getPPMTypeLabel } from 'shared/constants'; const PPMShipmentInfoList = ({ className, @@ -30,6 +31,7 @@ const PPMShipmentInfoList = ({ onErrorModalToggle, }) => { const { + ppmType, hasRequestedAdvance, advanceAmountRequested, advanceStatus, @@ -87,6 +89,14 @@ const PPMShipmentInfoList = ({ return (isExpanded || elementFlags.alwaysShow) && !elementFlags.hideRow; }; + const ppmTypeElementFlags = getDisplayFlags('ppmType'); + const ppmTypeElement = ( +
+
PPM Type
+
{getPPMTypeLabel(ppmType)}
+
+ ); + const expectedDepartureDateElementFlags = getDisplayFlags('expectedDepartureDate'); const expectedDepartureDateElement = (
@@ -283,6 +293,7 @@ const PPMShipmentInfoList = ({ )} data-testid="ppm-shipment-info-list" > + {ppmTypeElement} {!actualMoveDate && expectedDepartureDateElement} {actualMoveDate && actualDepartureDateElement} {pickupAddressElement} diff --git a/src/components/Office/DefinitionLists/PPMShipmentInfoList.stories.jsx b/src/components/Office/DefinitionLists/PPMShipmentInfoList.stories.jsx index bb521fa1c8a..d616638e4ee 100644 --- a/src/components/Office/DefinitionLists/PPMShipmentInfoList.stories.jsx +++ b/src/components/Office/DefinitionLists/PPMShipmentInfoList.stories.jsx @@ -1,8 +1,9 @@ import React from 'react'; -// import { object, text } from '@storybook/addon-knobs'; import PPMShipmentInfoList from './PPMShipmentInfoList'; +import { PPM_TYPES } from 'shared/constants'; + export default { title: 'Office Components/PPM Shipment Info List', component: PPMShipmentInfoList, @@ -10,6 +11,7 @@ export default { const ppmInfo = { ppmShipment: { + ppmType: PPM_TYPES.INCENTIVE_BASED, actualMoveDate: null, hasRequestedAdvance: true, advanceAmountRequested: 598700, diff --git a/src/components/Office/RequestedShipments/RequestedShipmentsTestData.js b/src/components/Office/RequestedShipments/RequestedShipmentsTestData.js index d3cb9c2a1e8..dca2b0ee7aa 100644 --- a/src/components/Office/RequestedShipments/RequestedShipmentsTestData.js +++ b/src/components/Office/RequestedShipments/RequestedShipmentsTestData.js @@ -1,7 +1,7 @@ import { ORDERS_TYPE, ORDERS_BRANCH_OPTIONS, ORDERS_PAY_GRADE_OPTIONS } from '../../../constants/orders'; import { DEPARTMENT_INDICATOR_OPTIONS } from '../../../constants/departmentIndicators'; -import { SHIPMENT_OPTIONS, MTOAgentType } from 'shared/constants'; +import { SHIPMENT_OPTIONS, MTOAgentType, PPM_TYPES } from 'shared/constants'; export const shipments = [ { @@ -414,6 +414,7 @@ export const zeroIncentivePPM = [ streetAddress3: 'c/o Some Person', }, ppmShipment: { + ppmType: PPM_TYPES.INCENTIVE_BASED, pickupAddress: { streetAddress1: '812 S 129th St', streetAddress2: '#123', diff --git a/src/components/Office/ShipmentCustomerSIT/ShipmentCustomerSIT.jsx b/src/components/Office/ShipmentCustomerSIT/ShipmentCustomerSIT.jsx index 2267b0ff9c0..d8d11c65286 100644 --- a/src/components/Office/ShipmentCustomerSIT/ShipmentCustomerSIT.jsx +++ b/src/components/Office/ShipmentCustomerSIT/ShipmentCustomerSIT.jsx @@ -69,7 +69,7 @@ const ShipmentCustomerSIT = ({ sitEstimatedWeight, sitEstimatedEntryDate, sitEst return (
-

Storage in transit (SIT)

+

Storage in transit (SIT)

diff --git a/src/components/Office/ShipmentDisplay/ShipmentDisplay.jsx b/src/components/Office/ShipmentDisplay/ShipmentDisplay.jsx index fcf86781d89..80a00cc3d04 100644 --- a/src/components/Office/ShipmentDisplay/ShipmentDisplay.jsx +++ b/src/components/Office/ShipmentDisplay/ShipmentDisplay.jsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import * as PropTypes from 'prop-types'; import { useNavigate } from 'react-router-dom'; import { Checkbox, Tag } from '@trussworks/react-uswds'; @@ -10,7 +10,7 @@ import { EditButton, ReviewButton } from 'components/form/IconButtons'; import ShipmentInfoListSelector from 'components/Office/DefinitionLists/ShipmentInfoListSelector'; import ShipmentContainer from 'components/Office/ShipmentContainer/ShipmentContainer'; import styles from 'components/Office/ShipmentDisplay/ShipmentDisplay.module.scss'; -import { SHIPMENT_OPTIONS, SHIPMENT_TYPES } from 'shared/constants'; +import { FEATURE_FLAG_KEYS, getPPMTypeLabel, PPM_TYPES, SHIPMENT_OPTIONS, SHIPMENT_TYPES } from 'shared/constants'; import { AddressShape } from 'types/address'; import { AgentShape } from 'types/agent'; import { OrdersLOAShape } from 'types/order'; @@ -21,6 +21,7 @@ import Restricted from 'components/Restricted/Restricted'; import { permissionTypes } from 'constants/permissions'; import affiliation from 'content/serviceMemberAgencies'; import { fieldValidationShape, objectIsMissingFieldWithCondition } from 'utils/displayFlags'; +import { isBooleanFlagEnabled } from 'utils/featureFlags'; const ShipmentDisplay = ({ shipmentType, @@ -45,6 +46,7 @@ const ShipmentDisplay = ({ const tac = retrieveTAC(displayInfo.tacType, ordersLOA); const sac = retrieveSAC(displayInfo.sacType, ordersLOA); const [isErrorModalVisible, setIsErrorModalVisible] = useState(false); + const [ppmSprFF, setPpmSprFF] = useState(false); const disableApproval = errorIfMissing.some((requiredInfo) => objectIsMissingFieldWithCondition(displayInfo, requiredInfo), @@ -65,6 +67,13 @@ const ShipmentDisplay = ({ const errorModalMessage = "Something went wrong downloading PPM paperwork. Please try again later. If that doesn't fix it, contact the "; + useEffect(() => { + const fetchData = async () => { + setPpmSprFF(await isBooleanFlagEnabled(FEATURE_FLAG_KEYS.PPM_SPR)); + }; + fetchData(); + }, []); + return (
@@ -96,8 +105,11 @@ const ShipmentDisplay = ({
- {displayInfo.isActualExpenseReimbursement && ( - actual expense reimbursement + {ppmSprFF && displayInfo.ppmShipment?.ppmType === PPM_TYPES.SMALL_PACKAGE && ( + {getPPMTypeLabel(displayInfo.ppmShipment.ppmType)} + )} + {displayInfo.ppmShipment?.ppmType === PPM_TYPES.ACTUAL_EXPENSE && ( + {getPPMTypeLabel(displayInfo.ppmShipment.ppmType)} )} {displayInfo.isDiversion && diversion} {(displayInfo.shipmentStatus === shipmentStatuses.CANCELED || diff --git a/src/components/Office/ShipmentDisplay/ShipmentDisplay.test.jsx b/src/components/Office/ShipmentDisplay/ShipmentDisplay.test.jsx index ccc4d8bb1b1..caca9613c59 100644 --- a/src/components/Office/ShipmentDisplay/ShipmentDisplay.test.jsx +++ b/src/components/Office/ShipmentDisplay/ShipmentDisplay.test.jsx @@ -20,7 +20,8 @@ import ShipmentDisplay from './ShipmentDisplay'; import { MockProviders } from 'testUtils'; import { permissionTypes } from 'constants/permissions'; -import { SHIPMENT_OPTIONS } from 'shared/constants'; +import { PPM_TYPES, SHIPMENT_OPTIONS } from 'shared/constants'; +import { isBooleanFlagEnabled } from 'utils/featureFlags'; const mockNavigate = jest.fn(); jest.mock('react-router-dom', () => ({ @@ -28,6 +29,11 @@ jest.mock('react-router-dom', () => ({ useNavigate: () => mockNavigate, })); +jest.mock('utils/featureFlags', () => ({ + ...jest.requireActual('utils/featureFlags'), + isBooleanFlagEnabled: jest.fn().mockImplementation(() => Promise.resolve(false)), +})); + const errorIfMissingStorageFacility = ['storageFacility']; describe('Shipment Container', () => { @@ -336,10 +342,12 @@ describe('Shipment Container', () => { }); }); it('renders the Actual Expense Reimbursement & PPM status tags', () => { + isBooleanFlagEnabled.mockImplementation(() => Promise.resolve(false)); + ppmInfo.ppmShipment.ppmType = PPM_TYPES.ACTUAL_EXPENSE; render( { /> , ); + expect(screen.getByTestId('ppmStatusTag')).toBeInTheDocument(); expect(screen.getByTestId('actualReimbursementTag')).toBeInTheDocument(); + }); + it('renders the Small Package Reimbursement (when FF is on) & PPM status tags', async () => { + isBooleanFlagEnabled.mockImplementation(() => Promise.resolve(true)); + ppmInfo.ppmShipment.ppmType = PPM_TYPES.SMALL_PACKAGE; + render( + + + , + ); expect(screen.getByTestId('ppmStatusTag')).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByTestId('smallPackageTag')).toBeInTheDocument(); + }); }); }); }); diff --git a/src/components/Office/ShipmentDisplay/ShipmentDisplayTestData.js b/src/components/Office/ShipmentDisplay/ShipmentDisplayTestData.js index 72923530aa5..a6b783be277 100644 --- a/src/components/Office/ShipmentDisplay/ShipmentDisplayTestData.js +++ b/src/components/Office/ShipmentDisplay/ShipmentDisplayTestData.js @@ -1,4 +1,5 @@ import { shipmentStatuses } from 'constants/shipments'; +import { PPM_TYPES } from 'shared/constants'; export const ordersLOA = { tac: '1111', @@ -143,6 +144,7 @@ export const ppmInfo = { heading: 'PPM', shipmentLocator: 'EVLRPT-03', ppmShipment: { + ppmType: PPM_TYPES.INCENTIVE_BASED, actualMoveDate: null, advanceAmountRequested: 598700, hasRequestedAdvance: true, @@ -169,6 +171,7 @@ export const ppmInfo = { export const ppmInfoApprovedOrExcluded = { heading: 'PPM', ppmShipment: { + ppmType: PPM_TYPES.INCENTIVE_BASED, actualMoveDate: null, advanceAmountRequested: 598700, hasRequestedAdvance: true, @@ -228,6 +231,7 @@ export const ppmInfoApprovedOrExcluded = { export const ppmInfoRejected = { heading: 'PPM', ppmShipment: { + ppmType: PPM_TYPES.INCENTIVE_BASED, actualMoveDate: null, advanceAmountRequested: 598700, hasRequestedAdvance: true, @@ -253,6 +257,7 @@ export const ppmInfoRejected = { export const ppmInfoMultiple = { heading: 'PPM 1', ppmShipment: { + ppmType: PPM_TYPES.INCENTIVE_BASED, actualMoveDate: null, advanceAmountRequested: 598700, hasRequestedAdvance: true, @@ -278,6 +283,7 @@ export const ppmInfoMultiple = { export const ppmInfoMultiple2 = { heading: 'PPM 2', ppmShipment: { + ppmType: PPM_TYPES.INCENTIVE_BASED, actualMoveDate: null, advanceAmountRequested: 4300, hasRequestedAdvance: true, diff --git a/src/components/Office/ShipmentForm/ShipmentForm.jsx b/src/components/Office/ShipmentForm/ShipmentForm.jsx index bddedbdfb45..b7a516a7d4d 100644 --- a/src/components/Office/ShipmentForm/ShipmentForm.jsx +++ b/src/components/Office/ShipmentForm/ShipmentForm.jsx @@ -49,7 +49,14 @@ import { updateMoveCloseoutOffice, dateSelectionIsWeekendHoliday, } from 'services/ghcApi'; -import { SHIPMENT_OPTIONS, SHIPMENT_TYPES, technicalHelpDeskURL } from 'shared/constants'; +import { + FEATURE_FLAG_KEYS, + getPPMTypeLabel, + PPM_TYPES, + SHIPMENT_OPTIONS, + SHIPMENT_TYPES, + technicalHelpDeskURL, +} from 'shared/constants'; import formStyles from 'styles/form.module.scss'; import { AccountingCodesShape } from 'types/accountingCodes'; import { AddressShape, SimpleAddressShape } from 'types/address'; @@ -107,17 +114,23 @@ const ShipmentForm = (props) => { const [errorCode, setErrorCode] = useState(null); const [successMessage, setSuccessMessage] = useState(null); const [shipmentAddressUpdateReviewErrorMessage, setShipmentAddressUpdateReviewErrorMessage] = useState(null); - const [isCancelModalVisible, setIsCancelModalVisible] = useState(false); const [isAddressChangeModalOpen, setIsAddressChangeModalOpen] = useState(false); - const [isTertiaryAddressEnabled, setIsTertiaryAddressEnabled] = useState(false); + const [ppmSprFF, setPpmSprFF] = useState(false); + useEffect(() => { const fetchData = async () => { setIsTertiaryAddressEnabled(await isBooleanFlagEnabled('third_address_available')); }; fetchData(); }, []); + useEffect(() => { + const fetchData = async () => { + setPpmSprFF(await isBooleanFlagEnabled(FEATURE_FLAG_KEYS.PPM_SPR)); + }; + fetchData(); + }, []); const shipments = mtoShipments; @@ -173,8 +186,6 @@ const ShipmentForm = (props) => { }); const getShipmentNumber = () => { - // TODO - this is not supported by IE11, shipment number should be calculable from Redux anyways - // we should fix this also b/c it doesn't display correctly in storybook const { search } = window.location; const params = new URLSearchParams(search); const shipmentNumber = params.get('shipmentNumber'); @@ -298,8 +309,6 @@ const ShipmentForm = (props) => { closeoutOffice: move.closeoutOffice, }, ); - if (isCreatePage && serviceMember?.grade === 'CIVILIAN_EMPLOYEE') - initialValues.isActualExpenseReimbursement = 'true'; } else if (isMobileHome) { const hhgInitialValues = formatMtoShipmentForDisplay( isCreatePage ? { userRole } : { userRole, shipmentType, agents: mtoShipment.mtoAgents, ...mtoShipment }, @@ -656,6 +665,7 @@ const ShipmentForm = (props) => { > {({ values, isValid, isSubmitting, setValues, handleSubmit, setFieldError, validateForm, ...formikProps }) => { const { + ppmType, hasSecondaryDestination, hasTertiaryDestination, hasDeliveryAddress, @@ -663,8 +673,16 @@ const ShipmentForm = (props) => { hasSecondaryDelivery, hasTertiaryPickup, hasTertiaryDelivery, - isActualExpenseReimbursement, } = values; + + const isCivilian = serviceMember?.grade === 'CIVILIAN_EMPLOYEE'; + if (!ppmType) { + const type = isCivilian ? PPM_TYPES.ACTUAL_EXPENSE : PPM_TYPES.INCENTIVE_BASED; + setValues({ + ...values, + ppmType: type, + }); + } const lengthHasError = !!( (formikProps.touched.lengthFeet && formikProps.errors.lengthFeet === 'Required') || (formikProps.touched.lengthInches && formikProps.errors.lengthFeet === 'Required') @@ -858,9 +876,14 @@ const ShipmentForm = (props) => {
- {isActualExpenseReimbursement === 'true' && ( + {ppmType === PPM_TYPES.SMALL_PACKAGE && ( + + {getPPMTypeLabel(ppmType)} + + )} + {ppmType === PPM_TYPES.ACTUAL_EXPENSE && ( - Actual Expense Reimbursement + {getPPMTypeLabel(ppmType)} )} @@ -926,7 +949,7 @@ const ShipmentForm = (props) => { {showPickupFields && ( -

Pickup details

+

Pickup details

{isRequestedPickupDateAlertVisible && ( @@ -1077,7 +1100,7 @@ const ShipmentForm = (props) => { {showDeliveryFields && ( -

Delivery details

+

Delivery details

{isRequestedDeliveryDateAlertVisible && ( @@ -1428,42 +1451,61 @@ const ShipmentForm = (props) => { {isPPM && !isAdvancePage && ( <> {isServiceCounselor && ( - -

Actual Expense Reimbursement

+ +

PPM Type

-
)} -

Departure date

- +

{ppmType === PPM_TYPES.SMALL_PACKAGE ? 'Shipped Date' : 'Departure Date'}

+ Enter the first day you expect to move things. It's OK if the actual date is different. We will ask for your actual departure date when you document and complete your PPM. @@ -1688,7 +1730,7 @@ const ShipmentForm = (props) => {
{showCloseoutOffice && ( -

Closeout office

+

Closeout office

{ describe('creating a new PPM shipment', () => { it('displays PPM content', async () => { + isBooleanFlagEnabled.mockImplementation(() => Promise.resolve(true)); + renderWithRouter( { ); expect(await screen.findByTestId('tag')).toHaveTextContent('PPM'); + const ppmTypeSection = await screen.findByTestId('ppmTypeSection'); + expect(ppmTypeSection).toBeVisible(); + + const incentiveRadio = screen.getByTestId('isIncentiveBased'); + expect(incentiveRadio).toBeInTheDocument(); + expect(incentiveRadio).toHaveAttribute('value', PPM_TYPES.INCENTIVE_BASED); + + const actualExpenseRadio = screen.getByTestId('isActualExpense'); + expect(actualExpenseRadio).toBeInTheDocument(); + expect(actualExpenseRadio).toHaveAttribute('value', PPM_TYPES.ACTUAL_EXPENSE); + + const smallPackageRadio = screen.getByTestId('isSmallPackage'); + expect(smallPackageRadio).toBeInTheDocument(); + expect(smallPackageRadio).toHaveAttribute('value', PPM_TYPES.SMALL_PACKAGE); }); it('PPM - delivery address street 1 is OPTIONAL', async () => { @@ -1493,6 +1512,7 @@ describe('ShipmentForm component', () => { isCreatePage={false} shipmentType={SHIPMENT_OPTIONS.PPM} mtoShipment={mockPPMShipment} + userRole={roleTypes.SERVICES_COUNSELOR} />, ); @@ -1562,13 +1582,14 @@ describe('ShipmentForm component', () => { mockPPMShipment.ppmShipment.secondaryDestinationAddress.postalCode, ); - expect(screen.getAllByLabelText('Yes')[0]).toBeChecked(); - expect(screen.getAllByLabelText('No')[0]).not.toBeChecked(); - expect(screen.getAllByLabelText('Yes')[1]).toBeChecked(); - expect(screen.getAllByLabelText('No')[1]).not.toBeChecked(); + expect(screen.getAllByLabelText('Incentive-based')[0]).toBeChecked(); + expect(screen.getAllByLabelText('Actual Expense Reimbursement')[0]).not.toBeChecked(); + await waitFor(() => { + expect(screen.getAllByLabelText('Small Package Reimbursement')[0]).not.toBeChecked(); + }); expect(screen.getByLabelText('Estimated PPM weight')).toHaveValue('4,999'); - expect(screen.getAllByLabelText('Yes')[3]).toBeChecked(); - expect(screen.getAllByLabelText('No')[3]).not.toBeChecked(); + expect(screen.getAllByLabelText('Yes')[0]).toBeChecked(); + expect(screen.getAllByLabelText('No')[1]).toBeChecked(); }); it('test delivery address street 1 is OPTIONAL', async () => { @@ -1898,8 +1919,8 @@ describe('ShipmentForm component', () => { />, ); + expect(screen.getByText('PPM Type')).toBeInTheDocument(); expect(await screen.findByTestId('tag')).toHaveTextContent('PPM'); - expect(screen.getByText('Is this PPM an Actual Expense Reimbursement?')).toBeInTheDocument(); expect(screen.getByText('What address are you moving from?')).toBeInTheDocument(); expect(screen.getByText('Second Pickup Address')).toBeInTheDocument(); expect( diff --git a/src/components/Office/ShipmentForm/ppmShipmentSchema.js b/src/components/Office/ShipmentForm/ppmShipmentSchema.js index 5eafe4b09ad..9b9bfcaad01 100644 --- a/src/components/Office/ShipmentForm/ppmShipmentSchema.js +++ b/src/components/Office/ShipmentForm/ppmShipmentSchema.js @@ -4,6 +4,7 @@ import { getFormattedMaxAdvancePercentage } from 'utils/incentives'; import { requiredAddressSchema, partialRequiredAddressSchema } from 'utils/validation'; import { OptionalAddressSchema } from 'components/Customer/MtoShipmentForm/validationSchemas'; import { ADVANCE_STATUSES } from 'constants/ppms'; +import { PPM_TYPES } from 'shared/constants'; function closeoutOfficeSchema(showCloseoutOffice, isAdvancePage) { if (showCloseoutOffice && !isAdvancePage) { @@ -28,6 +29,9 @@ const ppmShipmentSchema = ({ const proGearSpouseWeightLimit = weightAllotment.proGearWeightSpouse || 0; const formSchema = Yup.object().shape({ + ppmType: Yup.string() + .oneOf([PPM_TYPES.INCENTIVE_BASED, PPM_TYPES.ACTUAL_EXPENSE, PPM_TYPES.SMALL_PACKAGE], 'Invalid PPM Type') + .required('Required'), pickup: Yup.object().shape({ address: requiredAddressSchema, }), @@ -126,7 +130,6 @@ const ppmShipmentSchema = ({ (isAdvancePage && ADVANCE_STATUSES[advanceStatus] === ADVANCE_STATUSES.REJECTED), then: (schema) => schema.required('Required'), }), - isActualExpenseReimbursement: Yup.boolean().required('Required'), }); return formSchema; diff --git a/src/components/Office/ShipmentWeight/ShipmentWeight.jsx b/src/components/Office/ShipmentWeight/ShipmentWeight.jsx index b6b1f531b43..7d56aaa800b 100644 --- a/src/components/Office/ShipmentWeight/ShipmentWeight.jsx +++ b/src/components/Office/ShipmentWeight/ShipmentWeight.jsx @@ -31,7 +31,7 @@ const ShipmentWeight = ({ onEstimatedWeightChange }) => { return (
-

Weight

+

Weight

diff --git a/src/components/RegistrationConfirmationModal/RegistrationConfirmation.stories.jsx b/src/components/RegistrationConfirmationModal/RegistrationConfirmation.stories.jsx new file mode 100644 index 00000000000..7d0818f0c84 --- /dev/null +++ b/src/components/RegistrationConfirmationModal/RegistrationConfirmation.stories.jsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { action } from '@storybook/addon-actions'; + +import { RegistrationConfirmationModal } from './RegistrationConfirmationModal'; + +export default { + title: 'Components/Registration Confirmation Modal', + component: RegistrationConfirmationModal, +}; + +const props = { + onSubmit: action('clicked'), +}; + +export const RegistrationConfirmation = () => ; diff --git a/src/components/RegistrationConfirmationModal/RegistrationConfirmationModal.jsx b/src/components/RegistrationConfirmationModal/RegistrationConfirmationModal.jsx new file mode 100644 index 00000000000..04c4b53004c --- /dev/null +++ b/src/components/RegistrationConfirmationModal/RegistrationConfirmationModal.jsx @@ -0,0 +1,38 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Button } from '@trussworks/react-uswds'; + +import styles from './RegistrationConfirmationModal.module.scss'; + +import Modal, { ModalActions, ModalTitle, connectModal } from 'components/Modal/Modal'; + +export const RegistrationConfirmationModal = ({ onSubmit }) => { + return ( + + +

Registration Confirmation

+
+

+ Your MilMove & Okta accounts have successfully been created.
+ It is required that you first sign-in with a Common Access Card (CAC).
+
+ You will now be redirected to the Okta sign-in page where you will click on the "Sign in with PIV / CAC + Card" button to sign in. +
+

+ + + +
+ ); +}; + +RegistrationConfirmationModal.propTypes = { + onSubmit: PropTypes.func.isRequired, +}; + +RegistrationConfirmationModal.displayName = 'RegistrationConfirmationModal'; + +export default connectModal(RegistrationConfirmationModal); diff --git a/src/components/RegistrationConfirmationModal/RegistrationConfirmationModal.module.scss b/src/components/RegistrationConfirmationModal/RegistrationConfirmationModal.module.scss new file mode 100644 index 00000000000..350e337494a --- /dev/null +++ b/src/components/RegistrationConfirmationModal/RegistrationConfirmationModal.module.scss @@ -0,0 +1,6 @@ + .center { + justify-content: center; + align-items: center; + display: flex; + text-align: center; + } \ No newline at end of file diff --git a/src/components/RegistrationConfirmationModal/RegistrationConfirmationModal.test.jsx b/src/components/RegistrationConfirmationModal/RegistrationConfirmationModal.test.jsx new file mode 100644 index 00000000000..5edb295e497 --- /dev/null +++ b/src/components/RegistrationConfirmationModal/RegistrationConfirmationModal.test.jsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { fireEvent, render, screen } from '@testing-library/react'; + +import '@testing-library/jest-dom/extend-expect'; +import { RegistrationConfirmationModal } from './RegistrationConfirmationModal'; + +describe('RegistrationConfirmationModal', () => { + const onSubmitMock = jest.fn(); + + beforeEach(() => { + onSubmitMock.mockClear(); + }); + + it('renders the confirmation modal with expected text and button', () => { + render(); + + expect(screen.getByText('Registration Confirmation')).toBeInTheDocument(); + expect(screen.getByText(/Your MilMove & Okta accounts have successfully been created/i)).toBeInTheDocument(); + const continueButton = screen.getByTestId('modalSubmitButton'); + expect(continueButton).toBeInTheDocument(); + }); + + it('calls onSubmit when the Continue button is clicked', () => { + render(); + + const continueButton = screen.getByTestId('modalSubmitButton'); + fireEvent.click(continueButton); + + expect(onSubmitMock).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/components/StyledLine/StyledLine.jsx b/src/components/StyledLine/StyledLine.jsx new file mode 100644 index 00000000000..2dfadb30446 --- /dev/null +++ b/src/components/StyledLine/StyledLine.jsx @@ -0,0 +1,17 @@ +import React from 'react'; + +import styles from './StyledLine.module.scss'; + +export const StyledLine = ({ width, color, className }) => { + return ( +
+ ); +}; + +export default StyledLine; diff --git a/src/components/StyledLine/StyledLine.module.scss b/src/components/StyledLine/StyledLine.module.scss new file mode 100644 index 00000000000..24b5b66176b --- /dev/null +++ b/src/components/StyledLine/StyledLine.module.scss @@ -0,0 +1,7 @@ +.styledLine { + height: 2px; + margin: auto; + margin-top: 3vh; + margin-bottom: 1vh; + border-radius: 1px; + } diff --git a/src/components/StyledLine/StyledLine.test.jsx b/src/components/StyledLine/StyledLine.test.jsx new file mode 100644 index 00000000000..bc0b48ea14a --- /dev/null +++ b/src/components/StyledLine/StyledLine.test.jsx @@ -0,0 +1,37 @@ +// StyledLine.test.js +import React from 'react'; +import { render } from '@testing-library/react'; + +import { StyledLine } from './StyledLine'; + +describe('StyledLine', () => { + it('renders with default styles when no props are provided', () => { + const { container } = render(); + const div = container.firstChild; + + expect(div).toHaveStyle('width: 75%'); + expect(div).toHaveStyle('background-color: #565c65'); + + expect(div.className).toBeTruthy(); + }); + + it('renders with provided inline styles', () => { + const customWidth = '50%'; + const customColor = '#ff0000'; + const { container } = render(); + const div = container.firstChild; + + expect(div).toHaveStyle(`width: ${customWidth}`); + expect(div).toHaveStyle(`background-color: ${customColor}`); + }); + + it('uses the custom className if provided, overriding the default', () => { + const customClass = 'my-custom-line'; + const { container } = render(); + const div = container.firstChild; + + // the component renders the className as: `${className || styles.styledLine}`. + // so when customClass is provided, it should be the only class applied. + expect(div.className).toBe(customClass); + }); +}); diff --git a/src/components/ValidCACModal/ValidCACModal.jsx b/src/components/ValidCACModal/ValidCACModal.jsx new file mode 100644 index 00000000000..6fd2968e492 --- /dev/null +++ b/src/components/ValidCACModal/ValidCACModal.jsx @@ -0,0 +1,42 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Button } from '@trussworks/react-uswds'; + +import styles from '../../pages/SignUp/SignUp.module.scss'; + +import smartCard from 'shared/images/smart-card.png'; +import Modal, { ModalTitle, ModalActions, connectModal } from 'components/Modal/Modal'; + +export const ValidCACModal = ({ onClose, onSubmit }) => ( + + +

Do you have a valid CAC?

+
+

+ +

+

+ Common Access Card (CAC) authentication is required at first sign-in.
+ If you do not have a CAC, do not request your account here.
+ You must visit your nearest personal property office where they will assist you with creating your MilMove + account. +

+ + + + +
+); + +ValidCACModal.propTypes = { + onClose: PropTypes.func.isRequired, + onSubmit: PropTypes.func.isRequired, +}; + +ValidCACModal.displayName = 'ValidCACModal'; + +export default connectModal(ValidCACModal); diff --git a/src/components/ValidCACModal/ValidCACModal.stories.jsx b/src/components/ValidCACModal/ValidCACModal.stories.jsx new file mode 100644 index 00000000000..440d52e1ab6 --- /dev/null +++ b/src/components/ValidCACModal/ValidCACModal.stories.jsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { action } from '@storybook/addon-actions'; + +import { ValidCACModal } from './ValidCACModal'; + +export default { + title: 'Components/Valid CAC Modal', + component: ValidCACModal, +}; + +const props = { + onClose: action('clicked'), + onSubmit: action('clicked'), +}; + +export const ValidCAC = () => ; diff --git a/src/components/ValidCACModal/ValidCACModal.test.jsx b/src/components/ValidCACModal/ValidCACModal.test.jsx new file mode 100644 index 00000000000..b684ac66f4a --- /dev/null +++ b/src/components/ValidCACModal/ValidCACModal.test.jsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; + +import { ValidCACModal } from './ValidCACModal'; + +describe('ValidCACModal', () => { + const onCloseMock = jest.fn(); + const onSubmitMock = jest.fn(); + + beforeEach(() => { + onCloseMock.mockClear(); + onSubmitMock.mockClear(); + }); + + it('renders the modal with title, image, and description', () => { + render(); + + const heading = screen.getByRole('heading', { name: /do you have a valid cac\?/i }); + expect(heading).toBeInTheDocument(); + + const image = screen.getByRole('img'); + expect(image).toBeInTheDocument(); + + expect( + screen.getByText(/Common Access Card \(CAC\) authentication is required at first sign-in/i), + ).toBeInTheDocument(); + }); + + it('calls onSubmit when the "Yes" button is clicked', () => { + render(); + + const yesButton = screen.getByTestId('modalSubmitButton'); + fireEvent.click(yesButton); + + expect(onSubmitMock).toHaveBeenCalledTimes(1); + }); + + it('calls onClose when the "No" button is clicked', () => { + render(); + + const noButton = screen.getByTestId('modalBackButton'); + fireEvent.click(noButton); + + expect(onCloseMock).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/components/form/fields/TextField/TextField.jsx b/src/components/form/fields/TextField/TextField.jsx index 7bda906105c..bc8f8385210 100644 --- a/src/components/form/fields/TextField/TextField.jsx +++ b/src/components/form/fields/TextField/TextField.jsx @@ -35,6 +35,7 @@ const TextField = ({ isDisabled, display, button, + disablePaste, ...inputProps }) => { const [fieldProps, metaProps] = useField({ name, validate, type }); @@ -45,10 +46,14 @@ const TextField = ({ warning: showWarning, }); + const pasteHandler = disablePaste ? (e) => e.preventDefault() : undefined; + const getDisplay = (displayType) => { switch (displayType) { case 'textarea': - return