diff --git a/migrations/app/migrations_manifest.txt b/migrations/app/migrations_manifest.txt index c08d3d87e09..8c2120f0dd7 100644 --- a/migrations/app/migrations_manifest.txt +++ b/migrations/app/migrations_manifest.txt @@ -1070,3 +1070,4 @@ 20241230190647_add_missing_AK_zips_to_zip3_distances.up.sql 20250103130619_revert_data_change_for_gbloc_for_ak.up.sql 20250103180420_update_pricing_proc_to_use_local_price_variable.up.sql +20250110153428_add_shipment_address_updates_to_move_history.up.sql diff --git a/migrations/app/schema/20250110153428_add_shipment_address_updates_to_move_history.up.sql b/migrations/app/schema/20250110153428_add_shipment_address_updates_to_move_history.up.sql new file mode 100644 index 00000000000..ec30212f12c --- /dev/null +++ b/migrations/app/schema/20250110153428_add_shipment_address_updates_to_move_history.up.sql @@ -0,0 +1,9 @@ +-- adding shipment_address_updates table to move history so we can track the activity +SELECT add_audit_history_table( + target_table := 'shipment_address_updates', + audit_rows := BOOLEAN 't', + audit_query_text := BOOLEAN 't', + ignored_cols := ARRAY[ + 'created_at' + ] +); \ No newline at end of file diff --git a/pkg/assets/sql_scripts/move_history_fetcher.sql b/pkg/assets/sql_scripts/move_history_fetcher.sql index 305c682ec4f..75f961fa881 100644 --- a/pkg/assets/sql_scripts/move_history_fetcher.sql +++ b/pkg/assets/sql_scripts/move_history_fetcher.sql @@ -646,6 +646,25 @@ WITH move AS ( JOIN gsr_appeals ON gsr_appeals.id = audit_history.object_id WHERE audit_history.table_name = 'gsr_appeals' ), + shipment_address_updates AS ( + SELECT shipment_address_updates.*, + jsonb_agg(jsonb_build_object( + 'status', shipment_address_updates.status + ) + )::TEXT AS context + FROM shipment_address_updates + JOIN move_shipments ON shipment_address_updates.shipment_id = move_shipments.id + GROUP BY shipment_address_updates.id + ), + shipment_address_updates_logs as ( + SELECT audit_history.*, + shipment_address_updates.context AS context, + NULL AS context_id + FROM + audit_history + JOIN shipment_address_updates ON shipment_address_updates.id = audit_history.object_id + WHERE audit_history.table_name = 'shipment_address_updates' + ), combined_logs AS ( SELECT * @@ -736,6 +755,11 @@ WITH move AS ( * FROM gsr_appeals_logs + UNION + SELECT + * + FROM + shipment_address_updates_logs ) diff --git a/pkg/gen/ghcapi/embedded_spec.go b/pkg/gen/ghcapi/embedded_spec.go index 61bd52e4f5c..fd865c3c891 100644 --- a/pkg/gen/ghcapi/embedded_spec.go +++ b/pkg/gen/ghcapi/embedded_spec.go @@ -5935,7 +5935,8 @@ func init() { "application/json" ], "tags": [ - "shipment" + "shipment", + "shipment_address_updates" ], "summary": "Allows TOO to review a shipment address update", "operationId": "reviewShipmentAddressUpdate", @@ -22989,7 +22990,8 @@ func init() { "application/json" ], "tags": [ - "shipment" + "shipment", + "shipment_address_updates" ], "summary": "Allows TOO to review a shipment address update", "operationId": "reviewShipmentAddressUpdate", diff --git a/pkg/gen/ghcapi/ghcoperations/shipment/review_shipment_address_update.go b/pkg/gen/ghcapi/ghcoperations/shipment/review_shipment_address_update.go index d4532a282ce..61dafe8bc53 100644 --- a/pkg/gen/ghcapi/ghcoperations/shipment/review_shipment_address_update.go +++ b/pkg/gen/ghcapi/ghcoperations/shipment/review_shipment_address_update.go @@ -36,7 +36,7 @@ func NewReviewShipmentAddressUpdate(ctx *middleware.Context, handler ReviewShipm } /* - ReviewShipmentAddressUpdate swagger:route PATCH /shipments/{shipmentID}/review-shipment-address-update shipment reviewShipmentAddressUpdate + ReviewShipmentAddressUpdate swagger:route PATCH /shipments/{shipmentID}/review-shipment-address-update shipment shipment_address_updates reviewShipmentAddressUpdate # Allows TOO to review a shipment address update diff --git a/pkg/models/re_contract.go b/pkg/models/re_contract.go index 2c4b4a28e35..c0576ce403b 100644 --- a/pkg/models/re_contract.go +++ b/pkg/models/re_contract.go @@ -1,12 +1,17 @@ package models import ( + "database/sql" + "fmt" "time" "github.com/gobuffalo/pop/v6" "github.com/gobuffalo/validate/v3" "github.com/gobuffalo/validate/v3/validators" "github.com/gofrs/uuid" + + "github.com/transcom/mymove/pkg/appcontext" + "github.com/transcom/mymove/pkg/apperror" ) // ReContract represents a contract with pricing information @@ -32,3 +37,30 @@ func (r *ReContract) Validate(_ *pop.Connection) (*validate.Errors, error) { &validators.StringIsPresent{Field: r.Name, Name: "Name"}, ), nil } + +func FetchContractForMove(appCtx appcontext.AppContext, moveID uuid.UUID) (ReContract, error) { + var move Move + err := appCtx.DB().Find(&move, moveID) + if err != nil { + if err == sql.ErrNoRows { + return ReContract{}, apperror.NewNotFoundError(moveID, "looking for Move") + } + return ReContract{}, err + } + + if move.AvailableToPrimeAt == nil { + return ReContract{}, apperror.NewConflictError(moveID, "unable to pick contract because move is not available to prime") + } + + var contractYear ReContractYear + err = appCtx.DB().EagerPreload("Contract").Where("? between start_date and end_date", move.AvailableToPrimeAt). + First(&contractYear) + if err != nil { + if err == sql.ErrNoRows { + return ReContract{}, apperror.NewNotFoundError(uuid.Nil, fmt.Sprintf("no contract year found for %s", move.AvailableToPrimeAt.String())) + } + return ReContract{}, err + } + + return contractYear.Contract, nil +} diff --git a/pkg/models/re_contract_test.go b/pkg/models/re_contract_test.go index c2148951ede..9fca3401f22 100644 --- a/pkg/models/re_contract_test.go +++ b/pkg/models/re_contract_test.go @@ -1,7 +1,11 @@ package models_test import ( + "time" + + "github.com/transcom/mymove/pkg/factory" "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/testdatagen" ) func (suite *ModelSuite) TestReContractValidations() { @@ -23,3 +27,30 @@ func (suite *ModelSuite) TestReContractValidations() { suite.verifyValidationErrors(&emptyReContract, expErrors) }) } + +func (suite *ModelSuite) TestFetchContractForMove() { + suite.Run("finds valid contract", func() { + reContract := testdatagen.FetchOrMakeReContract(suite.DB(), testdatagen.Assertions{}) + testdatagen.FetchOrMakeReContractYear(suite.DB(), testdatagen.Assertions{ + ReContractYear: models.ReContractYear{ + Contract: reContract, + ContractID: reContract.ID, + StartDate: time.Now(), + EndDate: time.Now().Add(time.Hour * 12), + Escalation: 1.0, + EscalationCompounded: 1.0, + }, + }) + move := factory.BuildAvailableToPrimeMove(suite.DB(), nil, nil) + contract, err := models.FetchContractForMove(suite.AppContextForTest(), move.ID) + suite.NoError(err) + suite.Equal(contract.ID, reContract.ID) + }) + + suite.Run("returns error if no contract found", func() { + move := factory.BuildAvailableToPrimeMove(suite.DB(), nil, nil) + contract, err := models.FetchContractForMove(suite.AppContextForTest(), move.ID) + suite.Error(err) + suite.Equal(contract, models.ReContract{}) + }) +} diff --git a/pkg/models/re_oconus_rate_areas.go b/pkg/models/re_oconus_rate_areas.go index 72b85773159..84def705f95 100644 --- a/pkg/models/re_oconus_rate_areas.go +++ b/pkg/models/re_oconus_rate_areas.go @@ -3,6 +3,7 @@ package models import ( "time" + "github.com/gobuffalo/pop/v6" "github.com/gofrs/uuid" ) @@ -19,3 +20,16 @@ type OconusRateArea struct { func (o OconusRateArea) TableName() string { return "re_oconus_rate_areas" } + +func FetchOconusRateArea(db *pop.Connection, zip string) (*OconusRateArea, error) { + var reOconusRateArea OconusRateArea + err := db.Q(). + InnerJoin("re_rate_areas ra", "re_oconus_rate_areas.rate_area_id = ra.id"). + InnerJoin("us_post_region_cities upc", "upc.id = re_oconus_rate_areas.us_post_region_cities_id"). + Where("upc.uspr_zip_id = ?", zip). + First(&reOconusRateArea) + if err != nil { + return nil, err + } + return &reOconusRateArea, nil +} diff --git a/pkg/models/re_rate_area.go b/pkg/models/re_rate_area.go index 7b613b42a28..8eb7c56328f 100644 --- a/pkg/models/re_rate_area.go +++ b/pkg/models/re_rate_area.go @@ -56,8 +56,8 @@ func FetchReRateAreaItem(tx *pop.Connection, contractID uuid.UUID, code string) } // a db stored proc that takes in an address id & a service code to get the rate area id for an address -func FetchRateAreaID(db *pop.Connection, addressID uuid.UUID, serviceID uuid.UUID, contractID uuid.UUID) (uuid.UUID, error) { - if addressID != uuid.Nil && serviceID != uuid.Nil && contractID != uuid.Nil { +func FetchRateAreaID(db *pop.Connection, addressID uuid.UUID, serviceID *uuid.UUID, contractID uuid.UUID) (uuid.UUID, error) { + if addressID != uuid.Nil && contractID != uuid.Nil { var rateAreaID uuid.UUID err := db.RawQuery("SELECT get_rate_area_id($1, $2, $3)", addressID, serviceID, contractID).First(&rateAreaID) if err != nil { @@ -67,3 +67,17 @@ func FetchRateAreaID(db *pop.Connection, addressID uuid.UUID, serviceID uuid.UUI } return uuid.Nil, fmt.Errorf("error fetching rate area ID - required parameters not provided") } + +func FetchConusRateAreaByPostalCode(db *pop.Connection, zip string, contractID uuid.UUID) (*ReRateArea, error) { + var reRateArea ReRateArea + postalCode := zip[0:3] + err := db.Q(). + InnerJoin("re_zip3s rz", "rz.rate_area_id = re_rate_areas.id"). + Where("zip3 = ?", postalCode). + Where("re_rate_areas.contract_id = ?", contractID). + First(&reRateArea) + if err != nil { + return nil, err + } + return &reRateArea, nil +} diff --git a/pkg/models/re_rate_area_test.go b/pkg/models/re_rate_area_test.go index 87f310c2088..ab279418976 100644 --- a/pkg/models/re_rate_area_test.go +++ b/pkg/models/re_rate_area_test.go @@ -36,16 +36,14 @@ func (suite *ModelSuite) TestFetchRateAreaID() { service := factory.FetchReServiceByCode(suite.DB(), models.ReServiceCodeIHPK) contract := testdatagen.FetchOrMakeReContract(suite.DB(), testdatagen.Assertions{}) address := factory.BuildAddress(suite.DB(), nil, nil) - rateAreaId, err := models.FetchRateAreaID(suite.DB(), address.ID, service.ID, contract.ID) + rateAreaId, err := models.FetchRateAreaID(suite.DB(), address.ID, &service.ID, contract.ID) suite.NotNil(rateAreaId) suite.NoError(err) }) suite.Run("fail - receive error when not all values are provided", func() { - var nilUuid uuid.UUID - contract := testdatagen.FetchOrMakeReContract(suite.DB(), testdatagen.Assertions{}) address := factory.BuildAddress(suite.DB(), nil, nil) - rateAreaId, err := models.FetchRateAreaID(suite.DB(), address.ID, nilUuid, contract.ID) + rateAreaId, err := models.FetchRateAreaID(suite.DB(), address.ID, nil, uuid.Nil) suite.Equal(uuid.Nil, rateAreaId) suite.Error(err) }) diff --git a/pkg/payment_request/service_param_value_lookups/per_unit_cents_lookup.go b/pkg/payment_request/service_param_value_lookups/per_unit_cents_lookup.go index b339fbf43dd..68d59f13d27 100644 --- a/pkg/payment_request/service_param_value_lookups/per_unit_cents_lookup.go +++ b/pkg/payment_request/service_param_value_lookups/per_unit_cents_lookup.go @@ -24,7 +24,7 @@ func (p PerUnitCentsLookup) lookup(appCtx appcontext.AppContext, s *ServiceItemP switch p.ServiceItem.ReService.Code { case models.ReServiceCodeIHPK: // IHPK: Need rate area ID for the pickup address - rateAreaID, err := models.FetchRateAreaID(appCtx.DB(), *p.MTOShipment.PickupAddressID, serviceID, contractID) + rateAreaID, err := models.FetchRateAreaID(appCtx.DB(), *p.MTOShipment.PickupAddressID, &serviceID, contractID) if err != nil { return "", fmt.Errorf("error fetching rate area id for shipment ID: %s and service ID %s: %s", p.MTOShipment.ID, serviceID, err) } @@ -43,7 +43,7 @@ func (p PerUnitCentsLookup) lookup(appCtx appcontext.AppContext, s *ServiceItemP case models.ReServiceCodeIHUPK: // IHUPK: Need rate area ID for the destination address - rateAreaID, err := models.FetchRateAreaID(appCtx.DB(), *p.MTOShipment.PickupAddressID, serviceID, contractID) + rateAreaID, err := models.FetchRateAreaID(appCtx.DB(), *p.MTOShipment.PickupAddressID, &serviceID, contractID) if err != nil { return "", fmt.Errorf("error fetching rate area id for shipment ID: %s and service ID %s: %s", p.MTOShipment.ID, serviceID, err) } @@ -62,11 +62,11 @@ func (p PerUnitCentsLookup) lookup(appCtx appcontext.AppContext, s *ServiceItemP case models.ReServiceCodeISLH: // ISLH: Need rate area IDs for origin and destination - originRateAreaID, err := models.FetchRateAreaID(appCtx.DB(), *p.MTOShipment.PickupAddressID, serviceID, contractID) + originRateAreaID, err := models.FetchRateAreaID(appCtx.DB(), *p.MTOShipment.PickupAddressID, &serviceID, contractID) if err != nil { return "", fmt.Errorf("error fetching rate area id for origin address for shipment ID: %s and service ID %s: %s", p.MTOShipment.ID, serviceID, err) } - destRateAreaID, err := models.FetchRateAreaID(appCtx.DB(), *p.MTOShipment.DestinationAddressID, serviceID, contractID) + destRateAreaID, err := models.FetchRateAreaID(appCtx.DB(), *p.MTOShipment.DestinationAddressID, &serviceID, contractID) if err != nil { return "", fmt.Errorf("error fetching rate area id for destination address for shipment ID: %s and service ID %s: %s", p.MTOShipment.ID, serviceID, err) } diff --git a/pkg/services/move_history/move_history_fetcher_test.go b/pkg/services/move_history/move_history_fetcher_test.go index b64c2bd3f4a..1aabbab0a2b 100644 --- a/pkg/services/move_history/move_history_fetcher_test.go +++ b/pkg/services/move_history/move_history_fetcher_test.go @@ -252,8 +252,10 @@ func (suite *MoveHistoryServiceSuite) TestMoveHistoryFetcherFunctionality() { auditHistoryContains := func(auditHistories models.AuditHistories, keyword string) func() (success bool) { return func() (success bool) { for _, record := range auditHistories { - if strings.Contains(*record.ChangedData, keyword) { - return true + if record.ChangedData != nil { + if strings.Contains(*record.ChangedData, keyword) { + return true + } } } return false diff --git a/pkg/services/shipment_address_update/shipment_address_update_requester.go b/pkg/services/shipment_address_update/shipment_address_update_requester.go index 6d99f05a3a1..53d845af020 100644 --- a/pkg/services/shipment_address_update/shipment_address_update_requester.go +++ b/pkg/services/shipment_address_update/shipment_address_update_requester.go @@ -38,10 +38,10 @@ func NewShipmentAddressUpdateRequester(planner route.Planner, addressCreator ser } } -func (f *shipmentAddressUpdateRequester) isAddressChangeDistanceOver50(appCtx appcontext.AppContext, addressUpdate models.ShipmentAddressUpdate) (bool, error) { +func (f *shipmentAddressUpdateRequester) isAddressChangeDistanceOver50(appCtx appcontext.AppContext, addressUpdate models.ShipmentAddressUpdate, isInternationalShipment bool) (bool, error) { - //We calculate and set the distance between the old and new address - distance, err := f.planner.ZipTransitDistance(appCtx, addressUpdate.OriginalAddress.PostalCode, addressUpdate.NewAddress.PostalCode, false, false) + // We calculate and set the distance between the old and new address + distance, err := f.planner.ZipTransitDistance(appCtx, addressUpdate.OriginalAddress.PostalCode, addressUpdate.NewAddress.PostalCode, false, isInternationalShipment) if err != nil { return false, err } @@ -52,32 +52,62 @@ func (f *shipmentAddressUpdateRequester) isAddressChangeDistanceOver50(appCtx ap return true, nil } -func (f *shipmentAddressUpdateRequester) doesDeliveryAddressUpdateChangeServiceArea(appCtx appcontext.AppContext, contractID uuid.UUID, originalDeliveryAddress models.Address, newDeliveryAddress models.Address) (bool, error) { - var existingServiceArea models.ReZip3 - var actualServiceArea models.ReZip3 +func (f *shipmentAddressUpdateRequester) doesDeliveryAddressUpdateChangeServiceOrRateArea(appCtx appcontext.AppContext, contractID uuid.UUID, originalDeliveryAddress models.Address, newDeliveryAddress models.Address, shipment models.MTOShipment) (bool, error) { + // international shipments find their rate areas differently than domestic + if shipment.MarketCode == models.MarketCodeInternational { + // we already have the origin address in the db so we can check the rate area using the db func + originalRateArea, err := models.FetchRateAreaID(appCtx.DB(), originalDeliveryAddress.ID, nil, contractID) + if err != nil || originalRateArea == uuid.Nil { + return false, err + } + // since the new address isn't created yet we can't use the db func since it doesn't have an id, + // we need to manually find the rate area using the postal code + var updateRateArea uuid.UUID + newRateArea, err := models.FetchOconusRateArea(appCtx.DB(), newDeliveryAddress.PostalCode) + if err != nil && err != sql.ErrNoRows { + return false, err + } else if err == sql.ErrNoRows { // if we got no rows then the new address is likely CONUS + newRateArea, err := models.FetchConusRateAreaByPostalCode(appCtx.DB(), newDeliveryAddress.PostalCode, contractID) + if err != nil && err != sql.ErrNoRows { + return false, err + } + updateRateArea = newRateArea.ID + } else { + updateRateArea = newRateArea.RateAreaId + } + // if these are different, we need the TOO to approve this request since it will change ISLH pricing + if originalRateArea != updateRateArea { + return true, nil + } else { + return false, nil + } + } else { + var existingServiceArea models.ReZip3 + var actualServiceArea models.ReZip3 - originalZip := originalDeliveryAddress.PostalCode[0:3] - destinationZip := newDeliveryAddress.PostalCode[0:3] + originalZip := originalDeliveryAddress.PostalCode[0:3] + destinationZip := newDeliveryAddress.PostalCode[0:3] - if originalZip == destinationZip { - // If the ZIP hasn't changed, we must be in the same service area - return false, nil - } + if originalZip == destinationZip { + // If the ZIP hasn't changed, we must be in the same service area + return false, nil + } - err := appCtx.DB().Where("zip3 = ?", originalZip).Where("contract_id = ?", contractID).First(&existingServiceArea) - if err != nil { - return false, err - } + err := appCtx.DB().Where("zip3 = ?", originalZip).Where("contract_id = ?", contractID).First(&existingServiceArea) + if err != nil { + return false, err + } - err = appCtx.DB().Where("zip3 = ?", destinationZip).Where("contract_id = ?", contractID).First(&actualServiceArea) - if err != nil { - return false, err - } + err = appCtx.DB().Where("zip3 = ?", destinationZip).Where("contract_id = ?", contractID).First(&actualServiceArea) + if err != nil { + return false, err + } - if existingServiceArea.DomesticServiceAreaID != actualServiceArea.DomesticServiceAreaID { - return true, nil + if existingServiceArea.DomesticServiceAreaID != actualServiceArea.DomesticServiceAreaID { + return true, nil + } + return false, nil } - return false, nil } func (f *shipmentAddressUpdateRequester) doesDeliveryAddressUpdateChangeMileageBracket(appCtx appcontext.AppContext, originalPickupAddress models.Address, originalDeliveryAddress, newDeliveryAddress models.Address) (bool, error) { @@ -251,6 +281,7 @@ func (f *shipmentAddressUpdateRequester) RequestShipmentDeliveryAddressUpdate(ap if eTag != etag.GenerateEtag(shipment.UpdatedAt) { return nil, apperror.NewPreconditionFailedError(shipmentID, nil) } + isInternationalShipment := shipment.MarketCode == models.MarketCodeInternational shipmentHasApprovedDestSIT := f.doesShipmentContainApprovedDestinationSIT(shipment) @@ -333,12 +364,13 @@ func (f *shipmentAddressUpdateRequester) RequestShipmentDeliveryAddressUpdate(ap return nil, err } - updateNeedsTOOReview, err := f.doesDeliveryAddressUpdateChangeServiceArea(appCtx, contract.ID, addressUpdate.OriginalAddress, newAddress) + updateNeedsTOOReview, err := f.doesDeliveryAddressUpdateChangeServiceOrRateArea(appCtx, contract.ID, addressUpdate.OriginalAddress, newAddress, shipment) if err != nil { return nil, err } - if !updateNeedsTOOReview { + // international shipments don't need to be concerned with shorthaul/linehaul + if !updateNeedsTOOReview && !isInternationalShipment { if shipment.ShipmentType == models.MTOShipmentTypeHHG { updateNeedsTOOReview, err = f.doesDeliveryAddressUpdateChangeShipmentPricingType(*shipment.PickupAddress, addressUpdate.OriginalAddress, newAddress) if err != nil { @@ -354,7 +386,7 @@ func (f *shipmentAddressUpdateRequester) RequestShipmentDeliveryAddressUpdate(ap } } - if !updateNeedsTOOReview { + if !updateNeedsTOOReview && !isInternationalShipment { if shipment.ShipmentType == models.MTOShipmentTypeHHG { updateNeedsTOOReview, err = f.doesDeliveryAddressUpdateChangeMileageBracket(appCtx, *shipment.PickupAddress, addressUpdate.OriginalAddress, newAddress) if err != nil { @@ -371,7 +403,7 @@ func (f *shipmentAddressUpdateRequester) RequestShipmentDeliveryAddressUpdate(ap } if !updateNeedsTOOReview { - updateNeedsTOOReview, err = f.isAddressChangeDistanceOver50(appCtx, addressUpdate) + updateNeedsTOOReview, err = f.isAddressChangeDistanceOver50(appCtx, addressUpdate, isInternationalShipment) if err != nil { return nil, err } @@ -390,7 +422,7 @@ func (f *shipmentAddressUpdateRequester) RequestShipmentDeliveryAddressUpdate(ap return apperror.NewQueryError("ShipmentAddressUpdate", txnErr, "error saving shipment address update request") } - //Get the move + // Get the move var move models.Move err := txnAppCtx.DB().Find(&move, shipment.MoveTaskOrderID) if err != nil { @@ -463,6 +495,7 @@ func (f *shipmentAddressUpdateRequester) ReviewShipmentAddressChange(appCtx appc } shipment = addressUpdate.Shipment + isInternationalShipment := shipment.MarketCode == models.MarketCodeInternational if tooApprovalStatus == models.ShipmentAddressUpdateStatusApproved { queryBuilder := query.NewQueryBuilder() @@ -472,6 +505,7 @@ func (f *shipmentAddressUpdateRequester) ReviewShipmentAddressChange(appCtx appc addressUpdate.Status = models.ShipmentAddressUpdateStatusApproved addressUpdate.OfficeRemarks = &tooRemarks shipment.DestinationAddress = &addressUpdate.NewAddress + shipment.DestinationAddressID = &addressUpdate.NewAddressID var haulPricingTypeHasChanged bool if shipment.ShipmentType == models.MTOShipmentTypeHHG { @@ -526,7 +560,7 @@ func (f *shipmentAddressUpdateRequester) ReviewShipmentAddressChange(appCtx appc // If the pricing type has changed then we automatically reject the DLH or DSH service item on the shipment since it is now inaccurate var approvedPaymentRequestsExistsForServiceItem bool - if haulPricingTypeHasChanged && len(shipment.MTOServiceItems) > 0 { + if haulPricingTypeHasChanged && len(shipment.MTOServiceItems) > 0 && !isInternationalShipment { serviceItems := shipment.MTOServiceItems autoRejectionRemark := "Automatically rejected due to change in destination address affecting the ZIP code qualification for short haul / line haul." var regeneratedServiceItems models.MTOServiceItems @@ -630,7 +664,7 @@ func (f *shipmentAddressUpdateRequester) ReviewShipmentAddressChange(appCtx appc // if the shipment has an estimated weight, we need to update the service item pricing since we know the distances have changed // this only applies to international shipments that the TOO is approving the address change for if shipment.PrimeEstimatedWeight != nil && - shipment.MarketCode == models.MarketCodeInternational && + isInternationalShipment && tooApprovalStatus == models.ShipmentAddressUpdateStatusApproved { portZip, portType, err := models.GetPortLocationInfoForShipment(appCtx.DB(), shipment.ID) if err != nil { diff --git a/pkg/services/shipment_address_update/shipment_address_update_requester_test.go b/pkg/services/shipment_address_update/shipment_address_update_requester_test.go index e209730dee7..7a7e12a3733 100644 --- a/pkg/services/shipment_address_update/shipment_address_update_requester_test.go +++ b/pkg/services/shipment_address_update/shipment_address_update_requester_test.go @@ -102,7 +102,7 @@ func (suite *ShipmentAddressUpdateServiceSuite) TestCreateApprovedShipmentAddres moveRouter := moveservices.NewMoveRouter() addressUpdateRequester := NewShipmentAddressUpdateRequester(mockPlanner, addressCreator, moveRouter) - suite.Run("Successfully create ShipmentAddressUpdate", func() { + suite.Run("Successfully create ShipmentAddressUpdate for a domestic shipment", func() { mockPlanner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), "90210", @@ -144,6 +144,142 @@ func (suite *ShipmentAddressUpdateServiceSuite) TestCreateApprovedShipmentAddres suite.Equal(newAddress.City, updatedShipment.DestinationAddress.City) }) + suite.Run("Successfully create ShipmentAddressUpdate for an international shipment that requires approval", func() { + mockPlanner.On("ZipTransitDistance", + mock.AnythingOfType("*appcontext.appContext"), + "90210", + "94535", + false, + false, + ).Return(2500, nil).Twice() + mockPlanner.On("ZipTransitDistance", + mock.AnythingOfType("*appcontext.appContext"), + "94535", + "94535", + false, + false, + ).Return(2500, nil).Once() + move := setupTestData() + + pickupAddress := factory.BuildAddress(suite.DB(), []factory.Customization{ + { + Model: models.Address{ + StreetAddress1: "Tester Address", + City: "Des Moines", + State: "IA", + PostalCode: "50314", + IsOconus: models.BoolPointer(false), + }, + }, + }, nil) + destinationAddress := factory.BuildAddress(suite.DB(), []factory.Customization{ + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "Anchorage", + State: "AK", + PostalCode: "99505", + IsOconus: models.BoolPointer(true), + }, + }, + }, nil) + shipment := factory.BuildMTOShipmentWithMove(&move, suite.DB(), []factory.Customization{ + { + Model: models.MTOShipment{ + MarketCode: models.MarketCodeInternational, + PickupAddressID: &pickupAddress.ID, + DestinationAddressID: &destinationAddress.ID, + }, + }, + }, nil) + + newAddress := models.Address{ + StreetAddress1: "Colder Ave.", + City: "Klawock", + State: "AK", + PostalCode: "99925", + } + suite.NotEmpty(move.MTOShipments) + update, err := addressUpdateRequester.RequestShipmentDeliveryAddressUpdate(suite.AppContextForTest(), shipment.ID, newAddress, "we really need to change the address", etag.GenerateEtag(shipment.UpdatedAt)) + suite.NoError(err) + suite.NotNil(update) + suite.Equal(models.ShipmentAddressUpdateStatusRequested, update.Status) + + // Make sure the destination address on the shipment was NOT updated + var updatedShipment models.MTOShipment + err = suite.DB().EagerPreload("DestinationAddress").Find(&updatedShipment, shipment.ID) + suite.NoError(err) + + suite.NotEqual(newAddress.StreetAddress1, updatedShipment.DestinationAddress.StreetAddress1) + suite.NotEqual(newAddress.PostalCode, updatedShipment.DestinationAddress.PostalCode) + suite.NotEqual(newAddress.City, updatedShipment.DestinationAddress.City) + }) + + suite.Run("Successfully create ShipmentAddressUpdate for an international shipment that requires approval", func() { + mockPlanner.On("ZipTransitDistance", + mock.AnythingOfType("*appcontext.appContext"), + "99505", + "99506", + false, + true, + ).Return(49, nil) + move := setupTestData() + + pickupAddress := factory.BuildAddress(suite.DB(), []factory.Customization{ + { + Model: models.Address{ + StreetAddress1: "Tester Address", + City: "Des Moines", + State: "IA", + PostalCode: "50314", + IsOconus: models.BoolPointer(false), + }, + }, + }, nil) + destinationAddress := factory.BuildAddress(suite.DB(), []factory.Customization{ + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "Anchorage", + State: "AK", + PostalCode: "99505", + IsOconus: models.BoolPointer(true), + }, + }, + }, nil) + shipment := factory.BuildMTOShipmentWithMove(&move, suite.DB(), []factory.Customization{ + { + Model: models.MTOShipment{ + MarketCode: models.MarketCodeInternational, + PickupAddressID: &pickupAddress.ID, + DestinationAddressID: &destinationAddress.ID, + }, + }, + }, nil) + + // this shouldn't change the rate area + newAddress := models.Address{ + StreetAddress1: "Elsewhere Ave.", + City: "Anchorage", + State: "AK", + PostalCode: "99506", + } + suite.NotEmpty(move.MTOShipments) + update, err := addressUpdateRequester.RequestShipmentDeliveryAddressUpdate(suite.AppContextForTest(), shipment.ID, newAddress, "we really need to change the address", etag.GenerateEtag(shipment.UpdatedAt)) + suite.NoError(err) + suite.NotNil(update) + suite.Equal(models.ShipmentAddressUpdateStatusApproved, update.Status) + + // Make sure the destination address on the shipment was updated + var updatedShipment models.MTOShipment + err = suite.DB().EagerPreload("DestinationAddress").Find(&updatedShipment, shipment.ID) + suite.NoError(err) + + suite.Equal(newAddress.StreetAddress1, updatedShipment.DestinationAddress.StreetAddress1) + suite.Equal(newAddress.PostalCode, updatedShipment.DestinationAddress.PostalCode) + suite.Equal(newAddress.City, updatedShipment.DestinationAddress.City) + }) + suite.Run("Update with invalid etag should fail", func() { move := setupTestData() shipment := factory.BuildMTOShipmentWithMove(&move, suite.DB(), nil, nil) diff --git a/src/components/Office/AddressUpdatePreview/AddressUpdatePreview.jsx b/src/components/Office/AddressUpdatePreview/AddressUpdatePreview.jsx index 74a2ac1e06b..dc7e9d3a9d4 100644 --- a/src/components/Office/AddressUpdatePreview/AddressUpdatePreview.jsx +++ b/src/components/Office/AddressUpdatePreview/AddressUpdatePreview.jsx @@ -9,23 +9,35 @@ import DataTable from 'components/DataTable/index'; import { formatTwoLineAddress } from 'utils/shipmentDisplay'; import DataTableWrapper from 'components/DataTableWrapper'; import { ShipmentAddressUpdateShape } from 'types'; +import { MARKET_CODES } from 'shared/constants'; -const AddressUpdatePreview = ({ deliveryAddressUpdate }) => { +const AddressUpdatePreview = ({ deliveryAddressUpdate, shipment }) => { const { originalAddress, newAddress, contractorRemarks } = deliveryAddressUpdate; const newSitMileage = deliveryAddressUpdate.newSitDistanceBetween; + const { marketCode } = shipment; return (