diff --git a/migrations/app/migrations_manifest.txt b/migrations/app/migrations_manifest.txt index cd65e926c4a..2eaef4d57a5 100644 --- a/migrations/app/migrations_manifest.txt +++ b/migrations/app/migrations_manifest.txt @@ -1060,6 +1060,7 @@ 20241217180136_add_AK_zips_to_zip3_distances.up.sql 20241217191012_update_move_to_gbloc_for_ak.up.sql 20241218201833_add_PPPO_BASE_ELIZABETH.up.sql +20241218204620_add_international_nts_service_items.up.sql 20241220171035_add_additional_AK_zips_to_zip3_distances.up.sql 20241220213134_add_destination_gbloc_db_function.up.sql 20241224172258_add_and_update_po_box_zip.up.sql diff --git a/migrations/app/schema/20241218204620_add_international_nts_service_items.up.sql b/migrations/app/schema/20241218204620_add_international_nts_service_items.up.sql new file mode 100644 index 00000000000..207c5379080 --- /dev/null +++ b/migrations/app/schema/20241218204620_add_international_nts_service_items.up.sql @@ -0,0 +1,36 @@ +-- +-- Add service items for international NTS shipments. +-- +INSERT INTO re_service_items +(id, service_id, shipment_type, market_code, is_auto_approved, created_at, updated_at, sort) +VALUES + --ISLH International Shipping & Linehaul + ('2a560507-db09-4be1-b809-49c0f515b31b', '9f3d551a-0725-430e-897e-80ee9add3ae9' ,'HHG_INTO_NTS', 'i', true, now(), now(), 1), + --PODFSC International POD Fuel Surcharge + ('e702818f-defd-452c-81a3-865b902e7dd0', '388115e8-abe9-441d-96cf-a39f24baa0a3' ,'HHG_INTO_NTS', 'i', true, now(), now(), 2), + --INPK International NTS packing + ('366ee5a4-eb61-4ded-a68c-ddc29fe1a886', '874cb86a-bc39-4f57-a614-53ee3fcacf14' ,'HHG_INTO_NTS', 'i', true, now(), now(), 3), + --ICRT International crating + ('aac4e95e-27ed-4f09-9b6b-384d8542f410', '86203d72-7f7c-49ff-82f0-5b95e4958f60' ,'HHG_INTO_NTS', 'i', false, now(), now(), NULL), + --IDASIT International destination add'l day SIT + ('010f2f91-7381-4149-8d74-8eb5f593a864', '806c6d59-57ff-4a3f-9518-ebf29ba9cb10' ,'HHG_INTO_NTS', 'i', false, now(), now(), NULL), + --IDDSIT International destination SIT delivery + ('a41966b7-b83a-4eaf-8e68-d5e884777102', '28389ee1-56cf-400c-aa52-1501ecdd7c69' ,'HHG_INTO_NTS', 'i', false, now(), now(), NULL), + --IDFSIT International destination 1st day SIT + ('14c77957-3c76-465a-bb07-c98d36ef1e54', 'bd6064ca-e780-4ab4-a37b-0ae98eebb244' ,'HHG_INTO_NTS', 'i', false, now(), now(), NULL), + --IDSHUT International destination shuttle service + ('d52d2d03-100a-4ed9-b2de-16eac63a375f', '22fc07ed-be15-4f50-b941-cbd38153b378' ,'HHG_INTO_NTS', 'i', false, now(), now(), NULL), + --IOASIT International origin add'l day SIT + ('7fd91408-7d69-4375-b7e6-5b2ff714206b', 'bd424e45-397b-4766-9712-de4ae3a2da36' ,'HHG_INTO_NTS', 'i', false, now(), now(), NULL), + --IOFSIT International origin 1st day SIT + ('b3dc509d-d652-4300-a702-a1ddce6255b6', 'b488bf85-ea5e-49c8-ba5c-e2fa278ac806' ,'HHG_INTO_NTS', 'i', false, now(), now(), NULL), + --IOPSIT International origin SIT pickup + ('001eadb6-3526-45b9-96e0-0648bb481e86', '6f4f6e31-0675-4051-b659-89832259f390' ,'HHG_INTO_NTS', 'i', false, now(), now(), NULL), + --IOSHUT International origin shuttle service + ('b991c5c9-af2c-4146-b999-1d0bdf91de3f', '624a97c5-dfbf-4da9-a6e9-526b4f95af8d' ,'HHG_INTO_NTS', 'i', false, now(), now(), NULL), + --IUCRT International uncrating + ('5a89315a-257b-4ef0-92cb-4c06aa6f1332', '4132416b-b1aa-42e7-98f2-0ac0a03e8a31' ,'HHG_INTO_NTS', 'i', false, now(), now(), NULL), + --IOFSC International Origin SIT Fuel Surcharge + ('d4a98dea-a5f7-4b92-b5de-e6350ab07824', '81e29d0c-02a6-4a7a-be02-554deb3ee49e' ,'HHG_INTO_NTS', 'i', false, now(), now(), NULL), + --IDSFSC International Destination SIT Fuel Surcharge + ('eaea90c2-93d3-4db9-89cd-23ac57ec9ce1', '690a5fc1-0ea5-4554-8294-a367b5daefa9' ,'HHG_INTO_NTS', 'i', false, now(), now(), NULL); diff --git a/pkg/factory/mto_shipment_factory.go b/pkg/factory/mto_shipment_factory.go index 1c7858d572f..025a94fc09c 100644 --- a/pkg/factory/mto_shipment_factory.go +++ b/pkg/factory/mto_shipment_factory.go @@ -57,7 +57,7 @@ func buildMTOShipmentWithBuildType(db *pop.Connection, customs []Customization, defaultStatus = models.MTOShipmentStatusDraft buildStorageFacility = hasStorageFacilityCustom shipmentHasPickupDetails = true - shipmentHasDeliveryDetails = false + shipmentHasDeliveryDetails = true case mtoShipmentNTSR: defaultShipmentType = models.MTOShipmentTypeHHGOutOfNTSDom defaultStatus = models.MTOShipmentStatusDraft @@ -83,6 +83,10 @@ func buildMTOShipmentWithBuildType(db *pop.Connection, customs []Customization, MarketCode: defaultMarketCode, } + if newMTOShipment.ShipmentType == models.MTOShipmentTypeHHGIntoNTS && newMTOShipment.StorageFacility != nil { + newMTOShipment.DestinationAddress = &newMTOShipment.StorageFacility.Address + } + if cMtoShipment.Status == models.MTOShipmentStatusApproved { approvedDate := time.Date(GHCTestYear, time.March, 20, 0, 0, 0, 0, time.UTC) newMTOShipment.ApprovedDate = &approvedDate diff --git a/pkg/factory/mto_shipment_factory_test.go b/pkg/factory/mto_shipment_factory_test.go index ea43a0a0373..60403a50f0d 100644 --- a/pkg/factory/mto_shipment_factory_test.go +++ b/pkg/factory/mto_shipment_factory_test.go @@ -450,9 +450,9 @@ func (suite *FactorySuite) TestBuildMTOShipment() { suite.NotNil(ntsShipment.PrimeActualWeight) suite.Nil(ntsShipment.StorageFacility) suite.NotNil(ntsShipment.ScheduledPickupDate) - suite.Nil(ntsShipment.RequestedDeliveryDate) + suite.NotNil(ntsShipment.RequestedDeliveryDate) suite.Nil(ntsShipment.ActualDeliveryDate) - suite.Nil(ntsShipment.ScheduledDeliveryDate) + suite.NotNil(ntsShipment.ScheduledDeliveryDate) }) suite.Run("Successful creation of NTSShipment with storage facility", func() { diff --git a/pkg/gen/ghcapi/configure_mymove.go b/pkg/gen/ghcapi/configure_mymove.go index 08d71eb7034..031aea592b9 100644 --- a/pkg/gen/ghcapi/configure_mymove.go +++ b/pkg/gen/ghcapi/configure_mymove.go @@ -477,6 +477,11 @@ func configureAPI(api *ghcoperations.MymoveAPI) http.Handler { return middleware.NotImplemented("operation ppm.ShowAOAPacket has not yet been implemented") }) } + if api.TransportationOfficeShowCounselingOfficesHandler == nil { + api.TransportationOfficeShowCounselingOfficesHandler = transportation_office.ShowCounselingOfficesHandlerFunc(func(params transportation_office.ShowCounselingOfficesParams) middleware.Responder { + return middleware.NotImplemented("operation transportation_office.ShowCounselingOffices has not yet been implemented") + }) + } if api.PpmShowPaymentPacketHandler == nil { api.PpmShowPaymentPacketHandler = ppm.ShowPaymentPacketHandlerFunc(func(params ppm.ShowPaymentPacketParams) middleware.Responder { return middleware.NotImplemented("operation ppm.ShowPaymentPacket has not yet been implemented") diff --git a/pkg/gen/ghcapi/embedded_spec.go b/pkg/gen/ghcapi/embedded_spec.go index 94669c6c750..7be8690ecf0 100644 --- a/pkg/gen/ghcapi/embedded_spec.go +++ b/pkg/gen/ghcapi/embedded_spec.go @@ -6412,7 +6412,7 @@ func init() { "operationId": "getTransportationOfficesGBLOCs", "responses": { "200": { - "description": "Successfully retrieved transportation offices", + "description": "Successfully retrieved GBLOCs", "schema": { "$ref": "#/definitions/GBLOCs" } @@ -6435,6 +6435,40 @@ func init() { } } }, + "/transportation_offices/{dutyLocationId}/counseling_offices": { + "get": { + "description": "Returns the counseling locations matching the GBLOC from the selected duty location", + "produces": [ + "application/json" + ], + "tags": [ + "transportationOffice" + ], + "summary": "Returns the counseling locations in the GBLOC matching the duty location", + "operationId": "showCounselingOffices", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "UUID of the duty location", + "name": "dutyLocationId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Successfully retrieved counseling offices", + "schema": { + "$ref": "#/definitions/CounselingOffices" + } + }, + "500": { + "description": "internal server error" + } + } + } + }, "/uploads": { "post": { "description": "Uploads represent a single digital file, such as a JPEG or PDF. Currently, office application uploads are only for Services Counselors to upload files for orders, but this may be expanded in the future.", @@ -7151,6 +7185,30 @@ func init() { } } }, + "CounselingOffice": { + "type": "object", + "required": [ + "id", + "name" + ], + "properties": { + "id": { + "type": "string", + "format": "uuid", + "example": "c56a4180-65aa-42ec-a945-5fd21dec0538" + }, + "name": { + "type": "string", + "example": "Fort Bragg North Station" + } + } + }, + "CounselingOffices": { + "type": "array", + "items": { + "$ref": "#/definitions/CounselingOffice" + } + }, "CounselingUpdateAllowancePayload": { "type": "object", "properties": { @@ -7769,6 +7827,12 @@ func init() { "x-nullable": true, "example": true }, + "counselingOfficeId": { + "type": "string", + "format": "uuid", + "x-nullable": true, + "example": "cf1addea-a4f9-4173-8506-2bb82a064cb7" + }, "departmentIndicator": { "$ref": "#/definitions/DeptIndicator" }, @@ -23619,7 +23683,7 @@ func init() { "operationId": "getTransportationOfficesGBLOCs", "responses": { "200": { - "description": "Successfully retrieved transportation offices", + "description": "Successfully retrieved GBLOCs", "schema": { "$ref": "#/definitions/GBLOCs" } @@ -23657,6 +23721,40 @@ func init() { } } }, + "/transportation_offices/{dutyLocationId}/counseling_offices": { + "get": { + "description": "Returns the counseling locations matching the GBLOC from the selected duty location", + "produces": [ + "application/json" + ], + "tags": [ + "transportationOffice" + ], + "summary": "Returns the counseling locations in the GBLOC matching the duty location", + "operationId": "showCounselingOffices", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "UUID of the duty location", + "name": "dutyLocationId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Successfully retrieved counseling offices", + "schema": { + "$ref": "#/definitions/CounselingOffices" + } + }, + "500": { + "description": "internal server error" + } + } + } + }, "/uploads": { "post": { "description": "Uploads represent a single digital file, such as a JPEG or PDF. Currently, office application uploads are only for Services Counselors to upload files for orders, but this may be expanded in the future.", @@ -24389,6 +24487,30 @@ func init() { } } }, + "CounselingOffice": { + "type": "object", + "required": [ + "id", + "name" + ], + "properties": { + "id": { + "type": "string", + "format": "uuid", + "example": "c56a4180-65aa-42ec-a945-5fd21dec0538" + }, + "name": { + "type": "string", + "example": "Fort Bragg North Station" + } + } + }, + "CounselingOffices": { + "type": "array", + "items": { + "$ref": "#/definitions/CounselingOffice" + } + }, "CounselingUpdateAllowancePayload": { "type": "object", "properties": { @@ -25011,6 +25133,12 @@ func init() { "x-nullable": true, "example": true }, + "counselingOfficeId": { + "type": "string", + "format": "uuid", + "x-nullable": true, + "example": "cf1addea-a4f9-4173-8506-2bb82a064cb7" + }, "departmentIndicator": { "$ref": "#/definitions/DeptIndicator" }, diff --git a/pkg/gen/ghcapi/ghcoperations/mymove_api.go b/pkg/gen/ghcapi/ghcoperations/mymove_api.go index 61bda14b7d3..69d6706ee5e 100644 --- a/pkg/gen/ghcapi/ghcoperations/mymove_api.go +++ b/pkg/gen/ghcapi/ghcoperations/mymove_api.go @@ -314,6 +314,9 @@ func NewMymoveAPI(spec *loads.Document) *MymoveAPI { PpmShowAOAPacketHandler: ppm.ShowAOAPacketHandlerFunc(func(params ppm.ShowAOAPacketParams) middleware.Responder { return middleware.NotImplemented("operation ppm.ShowAOAPacket has not yet been implemented") }), + TransportationOfficeShowCounselingOfficesHandler: transportation_office.ShowCounselingOfficesHandlerFunc(func(params transportation_office.ShowCounselingOfficesParams) middleware.Responder { + return middleware.NotImplemented("operation transportation_office.ShowCounselingOffices has not yet been implemented") + }), PpmShowPaymentPacketHandler: ppm.ShowPaymentPacketHandlerFunc(func(params ppm.ShowPaymentPacketParams) middleware.Responder { return middleware.NotImplemented("operation ppm.ShowPaymentPacket has not yet been implemented") }), @@ -606,6 +609,8 @@ type MymoveAPI struct { MoveSetFinancialReviewFlagHandler move.SetFinancialReviewFlagHandler // PpmShowAOAPacketHandler sets the operation handler for the show a o a packet operation PpmShowAOAPacketHandler ppm.ShowAOAPacketHandler + // TransportationOfficeShowCounselingOfficesHandler sets the operation handler for the show counseling offices operation + TransportationOfficeShowCounselingOfficesHandler transportation_office.ShowCounselingOfficesHandler // PpmShowPaymentPacketHandler sets the operation handler for the show payment packet operation PpmShowPaymentPacketHandler ppm.ShowPaymentPacketHandler // EvaluationReportsSubmitEvaluationReportHandler sets the operation handler for the submit evaluation report operation @@ -988,6 +993,9 @@ func (o *MymoveAPI) Validate() error { if o.PpmShowAOAPacketHandler == nil { unregistered = append(unregistered, "ppm.ShowAOAPacketHandler") } + if o.TransportationOfficeShowCounselingOfficesHandler == nil { + unregistered = append(unregistered, "transportation_office.ShowCounselingOfficesHandler") + } if o.PpmShowPaymentPacketHandler == nil { unregistered = append(unregistered, "ppm.ShowPaymentPacketHandler") } @@ -1491,6 +1499,10 @@ func (o *MymoveAPI) initHandlerCache() { if o.handlers["GET"] == nil { o.handlers["GET"] = make(map[string]http.Handler) } + o.handlers["GET"]["/transportation_offices/{dutyLocationId}/counseling_offices"] = transportation_office.NewShowCounselingOffices(o.context, o.TransportationOfficeShowCounselingOfficesHandler) + if o.handlers["GET"] == nil { + o.handlers["GET"] = make(map[string]http.Handler) + } o.handlers["GET"]["/ppm-shipments/{ppmShipmentId}/payment-packet"] = ppm.NewShowPaymentPacket(o.context, o.PpmShowPaymentPacketHandler) if o.handlers["POST"] == nil { o.handlers["POST"] = make(map[string]http.Handler) diff --git a/pkg/gen/ghcapi/ghcoperations/transportation_office/get_transportation_offices_g_b_l_o_cs_responses.go b/pkg/gen/ghcapi/ghcoperations/transportation_office/get_transportation_offices_g_b_l_o_cs_responses.go index 309de84d0fa..c630be03fd6 100644 --- a/pkg/gen/ghcapi/ghcoperations/transportation_office/get_transportation_offices_g_b_l_o_cs_responses.go +++ b/pkg/gen/ghcapi/ghcoperations/transportation_office/get_transportation_offices_g_b_l_o_cs_responses.go @@ -17,7 +17,7 @@ import ( const GetTransportationOfficesGBLOCsOKCode int = 200 /* -GetTransportationOfficesGBLOCsOK Successfully retrieved transportation offices +GetTransportationOfficesGBLOCsOK Successfully retrieved GBLOCs swagger:response getTransportationOfficesGBLOCsOK */ diff --git a/pkg/gen/ghcapi/ghcoperations/transportation_office/show_counseling_offices.go b/pkg/gen/ghcapi/ghcoperations/transportation_office/show_counseling_offices.go new file mode 100644 index 00000000000..5b4c1967e43 --- /dev/null +++ b/pkg/gen/ghcapi/ghcoperations/transportation_office/show_counseling_offices.go @@ -0,0 +1,58 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package transportation_office + +// 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" +) + +// ShowCounselingOfficesHandlerFunc turns a function with the right signature into a show counseling offices handler +type ShowCounselingOfficesHandlerFunc func(ShowCounselingOfficesParams) middleware.Responder + +// Handle executing the request and returning a response +func (fn ShowCounselingOfficesHandlerFunc) Handle(params ShowCounselingOfficesParams) middleware.Responder { + return fn(params) +} + +// ShowCounselingOfficesHandler interface for that can handle valid show counseling offices params +type ShowCounselingOfficesHandler interface { + Handle(ShowCounselingOfficesParams) middleware.Responder +} + +// NewShowCounselingOffices creates a new http.Handler for the show counseling offices operation +func NewShowCounselingOffices(ctx *middleware.Context, handler ShowCounselingOfficesHandler) *ShowCounselingOffices { + return &ShowCounselingOffices{Context: ctx, Handler: handler} +} + +/* + ShowCounselingOffices swagger:route GET /transportation_offices/{dutyLocationId}/counseling_offices transportationOffice showCounselingOffices + +# Returns the counseling locations in the GBLOC matching the duty location + +Returns the counseling locations matching the GBLOC from the selected duty location +*/ +type ShowCounselingOffices struct { + Context *middleware.Context + Handler ShowCounselingOfficesHandler +} + +func (o *ShowCounselingOffices) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + route, rCtx, _ := o.Context.RouteInfo(r) + if rCtx != nil { + *r = *rCtx + } + var Params = NewShowCounselingOfficesParams() + if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params + o.Context.Respond(rw, r, route.Produces, route, err) + return + } + + res := o.Handler.Handle(Params) // actually handle the request + o.Context.Respond(rw, r, route.Produces, route, res) + +} diff --git a/pkg/gen/ghcapi/ghcoperations/transportation_office/show_counseling_offices_parameters.go b/pkg/gen/ghcapi/ghcoperations/transportation_office/show_counseling_offices_parameters.go new file mode 100644 index 00000000000..24f4c585bfd --- /dev/null +++ b/pkg/gen/ghcapi/ghcoperations/transportation_office/show_counseling_offices_parameters.go @@ -0,0 +1,91 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package transportation_office + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime/middleware" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/validate" +) + +// NewShowCounselingOfficesParams creates a new ShowCounselingOfficesParams object +// +// There are no default values defined in the spec. +func NewShowCounselingOfficesParams() ShowCounselingOfficesParams { + + return ShowCounselingOfficesParams{} +} + +// ShowCounselingOfficesParams contains all the bound params for the show counseling offices operation +// typically these are obtained from a http.Request +// +// swagger:parameters showCounselingOffices +type ShowCounselingOfficesParams struct { + + // HTTP Request Object + HTTPRequest *http.Request `json:"-"` + + /*UUID of the duty location + Required: true + In: path + */ + DutyLocationID strfmt.UUID +} + +// 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 NewShowCounselingOfficesParams() beforehand. +func (o *ShowCounselingOfficesParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error { + var res []error + + o.HTTPRequest = r + + rDutyLocationID, rhkDutyLocationID, _ := route.Params.GetOK("dutyLocationId") + if err := o.bindDutyLocationID(rDutyLocationID, rhkDutyLocationID, route.Formats); err != nil { + res = append(res, err) + } + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +// bindDutyLocationID binds and validates parameter DutyLocationID from path. +func (o *ShowCounselingOfficesParams) bindDutyLocationID(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 + + // Format: uuid + value, err := formats.Parse("uuid", raw) + if err != nil { + return errors.InvalidType("dutyLocationId", "path", "strfmt.UUID", raw) + } + o.DutyLocationID = *(value.(*strfmt.UUID)) + + if err := o.validateDutyLocationID(formats); err != nil { + return err + } + + return nil +} + +// validateDutyLocationID carries on validations for parameter DutyLocationID +func (o *ShowCounselingOfficesParams) validateDutyLocationID(formats strfmt.Registry) error { + + if err := validate.FormatOf("dutyLocationId", "path", "uuid", o.DutyLocationID.String(), formats); err != nil { + return err + } + return nil +} diff --git a/pkg/gen/ghcapi/ghcoperations/transportation_office/show_counseling_offices_responses.go b/pkg/gen/ghcapi/ghcoperations/transportation_office/show_counseling_offices_responses.go new file mode 100644 index 00000000000..73793566c47 --- /dev/null +++ b/pkg/gen/ghcapi/ghcoperations/transportation_office/show_counseling_offices_responses.go @@ -0,0 +1,87 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package transportation_office + +// 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/ghcmessages" +) + +// ShowCounselingOfficesOKCode is the HTTP code returned for type ShowCounselingOfficesOK +const ShowCounselingOfficesOKCode int = 200 + +/* +ShowCounselingOfficesOK Successfully retrieved counseling offices + +swagger:response showCounselingOfficesOK +*/ +type ShowCounselingOfficesOK struct { + + /* + In: Body + */ + Payload ghcmessages.CounselingOffices `json:"body,omitempty"` +} + +// NewShowCounselingOfficesOK creates ShowCounselingOfficesOK with default headers values +func NewShowCounselingOfficesOK() *ShowCounselingOfficesOK { + + return &ShowCounselingOfficesOK{} +} + +// WithPayload adds the payload to the show counseling offices o k response +func (o *ShowCounselingOfficesOK) WithPayload(payload ghcmessages.CounselingOffices) *ShowCounselingOfficesOK { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the show counseling offices o k response +func (o *ShowCounselingOfficesOK) SetPayload(payload ghcmessages.CounselingOffices) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *ShowCounselingOfficesOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(200) + payload := o.Payload + if payload == nil { + // return empty array + payload = ghcmessages.CounselingOffices{} + } + + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } +} + +// ShowCounselingOfficesInternalServerErrorCode is the HTTP code returned for type ShowCounselingOfficesInternalServerError +const ShowCounselingOfficesInternalServerErrorCode int = 500 + +/* +ShowCounselingOfficesInternalServerError internal server error + +swagger:response showCounselingOfficesInternalServerError +*/ +type ShowCounselingOfficesInternalServerError struct { +} + +// NewShowCounselingOfficesInternalServerError creates ShowCounselingOfficesInternalServerError with default headers values +func NewShowCounselingOfficesInternalServerError() *ShowCounselingOfficesInternalServerError { + + return &ShowCounselingOfficesInternalServerError{} +} + +// WriteResponse to the client +func (o *ShowCounselingOfficesInternalServerError) 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/ghcapi/ghcoperations/transportation_office/show_counseling_offices_urlbuilder.go b/pkg/gen/ghcapi/ghcoperations/transportation_office/show_counseling_offices_urlbuilder.go new file mode 100644 index 00000000000..1f0e360afa7 --- /dev/null +++ b/pkg/gen/ghcapi/ghcoperations/transportation_office/show_counseling_offices_urlbuilder.go @@ -0,0 +1,101 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package transportation_office + +// 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" + + "github.com/go-openapi/strfmt" +) + +// ShowCounselingOfficesURL generates an URL for the show counseling offices operation +type ShowCounselingOfficesURL struct { + DutyLocationID strfmt.UUID + + _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 *ShowCounselingOfficesURL) WithBasePath(bp string) *ShowCounselingOfficesURL { + 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 *ShowCounselingOfficesURL) SetBasePath(bp string) { + o._basePath = bp +} + +// Build a url path and query string +func (o *ShowCounselingOfficesURL) Build() (*url.URL, error) { + var _result url.URL + + var _path = "/transportation_offices/{dutyLocationId}/counseling_offices" + + dutyLocationID := o.DutyLocationID.String() + if dutyLocationID != "" { + _path = strings.Replace(_path, "{dutyLocationId}", dutyLocationID, -1) + } else { + return nil, errors.New("dutyLocationId is required on ShowCounselingOfficesURL") + } + + _basePath := o._basePath + if _basePath == "" { + _basePath = "/ghc/v1" + } + _result.Path = golangswaggerpaths.Join(_basePath, _path) + + return &_result, nil +} + +// Must is a helper function to panic when the url builder returns an error +func (o *ShowCounselingOfficesURL) 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 *ShowCounselingOfficesURL) String() string { + return o.Must(o.Build()).String() +} + +// BuildFull builds a full url with scheme, host, path and query string +func (o *ShowCounselingOfficesURL) BuildFull(scheme, host string) (*url.URL, error) { + if scheme == "" { + return nil, errors.New("scheme is required for a full url on ShowCounselingOfficesURL") + } + if host == "" { + return nil, errors.New("host is required for a full url on ShowCounselingOfficesURL") + } + + 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 *ShowCounselingOfficesURL) StringFull(scheme, host string) string { + return o.Must(o.BuildFull(scheme, host)).String() +} diff --git a/pkg/gen/ghcmessages/counseling_office.go b/pkg/gen/ghcmessages/counseling_office.go new file mode 100644 index 00000000000..b3bf2fea949 --- /dev/null +++ b/pkg/gen/ghcmessages/counseling_office.go @@ -0,0 +1,95 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package ghcmessages + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/go-openapi/validate" +) + +// CounselingOffice counseling office +// +// swagger:model CounselingOffice +type CounselingOffice struct { + + // id + // Example: c56a4180-65aa-42ec-a945-5fd21dec0538 + // Required: true + // Format: uuid + ID *strfmt.UUID `json:"id"` + + // name + // Example: Fort Bragg North Station + // Required: true + Name *string `json:"name"` +} + +// Validate validates this counseling office +func (m *CounselingOffice) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateID(formats); err != nil { + res = append(res, err) + } + + if err := m.validateName(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *CounselingOffice) validateID(formats strfmt.Registry) error { + + if err := validate.Required("id", "body", m.ID); err != nil { + return err + } + + if err := validate.FormatOf("id", "body", "uuid", m.ID.String(), formats); err != nil { + return err + } + + return nil +} + +func (m *CounselingOffice) validateName(formats strfmt.Registry) error { + + if err := validate.Required("name", "body", m.Name); err != nil { + return err + } + + return nil +} + +// ContextValidate validates this counseling office based on context it is used +func (m *CounselingOffice) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} + +// MarshalBinary interface implementation +func (m *CounselingOffice) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *CounselingOffice) UnmarshalBinary(b []byte) error { + var res CounselingOffice + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/pkg/gen/ghcmessages/counseling_offices.go b/pkg/gen/ghcmessages/counseling_offices.go new file mode 100644 index 00000000000..28a6d79e3b3 --- /dev/null +++ b/pkg/gen/ghcmessages/counseling_offices.go @@ -0,0 +1,78 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package ghcmessages + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "strconv" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" +) + +// CounselingOffices counseling offices +// +// swagger:model CounselingOffices +type CounselingOffices []*CounselingOffice + +// Validate validates this counseling offices +func (m CounselingOffices) Validate(formats strfmt.Registry) error { + var res []error + + for i := 0; i < len(m); i++ { + if swag.IsZero(m[i]) { // not required + continue + } + + if m[i] != nil { + if err := m[i].Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName(strconv.Itoa(i)) + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName(strconv.Itoa(i)) + } + return err + } + } + + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +// ContextValidate validate this counseling offices based on the context it is used +func (m CounselingOffices) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + for i := 0; i < len(m); i++ { + + if m[i] != nil { + + if swag.IsZero(m[i]) { // not required + return nil + } + + if err := m[i].ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName(strconv.Itoa(i)) + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName(strconv.Itoa(i)) + } + return err + } + } + + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} diff --git a/pkg/gen/ghcmessages/create_orders.go b/pkg/gen/ghcmessages/create_orders.go index 25f77f42c15..a71a85a4d74 100644 --- a/pkg/gen/ghcmessages/create_orders.go +++ b/pkg/gen/ghcmessages/create_orders.go @@ -23,6 +23,11 @@ type CreateOrders struct { // Example: true AccompaniedTour *bool `json:"accompaniedTour,omitempty"` + // counseling office Id + // Example: cf1addea-a4f9-4173-8506-2bb82a064cb7 + // Format: uuid + CounselingOfficeID *strfmt.UUID `json:"counselingOfficeId,omitempty"` + // department indicator DepartmentIndicator *DeptIndicator `json:"departmentIndicator,omitempty"` @@ -100,6 +105,10 @@ type CreateOrders struct { func (m *CreateOrders) Validate(formats strfmt.Registry) error { var res []error + if err := m.validateCounselingOfficeID(formats); err != nil { + res = append(res, err) + } + if err := m.validateDepartmentIndicator(formats); err != nil { res = append(res, err) } @@ -150,6 +159,18 @@ func (m *CreateOrders) Validate(formats strfmt.Registry) error { return nil } +func (m *CreateOrders) validateCounselingOfficeID(formats strfmt.Registry) error { + if swag.IsZero(m.CounselingOfficeID) { // not required + return nil + } + + if err := validate.FormatOf("counselingOfficeId", "body", "uuid", m.CounselingOfficeID.String(), formats); err != nil { + return err + } + + return nil +} + func (m *CreateOrders) validateDepartmentIndicator(formats strfmt.Registry) error { if swag.IsZero(m.DepartmentIndicator) { // not required return nil diff --git a/pkg/handlers/ghcapi/api.go b/pkg/handlers/ghcapi/api.go index 9e8c90b1ee0..3c4f3740941 100644 --- a/pkg/handlers/ghcapi/api.go +++ b/pkg/handlers/ghcapi/api.go @@ -675,6 +675,11 @@ func NewGhcAPIHandler(handlerConfig handlers.HandlerConfig) *ghcops.MymoveAPI { transportationOfficeFetcher, } + ghcAPI.TransportationOfficeShowCounselingOfficesHandler = ShowCounselingOfficesHandler{ + handlerConfig, + transportationOfficeFetcher, + } + ghcAPI.MoveUpdateCloseoutOfficeHandler = UpdateMoveCloseoutOfficeHandler{ handlerConfig, closeoutOfficeUpdater, diff --git a/pkg/handlers/ghcapi/internal/payloads/model_to_payload.go b/pkg/handlers/ghcapi/internal/payloads/model_to_payload.go index 5350b31d218..798858586a4 100644 --- a/pkg/handlers/ghcapi/internal/payloads/model_to_payload.go +++ b/pkg/handlers/ghcapi/internal/payloads/model_to_payload.go @@ -437,6 +437,18 @@ func GBLOCs(gblocs []string) ghcmessages.GBLOCs { return payload } +func CounselingOffices(counselingOffices models.TransportationOffices) ghcmessages.CounselingOffices { + payload := make(ghcmessages.CounselingOffices, len(counselingOffices)) + + for i, counselingOffice := range counselingOffices { + payload[i] = &ghcmessages.CounselingOffice{ + ID: handlers.FmtUUID(counselingOffice.ID), + Name: models.StringPointer(counselingOffice.Name), + } + } + return payload +} + // MoveHistory payload func MoveHistory(logger *zap.Logger, moveHistory *models.MoveHistory) *ghcmessages.MoveHistory { payload := &ghcmessages.MoveHistory{ diff --git a/pkg/handlers/ghcapi/internal/payloads/model_to_payload_test.go b/pkg/handlers/ghcapi/internal/payloads/model_to_payload_test.go index fe164e85723..d4fcc0c9910 100644 --- a/pkg/handlers/ghcapi/internal/payloads/model_to_payload_test.go +++ b/pkg/handlers/ghcapi/internal/payloads/model_to_payload_test.go @@ -1353,3 +1353,34 @@ func (suite *PayloadsSuite) TestMTOShipment_POE_POD_Locations() { suite.Nil(payload.PoeLocation, "Expected PODLocation to be nil when PODLocation is set") }) } + +func (suite *PayloadsSuite) TestCounselingOffices() { + suite.Run("correctly maps transportaion offices to counseling offices payload", func() { + office1 := factory.BuildTransportationOffice(nil, []factory.Customization{ + { + Model: models.TransportationOffice{ + ID: uuid.Must(uuid.NewV4()), + Name: "PPPO Fort Liberty", + }, + }, + }, nil) + + office2 := factory.BuildTransportationOffice(nil, []factory.Customization{ + { + Model: models.TransportationOffice{ + ID: uuid.Must(uuid.NewV4()), + Name: "PPPO Fort Walker", + }, + }, + }, nil) + + offices := models.TransportationOffices{office1, office2} + + payload := CounselingOffices(offices) + + suite.IsType(payload, ghcmessages.CounselingOffices{}) + suite.Equal(2, len(payload)) + suite.Equal(office1.ID.String(), payload[0].ID.String()) + suite.Equal(office2.ID.String(), payload[1].ID.String()) + }) +} diff --git a/pkg/handlers/ghcapi/orders.go b/pkg/handlers/ghcapi/orders.go index 09b1464c016..21706535e6a 100644 --- a/pkg/handlers/ghcapi/orders.go +++ b/pkg/handlers/ghcapi/orders.go @@ -331,15 +331,13 @@ func (h CreateOrderHandler) Handle(params orderop.CreateOrderParams) middleware. Show: models.BoolPointer(true), Status: &status, } - if !appCtx.Session().OfficeUserID.IsNil() { - officeUser, err := models.FetchOfficeUserByID(appCtx.DB(), appCtx.Session().OfficeUserID) + + if payload.CounselingOfficeID != nil { + counselingOffice, err := uuid.FromString(payload.CounselingOfficeID.String()) if err != nil { - err = apperror.NewBadDataError("Unable to fetch office user.") - appCtx.Logger().Error(err.Error()) - return orderop.NewCreateOrderUnprocessableEntity(), err - } else { - moveOptions.CounselingOfficeID = &officeUser.TransportationOfficeID + return handlers.ResponseForError(appCtx.Logger(), err), err } + moveOptions.CounselingOfficeID = &counselingOffice } if newOrder.OrdersType == "SAFETY" { diff --git a/pkg/handlers/ghcapi/orders_test.go b/pkg/handlers/ghcapi/orders_test.go index 686894bd630..24939aa09fb 100644 --- a/pkg/handlers/ghcapi/orders_test.go +++ b/pkg/handlers/ghcapi/orders_test.go @@ -73,6 +73,7 @@ func (suite *HandlerSuite) TestCreateOrder() { Sac: handlers.FmtString("SacNumber"), DepartmentIndicator: ghcmessages.NewDeptIndicator(deptIndicator), Grade: ghcmessages.GradeE1.Pointer(), + CounselingOfficeID: handlers.FmtUUID(*dutyLocation.TransportationOfficeID), } params := orderop.CreateOrderParams{ diff --git a/pkg/handlers/ghcapi/tranportation_offices.go b/pkg/handlers/ghcapi/tranportation_offices.go index 405580923bb..6c0dcacc1b6 100644 --- a/pkg/handlers/ghcapi/tranportation_offices.go +++ b/pkg/handlers/ghcapi/tranportation_offices.go @@ -2,6 +2,7 @@ package ghcapi import ( "github.com/go-openapi/runtime/middleware" + "github.com/gofrs/uuid" "go.uber.org/zap" "github.com/transcom/mymove/pkg/appcontext" @@ -73,3 +74,26 @@ func (h GetTransportationOfficesGBLOCsHandler) Handle(params transportationoffic return transportationofficeop.NewGetTransportationOfficesGBLOCsOK().WithPayload(returnPayload), nil }) } + +// ShowCounselingOfficesHandler returns the counseling offices for a duty location ID +type ShowCounselingOfficesHandler struct { + handlers.HandlerConfig + services.TransportationOfficesFetcher +} + +// Handle retrieves the counseling offices in the system for a given duty location ID +func (h ShowCounselingOfficesHandler) Handle(params transportationofficeop.ShowCounselingOfficesParams) middleware.Responder { + return h.AuditableAppContextFromRequestWithErrors(params.HTTPRequest, + func(appCtx appcontext.AppContext) (middleware.Responder, error) { + dutyLocationID := uuid.FromStringOrNil(params.DutyLocationID.String()) + + counselingOffices, err := h.TransportationOfficesFetcher.GetCounselingOffices(appCtx, dutyLocationID) + if err != nil { + appCtx.Logger().Error("Error searching for Counseling Offices: ", zap.Error(err)) + return transportationofficeop.NewShowCounselingOfficesInternalServerError(), err + } + + returnPayload := payloads.CounselingOffices(*counselingOffices) + return transportationofficeop.NewShowCounselingOfficesOK().WithPayload(returnPayload), nil + }) +} diff --git a/pkg/handlers/ghcapi/transportation_offices_test.go b/pkg/handlers/ghcapi/transportation_offices_test.go index 92ac98f630b..087c5c2d243 100644 --- a/pkg/handlers/ghcapi/transportation_offices_test.go +++ b/pkg/handlers/ghcapi/transportation_offices_test.go @@ -1,13 +1,16 @@ package ghcapi import ( + "fmt" "net/http/httptest" "github.com/go-openapi/strfmt" "github.com/transcom/mymove/pkg/factory" transportationofficeop "github.com/transcom/mymove/pkg/gen/ghcapi/ghcoperations/transportation_office" + "github.com/transcom/mymove/pkg/handlers" "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/services/address" transportationofficeservice "github.com/transcom/mymove/pkg/services/transportation_office" ) @@ -145,3 +148,63 @@ func (suite *HandlerSuite) TestGetTransportationOfficesGBLOCsHandler() { suite.Equal(transportationOffice1.Gbloc, responsePayload.Payload[0]) suite.Equal(transportationOffice2.Gbloc, responsePayload.Payload[1]) } + +func (suite *HandlerSuite) TestShowCounselingOfficesHandler() { + user := factory.BuildDefaultUser(suite.DB()) + + fetcher := transportationofficeservice.NewTransportationOfficesFetcher() + + newAddress := models.Address{ + StreetAddress1: "some address", + City: "city", + State: "CA", + PostalCode: "59801", + County: models.StringPointer("County"), + } + addressCreator := address.NewAddressCreator() + createdAddress, err := addressCreator.CreateAddress(suite.AppContextForTest(), &newAddress) + suite.NoError(err) + + origDutyLocation := factory.BuildDutyLocation(suite.DB(), []factory.Customization{ + { + Model: models.DutyLocation{ + AddressID: createdAddress.ID, + ProvidesServicesCounseling: true, + }, + }, + { + Model: models.TransportationOffice{ + Name: "New PPPO Travis AFB - USAF", + Gbloc: "KKFA", + ProvidesCloseout: true, + }, + }, + }, nil) + suite.MustSave(&origDutyLocation) + + path := fmt.Sprintf("/transportation_offices/%v/counseling_offices", origDutyLocation.ID.String()) + req := httptest.NewRequest("GET", path, nil) + req = suite.AuthenticateUserRequest(req, user) + params := transportationofficeop.ShowCounselingOfficesParams{ + HTTPRequest: req, + DutyLocationID: *handlers.FmtUUID(origDutyLocation.ID), + } + + handler := ShowCounselingOfficesHandler{ + HandlerConfig: suite.HandlerConfig(), + TransportationOfficesFetcher: fetcher} + + response := handler.Handle(params) + suite.Assertions.IsType(&transportationofficeop.ShowCounselingOfficesOK{}, response) + responsePayload := response.(*transportationofficeop.ShowCounselingOfficesOK) + + // Validate outgoing payload + suite.NoError(responsePayload.Payload.Validate(strfmt.Default)) + var i int + for index, office := range responsePayload.Payload { + if *office.Name == "New PPPO Travis AFB - USAF" { + i = index + } + } + suite.NotNil(i) +} 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 69d07e171ed..e7c8c526561 100644 --- a/pkg/handlers/internalapi/internal/payloads/model_to_payload_test.go +++ b/pkg/handlers/internalapi/internal/payloads/model_to_payload_test.go @@ -3,6 +3,7 @@ package payloads import ( "github.com/gofrs/uuid" + "github.com/transcom/mymove/pkg/factory" "github.com/transcom/mymove/pkg/gen/internalmessages" "github.com/transcom/mymove/pkg/handlers" "github.com/transcom/mymove/pkg/models" @@ -116,3 +117,34 @@ func (suite *PayloadsSuite) TestVLocation() { suite.Equal(county, *(payload.County), "Expected County to match") }) } + +func (suite *PayloadsSuite) TestCounselingOffices() { + suite.Run("correctly maps transportaion offices to counseling offices payload", func() { + office1 := factory.BuildTransportationOffice(nil, []factory.Customization{ + { + Model: models.TransportationOffice{ + ID: uuid.Must(uuid.NewV4()), + Name: "PPPO Fort Liberty", + }, + }, + }, nil) + + office2 := factory.BuildTransportationOffice(nil, []factory.Customization{ + { + Model: models.TransportationOffice{ + ID: uuid.Must(uuid.NewV4()), + Name: "PPPO Fort Walker", + }, + }, + }, nil) + + offices := models.TransportationOffices{office1, office2} + + payload := CounselingOffices(offices) + + suite.IsType(payload, internalmessages.CounselingOffices{}) + suite.Equal(2, len(payload)) + suite.Equal(office1.ID.String(), payload[0].ID.String()) + suite.Equal(office2.ID.String(), payload[1].ID.String()) + }) +} diff --git a/pkg/services/mto_shipment/mto_shipment_updater.go b/pkg/services/mto_shipment/mto_shipment_updater.go index d30e6842292..4da7227e596 100644 --- a/pkg/services/mto_shipment/mto_shipment_updater.go +++ b/pkg/services/mto_shipment/mto_shipment_updater.go @@ -855,8 +855,8 @@ func (f *mtoShipmentUpdater) updateShipmentRecord(appCtx appcontext.AppContext, // we will compare data here to see if we even need to update the pricing if newShipment.MarketCode == models.MarketCodeInternational && (newShipment.PrimeEstimatedWeight != nil || - newShipment.PickupAddress != nil && newShipment.PickupAddress.PostalCode != dbShipment.PickupAddress.PostalCode || - newShipment.DestinationAddress != nil && newShipment.DestinationAddress.PostalCode != dbShipment.DestinationAddress.PostalCode || + newShipment.PickupAddress != nil && dbShipment.PickupAddress != nil && newShipment.PickupAddress.PostalCode != dbShipment.PickupAddress.PostalCode || + newShipment.DestinationAddress != nil && dbShipment.DestinationAddress != nil && newShipment.DestinationAddress.PostalCode != dbShipment.DestinationAddress.PostalCode || newShipment.RequestedPickupDate != nil && newShipment.RequestedPickupDate.Format("2006-01-02") != dbShipment.RequestedPickupDate.Format("2006-01-02")) { portZip, portType, err := models.GetPortLocationInfoForShipment(appCtx.DB(), newShipment.ID) @@ -1110,9 +1110,9 @@ func reServiceCodesForShipment(shipment models.MTOShipment) []models.ReServiceCo // More info in MB-1140: https://dp3.atlassian.net/browse/MB-1140 // international shipment service items are created in the shipment_approver - switch shipment.ShipmentType { - case models.MTOShipmentTypeHHG: - if shipment.MarketCode != models.MarketCodeInternational { + if shipment.MarketCode != models.MarketCodeInternational { + switch shipment.ShipmentType { + case models.MTOShipmentTypeHHG: originZIP3 := shipment.PickupAddress.PostalCode[0:3] destinationZIP3 := shipment.DestinationAddress.PostalCode[0:3] @@ -1136,51 +1136,51 @@ func reServiceCodesForShipment(shipment models.MTOShipment) []models.ReServiceCo models.ReServiceCodeDPK, models.ReServiceCodeDUPK, } - } - case models.MTOShipmentTypeHHGIntoNTS: - // Need to create: Dom Linehaul, Fuel Surcharge, Dom Origin Price, Dom Destination Price, Dom NTS Packing - return []models.ReServiceCode{ - models.ReServiceCodeDLH, - models.ReServiceCodeFSC, - models.ReServiceCodeDOP, - models.ReServiceCodeDDP, - models.ReServiceCodeDNPK, - } - case models.MTOShipmentTypeHHGOutOfNTSDom: - // Need to create: Dom Linehaul, Fuel Surcharge, Dom Origin Price, Dom Destination Price, Dom Unpacking - return []models.ReServiceCode{ - models.ReServiceCodeDLH, - models.ReServiceCodeFSC, - models.ReServiceCodeDOP, - models.ReServiceCodeDDP, - models.ReServiceCodeDUPK, - } - case models.MTOShipmentTypeMobileHome: - // Need to create: Dom Linehaul, Fuel Surcharge, Dom Origin Price, Dom Destination Price, Dom Mobile Home Factor - return []models.ReServiceCode{ - models.ReServiceCodeDLH, - models.ReServiceCodeFSC, - models.ReServiceCodeDOP, - models.ReServiceCodeDDP, - models.ReServiceCodeDMHF, - } - case models.MTOShipmentTypeBoatHaulAway: - // Need to create: Dom Linehaul, Fuel Surcharge, Dom Origin Price, Dom Destination Price, Dom Haul Away Boat Factor - return []models.ReServiceCode{ - models.ReServiceCodeDLH, - models.ReServiceCodeFSC, - models.ReServiceCodeDOP, - models.ReServiceCodeDDP, - models.ReServiceCodeDBHF, - } - case models.MTOShipmentTypeBoatTowAway: - // Need to create: Dom Linehaul, Fuel Surcharge, Dom Origin Price, Dom Destination Price, Dom Tow Away Boat Factor - return []models.ReServiceCode{ - models.ReServiceCodeDLH, - models.ReServiceCodeFSC, - models.ReServiceCodeDOP, - models.ReServiceCodeDDP, - models.ReServiceCodeDBTF, + case models.MTOShipmentTypeHHGIntoNTS: + // Need to create: Dom Linehaul, Fuel Surcharge, Dom Origin Price, Dom Destination Price, Dom NTS Packing + return []models.ReServiceCode{ + models.ReServiceCodeDLH, + models.ReServiceCodeFSC, + models.ReServiceCodeDOP, + models.ReServiceCodeDDP, + models.ReServiceCodeDNPK, + } + case models.MTOShipmentTypeHHGOutOfNTSDom: + // Need to create: Dom Linehaul, Fuel Surcharge, Dom Origin Price, Dom Destination Price, Dom Unpacking + return []models.ReServiceCode{ + models.ReServiceCodeDLH, + models.ReServiceCodeFSC, + models.ReServiceCodeDOP, + models.ReServiceCodeDDP, + models.ReServiceCodeDUPK, + } + case models.MTOShipmentTypeMobileHome: + // Need to create: Dom Linehaul, Fuel Surcharge, Dom Origin Price, Dom Destination Price, Dom Mobile Home Factor + return []models.ReServiceCode{ + models.ReServiceCodeDLH, + models.ReServiceCodeFSC, + models.ReServiceCodeDOP, + models.ReServiceCodeDDP, + models.ReServiceCodeDMHF, + } + case models.MTOShipmentTypeBoatHaulAway: + // Need to create: Dom Linehaul, Fuel Surcharge, Dom Origin Price, Dom Destination Price, Dom Haul Away Boat Factor + return []models.ReServiceCode{ + models.ReServiceCodeDLH, + models.ReServiceCodeFSC, + models.ReServiceCodeDOP, + models.ReServiceCodeDDP, + models.ReServiceCodeDBHF, + } + case models.MTOShipmentTypeBoatTowAway: + // Need to create: Dom Linehaul, Fuel Surcharge, Dom Origin Price, Dom Destination Price, Dom Tow Away Boat Factor + return []models.ReServiceCode{ + models.ReServiceCodeDLH, + models.ReServiceCodeFSC, + models.ReServiceCodeDOP, + models.ReServiceCodeDDP, + models.ReServiceCodeDBTF, + } } } diff --git a/pkg/services/mto_shipment/mto_shipment_updater_test.go b/pkg/services/mto_shipment/mto_shipment_updater_test.go index ee3fde58147..bfced125206 100644 --- a/pkg/services/mto_shipment/mto_shipment_updater_test.go +++ b/pkg/services/mto_shipment/mto_shipment_updater_test.go @@ -3445,3 +3445,104 @@ func (suite *MTOShipmentServiceSuite) TestUpdateStatusServiceItems() { suite.Equal(models.ReServiceCodeDSH, serviceItems[0].ReService.Code) }) } + +func (suite *MTOShipmentServiceSuite) TestUpdateDomesticServiceItems() { + + expectedReServiceCodes := []models.ReServiceCode{ + models.ReServiceCodeDLH, + models.ReServiceCodeFSC, + models.ReServiceCodeDOP, + models.ReServiceCodeDDP, + models.ReServiceCodeDNPK, + } + + var pickupAddress models.Address + var storageFacility models.StorageFacility + var mto models.Move + + setupTestData := func() { + pickupAddress = factory.BuildAddress(suite.DB(), []factory.Customization{ + { + Model: models.Address{ + StreetAddress1: "Test Street 1", + City: "Des moines", + State: "IA", + PostalCode: "50309", + IsOconus: models.BoolPointer(false), + }, + }, + }, nil) + + storageFacility = factory.BuildStorageFacility(suite.DB(), []factory.Customization{ + { + Model: models.Address{ + StreetAddress1: "Test Street Adress 2", + City: "Des moines", + State: "IA", + PostalCode: "50314", + IsOconus: models.BoolPointer(false), + }, + }, + }, nil) + + mto = factory.BuildMove(suite.DB(), []factory.Customization{ + { + Model: models.Move{ + Status: models.MoveStatusAPPROVED, + }, + }, + }, nil) + } + + builder := query.NewQueryBuilder() + moveRouter := moveservices.NewMoveRouter() + planner := &mocks.Planner{} + planner.On("ZipTransitDistance", + mock.AnythingOfType("*appcontext.appContext"), + mock.Anything, + mock.Anything, + ).Return(400, nil) + siCreator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) + updater := NewMTOShipmentStatusUpdater(builder, siCreator, planner) + + suite.Run("Preapproved service items successfully added to domestic nts shipments", func() { + setupTestData() + + shipment := factory.BuildMTOShipment(suite.DB(), []factory.Customization{ + { + Model: mto, + LinkOnly: true, + }, + { + Model: pickupAddress, + Type: &factory.Addresses.PickupAddress, + LinkOnly: true, + }, + { + Model: storageFacility, + Type: &factory.StorageFacility, + LinkOnly: true, + }, + { + Model: models.MTOShipment{ + ShipmentType: models.MTOShipmentTypeHHGIntoNTS, + Status: models.MTOShipmentStatusSubmitted, + }, + }, + }, nil) + + appCtx := suite.AppContextForTest() + eTag := etag.GenerateEtag(shipment.UpdatedAt) + + updatedShipment, err := updater.UpdateMTOShipmentStatus(appCtx, shipment.ID, models.MTOShipmentStatusApproved, nil, nil, eTag) + suite.NoError(err) + + serviceItems := models.MTOServiceItems{} + err = appCtx.DB().EagerPreload("ReService").Where("mto_shipment_id = ?", updatedShipment.ID).All(&serviceItems) + suite.NoError(err) + + for i := 0; i < len(expectedReServiceCodes); i++ { + suite.Equal(expectedReServiceCodes[i], serviceItems[i].ReService.Code) + } + }) +} diff --git a/pkg/services/mto_shipment/shipment_approver.go b/pkg/services/mto_shipment/shipment_approver.go index bb7c5245617..93aed2d6540 100644 --- a/pkg/services/mto_shipment/shipment_approver.go +++ b/pkg/services/mto_shipment/shipment_approver.go @@ -2,6 +2,7 @@ package mtoshipment import ( "math" + "slices" "github.com/gofrs/uuid" "github.com/pkg/errors" @@ -80,7 +81,8 @@ func (f *shipmentApprover) ApproveShipment(appCtx appcontext.AppContext, shipmen transactionError := appCtx.NewTransaction(func(txnAppCtx appcontext.AppContext) error { // create international shipment service items before approving // we use a database proc to create the basic auto-approved service items - if (shipment.ShipmentType == models.MTOShipmentTypeHHG || shipment.ShipmentType == models.MTOShipmentTypeUnaccompaniedBaggage) && shipment.MarketCode == models.MarketCodeInternational { + internationalShipmentTypes := []models.MTOShipmentType{models.MTOShipmentTypeHHG, models.MTOShipmentTypeHHGIntoNTS, models.MTOShipmentTypeUnaccompaniedBaggage} + if slices.Contains(internationalShipmentTypes, shipment.ShipmentType) && shipment.MarketCode == models.MarketCodeInternational { err := models.CreateApprovedServiceItemsForShipment(appCtx.DB(), shipment) if err != nil { return err diff --git a/pkg/services/mto_shipment/shipment_approver_test.go b/pkg/services/mto_shipment/shipment_approver_test.go index 67dd08fec47..e0a80dfdf87 100644 --- a/pkg/services/mto_shipment/shipment_approver_test.go +++ b/pkg/services/mto_shipment/shipment_approver_test.go @@ -14,6 +14,7 @@ import ( "github.com/transcom/mymove/pkg/etag" "github.com/transcom/mymove/pkg/factory" "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/route" "github.com/transcom/mymove/pkg/route/mocks" "github.com/transcom/mymove/pkg/services" "github.com/transcom/mymove/pkg/services/ghcrateengine" @@ -299,7 +300,7 @@ func (suite *MTOShipmentServiceSuite) TestApproveShipment() { models.ReServiceCodeIHUPK, } - suite.Equal(4, len(serviceItems)) + suite.Equal(len(expectedReserviceCodes), len(serviceItems)) for i := 0; i < len(serviceItems); i++ { actualReServiceCode := serviceItems[i].ReService.Code suite.True(slices.Contains(expectedReserviceCodes, actualReServiceCode)) @@ -312,6 +313,163 @@ func (suite *MTOShipmentServiceSuite) TestApproveShipment() { } }) + suite.Run("Given international mtoShipment is approved successfully pre-approved mtoServiceItems are created NTS CONUS to OCONUS", func() { + storageFacility := factory.BuildStorageFacility(suite.DB(), []factory.Customization{ + { + Model: models.StorageFacility{ + FacilityName: *models.StringPointer("Test Storage Name"), + Email: models.StringPointer("old@email.com"), + LotNumber: models.StringPointer("Test lot number"), + Phone: models.StringPointer("555-555-5555"), + }, + }, + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "Anchorage", + State: "AK", + PostalCode: "99507", + IsOconus: models.BoolPointer(true), + }, + }, + }, nil) + + internationalShipment := factory.BuildMTOShipment(suite.DB(), []factory.Customization{ + { + Model: models.Move{ + Status: models.MoveStatusAPPROVED, + }, + }, + { + Model: models.Address{ + StreetAddress1: "Tester Address", + City: "Des Moines", + State: "IA", + PostalCode: "50314", + IsOconus: models.BoolPointer(false), + }, + Type: &factory.Addresses.PickupAddress, + }, + { + Model: models.MTOShipment{ + MarketCode: models.MarketCodeInternational, + Status: models.MTOShipmentStatusSubmitted, + ShipmentType: models.MTOShipmentTypeHHGIntoNTS, + }, + }, + { + Model: storageFacility, + LinkOnly: true, + }, + }, nil) + internationalShipmentEtag := etag.GenerateEtag(internationalShipment.UpdatedAt) + + shipmentRouter := NewShipmentRouter() + var serviceItemCreator services.MTOServiceItemCreator + var planner route.Planner + var moveWeights services.MoveWeights + + // Approve international shipment + shipmentApprover := NewShipmentApprover(shipmentRouter, serviceItemCreator, planner, moveWeights) + _, err := shipmentApprover.ApproveShipment(suite.AppContextForTest(), internationalShipment.ID, internationalShipmentEtag) + suite.NoError(err) + + // Get created pre approved service items + var serviceItems []models.MTOServiceItem + err2 := suite.AppContextForTest().DB().EagerPreload("ReService").Where("mto_shipment_id = ?", internationalShipment.ID).Order("created_at asc").All(&serviceItems) + suite.NoError(err2) + + expectedReserviceCodes := []models.ReServiceCode{ + models.ReServiceCodeISLH, + models.ReServiceCodeINPK, + } + + suite.Equal(len(expectedReserviceCodes), len(serviceItems)) + for i := 0; i < len(serviceItems); i++ { + actualReServiceCode := serviceItems[i].ReService.Code + suite.True(slices.Contains(expectedReserviceCodes, actualReServiceCode)) + } + }) + + suite.Run("Given international mtoShipment is approved successfully pre-approved mtoServiceItems are created NTS OCONUS to CONUS", func() { + storageFacility := factory.BuildStorageFacility(suite.DB(), []factory.Customization{ + { + Model: models.StorageFacility{ + FacilityName: *models.StringPointer("Test Storage Name"), + Email: models.StringPointer("old@email.com"), + LotNumber: models.StringPointer("Test lot number"), + Phone: models.StringPointer("555-555-5555"), + }, + }, + { + Model: models.Address{ + StreetAddress1: "Tester Address", + City: "Des Moines", + State: "IA", + PostalCode: "50314", + IsOconus: models.BoolPointer(false), + }, + }, + }, nil) + + internationalShipment := factory.BuildNTSShipment(suite.DB(), []factory.Customization{ + { + Model: models.Move{ + Status: models.MoveStatusAPPROVED, + }, + }, + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "Anchorage", + State: "AK", + PostalCode: "99507", + IsOconus: models.BoolPointer(true), + }, + Type: &factory.Addresses.PickupAddress, + }, + { + Model: models.MTOShipment{ + MarketCode: models.MarketCodeInternational, + Status: models.MTOShipmentStatusSubmitted, + ShipmentType: models.MTOShipmentTypeHHGIntoNTS, + }, + }, + { + Model: storageFacility, + LinkOnly: true, + }, + }, nil) + internationalShipmentEtag := etag.GenerateEtag(internationalShipment.UpdatedAt) + + shipmentRouter := NewShipmentRouter() + var serviceItemCreator services.MTOServiceItemCreator + var planner route.Planner + var moveWeights services.MoveWeights + + // Approve international shipment + shipmentApprover := NewShipmentApprover(shipmentRouter, serviceItemCreator, planner, moveWeights) + _, err := shipmentApprover.ApproveShipment(suite.AppContextForTest(), internationalShipment.ID, internationalShipmentEtag) + suite.NoError(err) + + // Get created pre approved service items + var serviceItems []models.MTOServiceItem + err2 := suite.AppContextForTest().DB().EagerPreload("ReService").Where("mto_shipment_id = ?", internationalShipment.ID).Order("created_at asc").All(&serviceItems) + suite.NoError(err2) + + expectedReserviceCodes := []models.ReServiceCode{ + models.ReServiceCodeISLH, + models.ReServiceCodePODFSC, + models.ReServiceCodeINPK, + } + + suite.Equal(len(expectedReserviceCodes), len(serviceItems)) + for i := 0; i < len(serviceItems); i++ { + actualReServiceCode := serviceItems[i].ReService.Code + suite.True(slices.Contains(expectedReserviceCodes, actualReServiceCode)) + } + }) + suite.Run("If the mtoShipment is approved successfully it should create approved mtoServiceItems", func() { subtestData := suite.createApproveShipmentSubtestData() appCtx := subtestData.appCtx @@ -1034,4 +1192,24 @@ func (suite *MTOShipmentServiceSuite) TestApproveShipment() { } }) + suite.Run("Given invalid shipment error returned", func() { + invalidShipment := factory.BuildMTOShipment(suite.AppContextForTest().DB(), []factory.Customization{ + { + Model: models.MTOShipment{ + ShipmentType: models.MTOShipmentTypePPM, + }, + }, + }, nil) + invalidShipmentEtag := etag.GenerateEtag(invalidShipment.UpdatedAt) + + shipmentRouter := NewShipmentRouter() + var serviceItemCreator services.MTOServiceItemCreator + var planner route.Planner + var moveWeights services.MoveWeights + + // Approve international shipment + shipmentApprover := NewShipmentApprover(shipmentRouter, serviceItemCreator, planner, moveWeights) + _, err := shipmentApprover.ApproveShipment(suite.AppContextForTest(), invalidShipment.ID, invalidShipmentEtag) + suite.Error(err) + }) } diff --git a/src/components/Office/AddOrdersForm/AddOrdersForm.jsx b/src/components/Office/AddOrdersForm/AddOrdersForm.jsx index f934938ca22..1ce24a86f18 100644 --- a/src/components/Office/AddOrdersForm/AddOrdersForm.jsx +++ b/src/components/Office/AddOrdersForm/AddOrdersForm.jsx @@ -20,6 +20,7 @@ import Callout from 'components/Callout'; import ConnectedFlashMessage from 'containers/FlashMessage/FlashMessage'; import MaskedTextField from 'components/form/fields/MaskedTextField/MaskedTextField'; import formStyles from 'styles/form.module.scss'; +import { showCounselingOffices } from 'services/ghcApi'; let originMeta; let newDutyMeta = ''; @@ -32,6 +33,7 @@ const AddOrdersForm = ({ isBluebarkMoveSelected, }) => { const payGradeOptions = dropdownInputOptions(ORDERS_PAY_GRADE_OPTIONS); + const [counselingOfficeOptions, setCounselingOfficeOptions] = useState(null); const [currentDutyLocation, setCurrentDutyLocation] = useState(''); const [newDutyLocation, setNewDutyLocation] = useState(''); const [showAccompaniedTourField, setShowAccompaniedTourField] = useState(false); @@ -79,6 +81,17 @@ const AddOrdersForm = ({ }, []); useEffect(() => { + if (currentDutyLocation?.id) { + showCounselingOffices(currentDutyLocation.id).then((fetchedData) => { + if (fetchedData.body) { + const counselingOffices = fetchedData.body.map((item) => ({ + key: item.id, + value: item.name, + })); + setCounselingOfficeOptions(counselingOffices); + } + }); + } // Check if either currentDutyLocation or newDutyLocation is OCONUS if (currentDutyLocation?.address?.isOconus || newDutyLocation?.address?.isOconus) { setIsOconusMove(true); @@ -115,11 +128,19 @@ const AddOrdersForm = ({ return ( - {({ values, isValid, isSubmitting, handleSubmit, handleChange, touched, setFieldValue }) => { + {({ values, isValid, isSubmitting, handleSubmit, handleChange, touched, setFieldValue, setValues }) => { const isRetirementOrSeparation = ['RETIREMENT', 'SEPARATION'].includes(values.ordersType); if (!values.origin_duty_location && touched.origin_duty_location) originMeta = 'Required'; else originMeta = null; + const handleCounselingOfficeChange = () => { + setValues({ + ...values, + counselingOfficeId: null, + }); + setCounselingOfficeOptions(null); + }; + if (!values.newDutyLocation && touched.newDutyLocation) newDutyMeta = 'Required'; else newDutyMeta = null; const handleHasDependentsChange = (e) => { @@ -182,10 +203,29 @@ const AddOrdersForm = ({ id="originDutyLocation" onDutyLocationChange={(e) => { setCurrentDutyLocation(e); + handleCounselingOfficeChange(); }} metaOverride={originMeta} required /> + {currentDutyLocation.provides_services_counseling && ( +
+ + +
+ )} {isRetirementOrSeparation ? ( <> diff --git a/src/components/Office/AddOrdersForm/AddOrdersForm.test.jsx b/src/components/Office/AddOrdersForm/AddOrdersForm.test.jsx index 3692f7432ee..c3cc2f27d8b 100644 --- a/src/components/Office/AddOrdersForm/AddOrdersForm.test.jsx +++ b/src/components/Office/AddOrdersForm/AddOrdersForm.test.jsx @@ -75,10 +75,44 @@ jest.mock('components/LocationSearchBox/api', () => ({ name: 'Luke AFB', updated_at: '2021-02-11T16:48:04.117Z', }, + { + address: { + city: '', + id: '25be4d12-fe93-47f1-bbec-1db386dfa67e', + postalCode: '', + state: '', + streetAddress1: '', + }, + address_id: '4334640b-c35e-4293-a2f1-36c7b629f904', + affiliation: 'AIR_FORCE', + created_at: '2021-02-11T16:48:04.117Z', + id: '22f0755f-6f35-478b-9a75-35a69211da1d', + name: 'Scott AFB', + updated_at: '2021-02-11T16:48:04.117Z', + provides_services_counseling: true, + }, ]), ), })); +jest.mock('services/ghcApi', () => ({ + ...jest.requireActual('services/ghcApi'), + showCounselingOffices: jest.fn().mockImplementation(() => + Promise.resolve({ + body: [ + { + id: '3e937c1f-5539-4919-954d-017989130584', + name: 'Albuquerque AFB', + }, + { + id: 'fa51dab0-4553-4732-b843-1f33407f77bc', + name: 'Glendale Luke AFB', + }, + ], + }), + ), +})); + jest.mock('utils/featureFlags', () => ({ ...jest.requireActual('utils/featureFlags'), isBooleanFlagEnabled: jest.fn().mockImplementation(() => Promise.resolve(false)), @@ -96,6 +130,7 @@ const initialValues = { accompaniedTour: '', dependentsUnderTwelve: '', dependentsTwelveAndOver: '', + counselingOfficeId: '', }; const testProps = { initialValues, @@ -206,6 +241,9 @@ describe('AddOrdersForm - OCONUS and Accompanied Tour Test', () => { await userEvent.type(screen.getByLabelText(/Current duty location/), 'AFB'); await userEvent.click(await screen.findByText(/Elmendorf/)); + const counselingOfficeLabel = await screen.queryByText(/Counseling office/); + expect(counselingOfficeLabel).toBeFalsy(); + await userEvent.type(screen.getByLabelText(/New duty location/), 'AFB'); await userEvent.click(await screen.findByText(/Luke/)); @@ -380,3 +418,38 @@ describe('AddOrdersForm - Student Travel, Early Return of Dependents Test', () = }); }); }); + +describe('AddOrdersForm - With Counseling Office', () => { + it('displays the counseling office dropdown', async () => { + isBooleanFlagEnabled.mockImplementation(() => Promise.resolve(true)); + render( + + + , + ); + + await userEvent.selectOptions(await screen.findByLabelText(/Orders type/), 'PERMANENT_CHANGE_OF_STATION'); + await userEvent.type(screen.getByLabelText(/Orders date/), '08 Nov 2020'); + await userEvent.type(screen.getByLabelText(/Report by date/), '26 Nov 2020'); + await userEvent.click(screen.getByLabelText('No')); + await userEvent.selectOptions(screen.getByLabelText(/Pay grade/), ['E_5']); + + // Test Current Duty Location Search Box interaction + await userEvent.type(screen.getByLabelText(/Current duty location/), 'AFB', { delay: 100 }); + const selectedOptionCurrent = await screen.findByText(/Scott/); + await userEvent.click(selectedOptionCurrent); + + // Test New Duty Location Search Box interaction + await userEvent.type(screen.getByLabelText(/New duty location/), 'AFB', { delay: 100 }); + const selectedOptionNew = await screen.findByText(/Luke/); + await userEvent.click(selectedOptionNew); + + const counselingOfficeLabel = await screen.queryByText(/Counseling office/); + expect(counselingOfficeLabel).toBeTruthy(); + + await userEvent.selectOptions(screen.getByLabelText(/Counseling office/), ['Albuquerque AFB']); + + const nextBtn = screen.getByRole('button', { name: 'Next' }); + expect(nextBtn.getAttribute('disabled')).toBeFalsy(); + }); +}); diff --git a/src/pages/Office/ServicesCounselingAddOrders/ServicesCounselingAddOrders.test.jsx b/src/pages/Office/ServicesCounselingAddOrders/ServicesCounselingAddOrders.test.jsx index eb57eefb665..a0accc4b2db 100644 --- a/src/pages/Office/ServicesCounselingAddOrders/ServicesCounselingAddOrders.test.jsx +++ b/src/pages/Office/ServicesCounselingAddOrders/ServicesCounselingAddOrders.test.jsx @@ -23,6 +23,20 @@ jest.mock('utils/featureFlags', () => ({ jest.mock('services/ghcApi', () => ({ ...jest.requireActual('services/ghcApi'), counselingCreateOrder: jest.fn().mockImplementation(() => Promise.resolve()), + showCounselingOffices: jest.fn().mockImplementation(() => + Promise.resolve({ + body: [ + { + id: '3e937c1f-5539-4919-954d-017989130584', + name: 'Albuquerque AFB', + }, + { + id: 'fa51dab0-4553-4732-b843-1f33407f77bc', + name: 'Glendale Luke AFB', + }, + ], + }), + ), })); jest.mock('services/internalApi', () => ({ @@ -79,6 +93,7 @@ jest.mock('components/LocationSearchBox/api', () => ({ id: '7d123884-7c1b-4611-92ae-e8d43ca03ad9', name: 'Hill AFB', updated_at: '2021-02-11T16:48:04.117Z', + provides_services_counseling: true, }, { address: { @@ -353,6 +368,9 @@ describe('ServicesCounselingAddOrders component', () => { const selectedOptionCurrent = await screen.findByText(/Altus/); await user.click(selectedOptionCurrent); + const counselingOfficeLabel = await screen.queryByText(/Counseling office/); + expect(counselingOfficeLabel).toBeFalsy(); + await user.type(screen.getByLabelText('New duty location'), 'AFB', { delay: 500 }); const selectedOptionNew = await screen.findByText(/Luke/); await user.click(selectedOptionNew); @@ -370,6 +388,39 @@ describe('ServicesCounselingAddOrders component', () => { }); }); + it('Displays the counseling office dropdown', async () => { + renderWithMocks(); + + counselingCreateOrder.mockImplementation(() => Promise.resolve(fakeResponse)); + + const user = userEvent.setup(); + + await user.selectOptions(screen.getByLabelText('Orders type'), 'PERMANENT_CHANGE_OF_STATION'); + await user.type(screen.getByLabelText('Orders date'), '08 Nov 2020'); + await user.type(screen.getByLabelText('Report by date'), '29 Nov 2020'); + await user.click(screen.getByLabelText('No')); + await user.selectOptions(screen.getByLabelText('Pay grade'), ['E-5']); + + // Test Current Duty Location Search Box interaction + await user.type(screen.getByLabelText('Current duty location'), 'AFB', { delay: 500 }); + const selectedOptionCurrent = await screen.findByText(/Hill/); + await user.click(selectedOptionCurrent); + + const counselingOfficeLabel = await screen.queryByText(/Counseling office/); + expect(counselingOfficeLabel).toBeTruthy(); + + await userEvent.selectOptions(screen.getByLabelText(/Counseling office/), ['Glendale Luke AFB']); + + await user.type(screen.getByLabelText('New duty location'), 'AFB', { delay: 500 }); + const selectedOptionNew = await screen.findByText(/Luke/); + await user.click(selectedOptionNew); + + const nextBtn = await screen.findByRole('button', { name: 'Next' }); + await waitFor(() => { + expect(nextBtn.getAttribute('disabled')).toBeFalsy(); + }); + }); + it('routes to the move details page when the next button is clicked for OCONUS orders', async () => { isBooleanFlagEnabled.mockImplementation(() => Promise.resolve(true)); renderWithMocks(); diff --git a/src/services/ghcApi.js b/src/services/ghcApi.js index ab6e6875fbc..56bca53d4b0 100644 --- a/src/services/ghcApi.js +++ b/src/services/ghcApi.js @@ -779,6 +779,10 @@ export async function getGBLOCs() { return makeGHCRequest(operationPath, {}, { normalize: false }); } +export async function showCounselingOffices(dutyLocationId) { + return makeGHCRequestRaw('transportationOffice.showCounselingOffices', { dutyLocationId }); +} + export const reviewShipmentAddressUpdate = async ({ shipmentID, ifMatchETag, body }) => { const operationPath = 'shipment.reviewShipmentAddressUpdate'; const schemaKey = 'ShipmentAddressUpdate'; diff --git a/swagger-def/ghc.yaml b/swagger-def/ghc.yaml index 432c2d07b9c..f4d84c62dc2 100644 --- a/swagger-def/ghc.yaml +++ b/swagger-def/ghc.yaml @@ -4218,8 +4218,8 @@ paths: tags: - transportationOffice responses: - "200": - description: Successfully retrieved transportation offices + '200': + description: Successfully retrieved GBLOCs schema: $ref: "#/definitions/GBLOCs" "400": @@ -4249,14 +4249,37 @@ paths: description: the requested list of city, state, county, and postal code matches schema: $ref: "#/definitions/VLocations" - "400": - $ref: "#/responses/InvalidRequest" - "403": - $ref: "#/responses/PermissionDenied" - "404": - $ref: "#/responses/NotFound" - "500": - $ref: "#/responses/ServerError" + '400': + $ref: '#/responses/InvalidRequest' + '403': + $ref: '#/responses/PermissionDenied' + '404': + $ref: '#/responses/NotFound' + '500': + $ref: '#/responses/ServerError' + /transportation_offices/{dutyLocationId}/counseling_offices: + get: + summary: Returns the counseling locations in the GBLOC matching the duty location + description: Returns the counseling locations matching the GBLOC from the selected duty location + operationId: showCounselingOffices + tags: + - transportationOffice + parameters: + - in: path + name: dutyLocationId + format: uuid + type: string + required: true + description: UUID of the duty location + produces: + - application/json + responses: + '200': + description: Successfully retrieved counseling offices + schema: + $ref: '#/definitions/CounselingOffices' + '500': + description: internal server error /uploads: post: summary: Create a new upload @@ -5790,6 +5813,11 @@ definitions: type: string format: uuid example: c56a4180-65aa-42ec-a945-5fd21dec0538 + counselingOfficeId: + type: string + format: uuid + example: cf1addea-a4f9-4173-8506-2bb82a064cb7 + x-nullable: true ordersNumber: type: string title: Orders Number @@ -7937,6 +7965,23 @@ definitions: type: array items: type: string + CounselingOffices: + type: array + items: + $ref: '#/definitions/CounselingOffice' + CounselingOffice: + type: object + properties: + id: + type: string + format: uuid + example: c56a4180-65aa-42ec-a945-5fd21dec0538 + name: + type: string + example: Fort Bragg North Station + required: + - id + - name MovePayload: type: object properties: diff --git a/swagger/ghc.yaml b/swagger/ghc.yaml index 8fb55f3300c..7075815407e 100644 --- a/swagger/ghc.yaml +++ b/swagger/ghc.yaml @@ -4409,7 +4409,7 @@ paths: - transportationOffice responses: '200': - description: Successfully retrieved transportation offices + description: Successfully retrieved GBLOCs schema: $ref: '#/definitions/GBLOCs' '400': @@ -4452,6 +4452,31 @@ paths: $ref: '#/responses/NotFound' '500': $ref: '#/responses/ServerError' + /transportation_offices/{dutyLocationId}/counseling_offices: + get: + summary: Returns the counseling locations in the GBLOC matching the duty location + description: >- + Returns the counseling locations matching the GBLOC from the selected + duty location + operationId: showCounselingOffices + tags: + - transportationOffice + parameters: + - in: path + name: dutyLocationId + format: uuid + type: string + required: true + description: UUID of the duty location + produces: + - application/json + responses: + '200': + description: Successfully retrieved counseling offices + schema: + $ref: '#/definitions/CounselingOffices' + '500': + description: internal server error /uploads: post: summary: Create a new upload @@ -6040,6 +6065,11 @@ definitions: type: string format: uuid example: c56a4180-65aa-42ec-a945-5fd21dec0538 + counselingOfficeId: + type: string + format: uuid + example: cf1addea-a4f9-4173-8506-2bb82a064cb7 + x-nullable: true ordersNumber: type: string title: Orders Number @@ -8291,6 +8321,23 @@ definitions: type: array items: type: string + CounselingOffices: + type: array + items: + $ref: '#/definitions/CounselingOffice' + CounselingOffice: + type: object + properties: + id: + type: string + format: uuid + example: c56a4180-65aa-42ec-a945-5fd21dec0538 + name: + type: string + example: Fort Bragg North Station + required: + - id + - name MovePayload: type: object properties: