diff --git a/migrations/app/schema/20250114164752_add_ppm_estimated_incentive_proc.up.sql b/migrations/app/schema/20250114164752_add_ppm_estimated_incentive_proc.up.sql index 47436e133a9..5aff4a61dd3 100644 --- a/migrations/app/schema/20250114164752_add_ppm_estimated_incentive_proc.up.sql +++ b/migrations/app/schema/20250114164752_add_ppm_estimated_incentive_proc.up.sql @@ -1,3 +1,47 @@ +-- inserting params for IDFSIT +INSERT INTO service_params (id,service_id,service_item_param_key_id,created_at,updated_at,is_optional) VALUES + ('fb7925e7-ebfe-49d9-9cf4-7219e68ec686'::uuid,'bd6064ca-e780-4ab4-a37b-0ae98eebb244','597bb77e-0ce7-4ba2-9624-24300962625f','2024-01-17 15:55:50.041957','2024-01-17 15:55:50.041957',false); -- PerUnitCents + +-- IDASIT +INSERT INTO service_params (id,service_id,service_item_param_key_id,created_at,updated_at,is_optional) VALUES + ('51393ee1-f505-4f7b-96c4-135f771af814'::uuid,'806c6d59-57ff-4a3f-9518-ebf29ba9cb10','597bb77e-0ce7-4ba2-9624-24300962625f','2024-01-17 15:55:50.041957','2024-01-17 15:55:50.041957',false); -- PerUnitCents + +-- IOFSIT +INSERT INTO service_params (id,service_id,service_item_param_key_id,created_at,updated_at,is_optional) VALUES + ('7518ec84-0c40-4c17-86dd-3ce04e2fe701'::uuid,'b488bf85-ea5e-49c8-ba5c-e2fa278ac806','597bb77e-0ce7-4ba2-9624-24300962625f','2024-01-17 15:55:50.041957','2024-01-17 15:55:50.041957',false); -- PerUnitCents + +-- IOASIT +INSERT INTO service_params (id,service_id,service_item_param_key_id,created_at,updated_at,is_optional) VALUES + ('cff34123-e2a5-40ed-9cf3-451701850a26'::uuid,'bd424e45-397b-4766-9712-de4ae3a2da36','597bb77e-0ce7-4ba2-9624-24300962625f','2024-01-17 15:55:50.041957','2024-01-17 15:55:50.041957',false); -- PerUnitCents + +-- inserting params for FSC +INSERT INTO service_params (id,service_id,service_item_param_key_id,created_at,updated_at,is_optional) VALUES + ('bb53e034-80c2-420e-8492-f54d2018fff1'::uuid,'4780b30c-e846-437a-b39a-c499a6b09872','d9ad3878-4b94-4722-bbaf-d4b8080f339d','2024-01-17 15:55:50.041957','2024-01-17 15:55:50.041957',true); -- PortZip + +-- remove PriceAreaIntlOrigin, we don't need it +DELETE FROM service_params +WHERE service_item_param_key_id = '6d44624c-b91b-4226-8fcd-98046e2f433d'; + +-- remove PriceAreaIntlDest, we don't need it +DELETE FROM service_params +WHERE service_item_param_key_id = '4736f489-dfda-4df1-a303-8c434a120d5d'; + +-- func to fetch a service id from re_services by providing the service code +CREATE OR REPLACE FUNCTION get_service_id(service_code TEXT) RETURNS UUID AS $$ +DECLARE + service_id UUID; +BEGIN + SELECT rs.id INTO service_id FROM re_services rs WHERE rs.code = service_code; + IF service_id IS NULL THEN + RAISE EXCEPTION 'Service code % not found in re_services', service_code; + END IF; + RETURN service_id; +END; +$$ LANGUAGE plpgsql; + + +-- db proc that will calculate a PPM's incentive +-- this is used for estimated/final/max incentives CREATE OR REPLACE FUNCTION calculate_ppm_incentive( ppm_id UUID, pickup_address_id UUID, @@ -32,7 +76,7 @@ BEGIN RAISE EXCEPTION 'is_estimated, is_actual, and is_max cannot all be FALSE. No update will be performed.'; END IF; - -- Validating it's a real PPM + -- validating it's a real PPM SELECT ppms.id INTO ppm FROM ppm_shipments ppms WHERE ppms.id = ppm_id; IF ppm IS NULL THEN RAISE EXCEPTION 'PPM with ID % not found', ppm_id; @@ -54,7 +98,7 @@ BEGIN END IF; -- ISLH calculation - SELECT rs.id INTO service_id FROM re_services rs WHERE rs.code = 'ISLH'; + service_id := get_service_id('ISLH'); price_islh := ROUND( calculate_escalated_price( o_rate_area_id, @@ -67,7 +111,7 @@ BEGIN ); -- IHPK calculation - SELECT rs.id INTO service_id FROM re_services rs WHERE rs.code = 'IHPK'; + service_id := get_service_id('IHPK'); price_ihpk := ROUND( calculate_escalated_price( o_rate_area_id, @@ -80,7 +124,7 @@ BEGIN ); -- IHUPK calculation - SELECT rs.id INTO service_id FROM re_services rs WHERE rs.code = 'IHUPK'; + service_id := get_service_id('IHUPK'); price_ihupk := ROUND( calculate_escalated_price( NULL, @@ -99,17 +143,95 @@ BEGIN cents_above_baseline := mileage * estimated_fsc_multiplier; price_fsc := ROUND((cents_above_baseline * price_difference) * 100); - -- Total incentive total_incentive := price_islh + price_ihpk + price_ihupk + price_fsc; - -- Update the PPM incentive values UPDATE ppm_shipments SET estimated_incentive = CASE WHEN is_estimated THEN total_incentive ELSE estimated_incentive END, final_incentive = CASE WHEN is_actual THEN total_incentive ELSE final_incentive END, max_incentive = CASE WHEN is_max THEN total_incentive ELSE max_incentive END WHERE id = ppm_id; - -- Return all values + -- returning a table so we can use this data in the breakdown for the service member RETURN QUERY SELECT total_incentive, price_islh, price_ihpk, price_ihupk, price_fsc; END; $$ LANGUAGE plpgsql; + + +-- db proc that will calculate a PPM's SIT cost +-- returns a table with total cost and the cost of each first day/add'l day SIT service item +CREATE OR REPLACE FUNCTION calculate_ppm_sit_cost( + ppm_id UUID, + address_id UUID, + is_origin BOOLEAN, + move_date DATE, + weight INT, + sit_days INT +) RETURNS TABLE ( + total_cost INT, + price_first_day INT, + price_addl_day INT +) AS +$$ +DECLARE + ppm RECORD; + contract_id UUID; + sit_rate_area_id UUID; + service_id UUID; +BEGIN + -- Validate SIT days + IF sit_days IS NULL OR sit_days < 0 THEN + RAISE EXCEPTION 'SIT days must be a positive integer. Provided value: %', sit_days; + END IF; + + -- Validate PPM existence + SELECT ppms.id INTO ppm FROM ppm_shipments ppms WHERE ppms.id = ppm_id; + IF ppm IS NULL THEN + RAISE EXCEPTION 'PPM with ID % not found', ppm_id; + END IF; + + -- Get contract ID + contract_id := get_contract_id(move_date); + IF contract_id IS NULL THEN + RAISE EXCEPTION 'Contract not found for date: %', move_date; + END IF; + + -- Get rate area + sit_rate_area_id := get_rate_area_id(address_id, NULL, contract_id); + IF sit_rate_area_id IS NULL THEN + RAISE EXCEPTION 'Rate area is NULL for address ID % and contract ID %', address_id, contract_id; + END IF; + + -- Calculate first day SIT cost + service_id := get_service_id(CASE WHEN is_origin THEN 'IOFSIT' ELSE 'IDFSIT' END); + price_first_day := ( + calculate_escalated_price( + CASE WHEN is_origin THEN sit_rate_area_id ELSE NULL END, + CASE WHEN NOT is_origin THEN sit_rate_area_id ELSE NULL END, + service_id, + contract_id, + CASE WHEN is_origin THEN 'IOFSIT' ELSE 'IDFSIT' END, + move_date + ) * (weight / 100)::NUMERIC * 100 + )::INT; + + -- Calculate additional day SIT cost + service_id := get_service_id(CASE WHEN is_origin THEN 'IOASIT' ELSE 'IDASIT' END); + price_addl_day := ( + calculate_escalated_price( + CASE WHEN is_origin THEN sit_rate_area_id ELSE NULL END, + CASE WHEN NOT is_origin THEN sit_rate_area_id ELSE NULL END, + service_id, + contract_id, + CASE WHEN is_origin THEN 'IOASIT' ELSE 'IDASIT' END, + move_date + ) * (weight / 100)::NUMERIC * 100 * sit_days + )::INT; + + -- Calculate total SIT cost + total_cost := price_first_day + price_addl_day; + + -- Return the breakdown for SIT costs + RETURN QUERY SELECT total_cost, price_first_day, price_addl_day; +END; +$$ LANGUAGE plpgsql; + diff --git a/pkg/gen/ghcapi/embedded_spec.go b/pkg/gen/ghcapi/embedded_spec.go index e0271647ece..2ba350e437e 100644 --- a/pkg/gen/ghcapi/embedded_spec.go +++ b/pkg/gen/ghcapi/embedded_spec.go @@ -11332,6 +11332,27 @@ func init() { "readOnly": true, "example": "1f2270c7-7166-40ae-981e-b200ebdf3054" }, + "intlLinehaulPrice": { + "description": "The full price of international shipping and linehaul (ISLH)", + "type": "integer", + "format": "cents", + "x-nullable": true, + "x-omitempty": false + }, + "intlPackPrice": { + "description": "The full price of international packing (IHPK)", + "type": "integer", + "format": "cents", + "x-nullable": true, + "x-omitempty": false + }, + "intlUnpackPrice": { + "description": "The full price of international unpacking (IHUPK)", + "type": "integer", + "format": "cents", + "x-nullable": true, + "x-omitempty": false + }, "miles": { "description": "The distance between the old address and the new address in miles.", "type": "integer", @@ -28257,6 +28278,27 @@ func init() { "readOnly": true, "example": "1f2270c7-7166-40ae-981e-b200ebdf3054" }, + "intlLinehaulPrice": { + "description": "The full price of international shipping and linehaul (ISLH)", + "type": "integer", + "format": "cents", + "x-nullable": true, + "x-omitempty": false + }, + "intlPackPrice": { + "description": "The full price of international packing (IHPK)", + "type": "integer", + "format": "cents", + "x-nullable": true, + "x-omitempty": false + }, + "intlUnpackPrice": { + "description": "The full price of international unpacking (IHUPK)", + "type": "integer", + "format": "cents", + "x-nullable": true, + "x-omitempty": false + }, "miles": { "description": "The distance between the old address and the new address in miles.", "type": "integer", diff --git a/pkg/gen/ghcmessages/p_p_m_closeout.go b/pkg/gen/ghcmessages/p_p_m_closeout.go index b0f423ba61a..a84e0e4c2e0 100644 --- a/pkg/gen/ghcmessages/p_p_m_closeout.go +++ b/pkg/gen/ghcmessages/p_p_m_closeout.go @@ -69,6 +69,15 @@ type PPMCloseout struct { // Format: uuid ID strfmt.UUID `json:"id"` + // The full price of international shipping and linehaul (ISLH) + IntlLinehaulPrice *int64 `json:"intlLinehaulPrice"` + + // The full price of international packing (IHPK) + IntlPackPrice *int64 `json:"intlPackPrice"` + + // The full price of international unpacking (IHUPK) + IntlUnpackPrice *int64 `json:"intlUnpackPrice"` + // The distance between the old address and the new address in miles. // Example: 54 // Minimum: 0 diff --git a/pkg/handlers/ghcapi/internal/payloads/model_to_payload.go b/pkg/handlers/ghcapi/internal/payloads/model_to_payload.go index 89a6290a7ff..87a7517ded5 100644 --- a/pkg/handlers/ghcapi/internal/payloads/model_to_payload.go +++ b/pkg/handlers/ghcapi/internal/payloads/model_to_payload.go @@ -1313,6 +1313,9 @@ func PPMCloseout(ppmCloseout *models.PPMCloseout) *ghcmessages.PPMCloseout { Ddp: handlers.FmtCost(ppmCloseout.DDP), PackPrice: handlers.FmtCost(ppmCloseout.PackPrice), UnpackPrice: handlers.FmtCost(ppmCloseout.UnpackPrice), + IntlPackPrice: handlers.FmtCost((ppmCloseout.IntlPackPrice)), + IntlUnpackPrice: handlers.FmtCost((ppmCloseout.IntlUnpackPrice)), + IntlLinehaulPrice: handlers.FmtCost((ppmCloseout.IntlLinehaulPrice)), SITReimbursement: handlers.FmtCost(ppmCloseout.SITReimbursement), } diff --git a/pkg/models/ppm_shipment.go b/pkg/models/ppm_shipment.go index 7989b973119..4506451053a 100644 --- a/pkg/models/ppm_shipment.go +++ b/pkg/models/ppm_shipment.go @@ -40,10 +40,9 @@ type PPMCloseout struct { DDP *unit.Cents PackPrice *unit.Cents UnpackPrice *unit.Cents - IHPKPrice *unit.Cents - IHUPKPrice *unit.Cents - ISLHPrice *unit.Cents - FSCPrice *unit.Cents + IntlPackPrice *unit.Cents + IntlUnpackPrice *unit.Cents + IntlLinehaulPrice *unit.Cents SITReimbursement *unit.Cents } @@ -333,12 +332,11 @@ type PPMIncentive struct { PriceFSC int `db:"price_fsc"` } -// a db stored proc that will handle updating the estimated_incentive value +// a db function that will handle updating the estimated_incentive value // this simulates pricing of a basic iHHG shipment with ISLH, IHPK, IHUPK, and the CONUS portion for a FSC func CalculatePPMIncentive(db *pop.Connection, ppmID uuid.UUID, pickupAddressID uuid.UUID, destAddressID uuid.UUID, moveDate time.Time, mileage int, weight int, isEstimated bool, isActual bool, isMax bool) (*PPMIncentive, error) { var incentive PPMIncentive - // Run the stored procedure and scan the results into the struct err := db.RawQuery("SELECT * FROM calculate_ppm_incentive($1, $2, $3, $4, $5, $6, $7, $8, $9)", ppmID, pickupAddressID, destAddressID, moveDate, mileage, weight, isEstimated, isActual, isMax). First(&incentive) if err != nil { @@ -347,3 +345,22 @@ func CalculatePPMIncentive(db *pop.Connection, ppmID uuid.UUID, pickupAddressID return &incentive, nil } + +type PPMSITCosts struct { + TotalSITCost int `db:"total_cost"` + PriceFirstDaySIT int `db:"price_first_day"` + PriceAddlDaySIT int `db:"price_addl_day"` +} + +// a db function that will handle calculating and returning the SIT costs related to a PPM shipment +func CalculatePPMSITCost(db *pop.Connection, ppmID uuid.UUID, addressID uuid.UUID, isOrigin bool, moveDate time.Time, weight int, sitDays int) (*PPMSITCosts, error) { + var costs PPMSITCosts + + err := db.RawQuery("SELECT * FROM calculate_ppm_SIT_cost($1, $2, $3, $4, $5, $6)", ppmID, addressID, isOrigin, moveDate, weight, sitDays). + First(&costs) + if err != nil { + return nil, fmt.Errorf("error calculating PPM SIT costs for PPM ID %s: %w", ppmID, err) + } + + return &costs, nil +} diff --git a/pkg/models/re_service_item.go b/pkg/models/re_service_item.go index f06ee0990a2..298c8dfa26a 100644 --- a/pkg/models/re_service_item.go +++ b/pkg/models/re_service_item.go @@ -3,7 +3,10 @@ package models import ( "time" + "github.com/gobuffalo/pop/v6" "github.com/gofrs/uuid" + + "github.com/transcom/mymove/pkg/apperror" ) type ReServiceItem struct { @@ -24,3 +27,12 @@ func (r ReServiceItem) TableName() string { // ReServiceItems is a slice of ReServiceItem type ReServiceItems []ReServiceItem + +func FetchReServiceByCode(db *pop.Connection, code ReServiceCode) (*ReService, error) { + reService := ReService{} + err := db.Where("code = ?", code).First(&reService) + if err != nil { + return nil, apperror.NewQueryError("ReService", err, "") + } + return &reService, err +} diff --git a/pkg/payment_request/service_param_value_lookups/distance_zip_lookup.go b/pkg/payment_request/service_param_value_lookups/distance_zip_lookup.go index d2e08c221f0..2a6e1b70a97 100644 --- a/pkg/payment_request/service_param_value_lookups/distance_zip_lookup.go +++ b/pkg/payment_request/service_param_value_lookups/distance_zip_lookup.go @@ -48,6 +48,8 @@ func (r DistanceZipLookup) lookup(appCtx appcontext.AppContext, keyData *Service "Distance", "PickupAddress", "DestinationAddress", + "PPMShipment.PickupAddress", + "PPMShipment.DestinationAddress", ).Find(&mtoShipment, mtoShipment.ID) if err != nil { return "", err @@ -59,20 +61,47 @@ func (r DistanceZipLookup) lookup(appCtx appcontext.AppContext, keyData *Service // if the shipment is international, we need to change the respective ZIP to use the port ZIP and not the address ZIP if mtoShipment.MarketCode == models.MarketCodeInternational { - portZip, portType, err := models.GetPortLocationInfoForShipment(appCtx.DB(), *mtoShipmentID) - if err != nil { - return "", err - } - if portZip != nil && portType != nil { - // if the port type is POEFSC this means the shipment is CONUS -> OCONUS (pickup -> port) - // if the port type is PODFSC this means the shipment is OCONUS -> CONUS (port -> destination) - if *portType == models.ReServiceCodePOEFSC.String() { - destinationZip = *portZip - } else if *portType == models.ReServiceCodePODFSC.String() { - pickupZip = *portZip + if mtoShipment.ShipmentType != models.MTOShipmentTypePPM { + portZip, portType, err := models.GetPortLocationInfoForShipment(appCtx.DB(), *mtoShipmentID) + if err != nil { + return "", err + } + if portZip != nil && portType != nil { + // if the port type is POEFSC this means the shipment is CONUS -> OCONUS (pickup -> port) + // if the port type is PODFSC this means the shipment is OCONUS -> CONUS (port -> destination) + if *portType == models.ReServiceCodePOEFSC.String() { + destinationZip = *portZip + } else if *portType == models.ReServiceCodePODFSC.String() { + pickupZip = *portZip + } + } else { + return "", apperror.NewNotFoundError(*mtoShipmentID, "looking for port ZIP for shipment") } } else { - return "", apperror.NewNotFoundError(*mtoShipmentID, "looking for port ZIP for shipment") + // PPMs get reimbursed for their travel from CONUS <-> Port ZIPs, but only for the Tacoma Port + portLocation, err := models.FetchPortLocationByCode(appCtx.DB(), "3002") // Tacoma port code + if err != nil { + return "", fmt.Errorf("unable to find port zip with code %s", "3002") + } + if mtoShipment.PPMShipment != nil && mtoShipment.PPMShipment.PickupAddress != nil && mtoShipment.PPMShipment.DestinationAddress != nil { + // need to figure out if we are going to go Port -> CONUS or CONUS -> Port + pickupOconus := *mtoShipment.PPMShipment.PickupAddress.IsOconus + destOconus := *mtoShipment.PPMShipment.DestinationAddress.IsOconus + if pickupOconus && !destOconus { + // Port ZIP -> CONUS ZIP + pickupZip = portLocation.UsPostRegionCity.UsprZipID + destinationZip = mtoShipment.PPMShipment.DestinationAddress.PostalCode + } else if !pickupOconus && destOconus { + // CONUS ZIP -> Port ZIP + pickupZip = mtoShipment.PPMShipment.PickupAddress.PostalCode + destinationZip = portLocation.UsPostRegionCity.UsprZipID + } else { + // OCONUS -> OCONUS mileage they don't get reimbursed for + return strconv.Itoa(0), nil + } + } else { + return "", fmt.Errorf("missing required PPM & address information for shipment with id %s", mtoShipmentID) + } } } errorMsgForPickupZip := fmt.Sprintf("Shipment must have valid pickup zipcode. Received: %s", pickupZip) 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..847093df3e2 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 @@ -2,6 +2,9 @@ package serviceparamvaluelookups import ( "fmt" + "time" + + "github.com/gofrs/uuid" "github.com/transcom/mymove/pkg/appcontext" "github.com/transcom/mymove/pkg/models" @@ -16,19 +19,67 @@ type PerUnitCentsLookup struct { func (p PerUnitCentsLookup) lookup(appCtx appcontext.AppContext, s *ServiceItemParamKeyData) (string, error) { serviceID := p.ServiceItem.ReServiceID + if serviceID == uuid.Nil { + reService, err := models.FetchReServiceByCode(appCtx.DB(), p.ServiceItem.ReService.Code) + if err != nil { + return "", fmt.Errorf("error fetching ReService Code %s: %w", p.ServiceItem.ReService.Code, err) + } + serviceID = reService.ID + } contractID := s.ContractID - if p.MTOShipment.RequestedPickupDate == nil { - return "", fmt.Errorf("requested pickup date is required for shipment with id: %s", p.MTOShipment.ID) + var shipmentID uuid.UUID + var pickupAddressID uuid.UUID + var destinationAddressID uuid.UUID + var moveDate time.Time + // HHG shipment + if p.MTOShipment.ShipmentType != models.MTOShipmentTypePPM { + shipmentID = p.MTOShipment.ID + if p.MTOShipment.RequestedPickupDate != nil { + moveDate = *p.MTOShipment.RequestedPickupDate + } else { + return "", fmt.Errorf("requested pickup date is required for shipment with id: %s", shipmentID) + } + if p.MTOShipment.PickupAddressID != nil { + pickupAddressID = *p.MTOShipment.PickupAddressID + } else { + return "", fmt.Errorf("pickup address is required for shipment with id: %s", shipmentID) + } + if p.MTOShipment.DestinationAddressID != nil { + destinationAddressID = *p.MTOShipment.DestinationAddressID + } else { + return "", fmt.Errorf("destination address is required for shipment with id: %s", shipmentID) + } + } else { // PPM shipment + shipmentID = p.MTOShipment.PPMShipment.ID + if p.MTOShipment.ActualPickupDate != nil { + moveDate = *p.MTOShipment.ActualPickupDate + } else if p.MTOShipment.RequestedPickupDate != nil { + moveDate = *p.MTOShipment.RequestedPickupDate + } else { + return "", fmt.Errorf("actual move date is required for PPM shipment with id: %s", shipmentID) + } + + if p.MTOShipment.PPMShipment.PickupAddressID != nil { + pickupAddressID = *p.MTOShipment.PPMShipment.PickupAddressID + } else { + return "", fmt.Errorf("pickup address is required for PPM shipment with id: %s", shipmentID) + } + + if p.MTOShipment.PPMShipment.DestinationAddressID != nil { + destinationAddressID = *p.MTOShipment.PPMShipment.DestinationAddressID + } else { + return "", fmt.Errorf("destination address is required for PPM shipment with id: %s", shipmentID) + } } 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(), 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) + return "", fmt.Errorf("error fetching rate area id for shipment ID: %s and service ID %s: %s", shipmentID, serviceID, err) } - isPeakPeriod := ghcrateengine.IsPeakPeriod(*p.MTOShipment.RequestedPickupDate) + isPeakPeriod := ghcrateengine.IsPeakPeriod(moveDate) var reIntlOtherPrice models.ReIntlOtherPrice err = appCtx.DB().Q(). Where("contract_id = ?", contractID). @@ -43,11 +94,11 @@ 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(), destinationAddressID, 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) + return "", fmt.Errorf("error fetching rate area id for shipment ID: %s and service ID %s: %s", shipmentID, serviceID, err) } - isPeakPeriod := ghcrateengine.IsPeakPeriod(*p.MTOShipment.RequestedPickupDate) + isPeakPeriod := ghcrateengine.IsPeakPeriod(moveDate) var reIntlOtherPrice models.ReIntlOtherPrice err = appCtx.DB().Q(). Where("contract_id = ?", contractID). @@ -62,15 +113,15 @@ 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(), 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) + return "", fmt.Errorf("error fetching rate area id for origin address for shipment ID: %s and service ID %s: %s", shipmentID, serviceID, err) } - destRateAreaID, err := models.FetchRateAreaID(appCtx.DB(), *p.MTOShipment.DestinationAddressID, serviceID, contractID) + destRateAreaID, err := models.FetchRateAreaID(appCtx.DB(), 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) + return "", fmt.Errorf("error fetching rate area id for destination address for shipment ID: %s and service ID %s: %s", shipmentID, serviceID, err) } - isPeakPeriod := ghcrateengine.IsPeakPeriod(*p.MTOShipment.RequestedPickupDate) + isPeakPeriod := ghcrateengine.IsPeakPeriod(moveDate) var reIntlPrice models.ReIntlPrice err = appCtx.DB().Q(). Where("contract_id = ?", contractID). @@ -84,6 +135,82 @@ func (p PerUnitCentsLookup) lookup(appCtx appcontext.AppContext, s *ServiceItemP } return reIntlPrice.PerUnitCents.ToMillicents().ToCents().String(), nil + case models.ReServiceCodeIOFSIT: + // IOFSIT: Need rate area ID for origin + originRateAreaID, err := models.FetchRateAreaID(appCtx.DB(), 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", shipmentID, serviceID, err) + } + isPeakPeriod := ghcrateengine.IsPeakPeriod(moveDate) + var reIntlOtherPrice models.ReIntlOtherPrice + err = appCtx.DB().Q(). + Where("contract_id = ?", contractID). + Where("service_id = ?", serviceID). + Where("is_peak_period = ?", isPeakPeriod). + Where("origin_rate_area_id = ?", originRateAreaID). + First(&reIntlOtherPrice) + if err != nil { + return "", fmt.Errorf("error fetching IOFSIT per unit cents for contractID: %s, serviceID %s, isPeakPeriod: %t, originRateAreaID: %s: %s", contractID, serviceID, isPeakPeriod, originRateAreaID, err) + } + return reIntlOtherPrice.PerUnitCents.ToMillicents().ToCents().String(), nil + + case models.ReServiceCodeIOASIT: + // IOASIT: Need rate area ID for origin + originRateAreaID, err := models.FetchRateAreaID(appCtx.DB(), pickupAddressID, serviceID, contractID) + if err != nil { + return "", fmt.Errorf("error fetching rate area id for origin address for shipment ID: %s, service ID %s: %s", shipmentID, serviceID, err) + } + isPeakPeriod := ghcrateengine.IsPeakPeriod(moveDate) + var reIntlOtherPrice models.ReIntlOtherPrice + err = appCtx.DB().Q(). + Where("contract_id = ?", contractID). + Where("service_id = ?", serviceID). + Where("is_peak_period = ?", isPeakPeriod). + Where("origin_rate_area_id = ?", originRateAreaID). + First(&reIntlOtherPrice) + if err != nil { + return "", fmt.Errorf("error fetching IOASIT per unit cents for contractID: %s, serviceID %s, isPeakPeriod: %t, originRateAreaID: %s: %s", contractID, serviceID, isPeakPeriod, originRateAreaID, err) + } + return reIntlOtherPrice.PerUnitCents.ToMillicents().ToCents().String(), nil + + case models.ReServiceCodeIDFSIT: + // IDFSIT: Need rate area ID for destination + destRateAreaID, err := models.FetchRateAreaID(appCtx.DB(), destinationAddressID, serviceID, contractID) + if err != nil { + return "", fmt.Errorf("error fetching rate area id for destination address for shipment ID: %s, service ID %s: %s", shipmentID, serviceID, err) + } + isPeakPeriod := ghcrateengine.IsPeakPeriod(moveDate) + var reIntlOtherPrice models.ReIntlOtherPrice + err = appCtx.DB().Q(). + Where("contract_id = ?", contractID). + Where("service_id = ?", serviceID). + Where("is_peak_period = ?", isPeakPeriod). + Where("destination_rate_area_id = ?", destRateAreaID). + First(&reIntlOtherPrice) + if err != nil { + return "", fmt.Errorf("error fetching IDFSIT per unit cents for contractID: %s, serviceID %s, isPeakPeriod: %t, destRateAreaID: %s: %s", contractID, serviceID, isPeakPeriod, destRateAreaID, err) + } + return reIntlOtherPrice.PerUnitCents.ToMillicents().ToCents().String(), nil + + case models.ReServiceCodeIDASIT: + // IDASIT: Need rate area ID for destination + destRateAreaID, err := models.FetchRateAreaID(appCtx.DB(), 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", shipmentID, serviceID, err) + } + isPeakPeriod := ghcrateengine.IsPeakPeriod(moveDate) + var reIntlOtherPrice models.ReIntlOtherPrice + err = appCtx.DB().Q(). + Where("contract_id = ?", contractID). + Where("service_id = ?", serviceID). + Where("is_peak_period = ?", isPeakPeriod). + Where("destination_rate_area_id = ?", destRateAreaID). + First(&reIntlOtherPrice) + if err != nil { + return "", fmt.Errorf("error fetching IDASIT per unit cents for contractID: %s, serviceID %s, isPeakPeriod: %t, destRateAreaID: %s: %s", contractID, serviceID, isPeakPeriod, destRateAreaID, err) + } + return reIntlOtherPrice.PerUnitCents.ToMillicents().ToCents().String(), nil + default: return "", fmt.Errorf("unsupported service code to retrieve service item param PerUnitCents") } diff --git a/pkg/payment_request/service_param_value_lookups/port_zip_lookup.go b/pkg/payment_request/service_param_value_lookups/port_zip_lookup.go index 3ea8be94315..bf4971f9db2 100644 --- a/pkg/payment_request/service_param_value_lookups/port_zip_lookup.go +++ b/pkg/payment_request/service_param_value_lookups/port_zip_lookup.go @@ -15,13 +15,25 @@ type PortZipLookup struct { ServiceItem models.MTOServiceItem } -func (p PortZipLookup) lookup(appCtx appcontext.AppContext, _ *ServiceItemParamKeyData) (string, error) { +func (p PortZipLookup) lookup(appCtx appcontext.AppContext, keyData *ServiceItemParamKeyData) (string, error) { var portLocationID *uuid.UUID if p.ServiceItem.PODLocationID != nil { portLocationID = p.ServiceItem.PODLocationID } else if p.ServiceItem.POELocationID != nil { portLocationID = p.ServiceItem.POELocationID } else { + // for PPMs we need to send back the ZIP for the Tacoma Port, they are reimbursed for their CONUS <-> Port travel + shipment, err := models.FetchShipmentByID(appCtx.DB(), *keyData.mtoShipmentID) + if err != nil { + return "", fmt.Errorf("unable to find shipment with id %s", keyData.mtoShipmentID) + } + if shipment.ShipmentType == models.MTOShipmentTypePPM && shipment.MarketCode == models.MarketCodeInternational { + portLocation, err := models.FetchPortLocationByCode(appCtx.DB(), "3002") + if err != nil { + return "", fmt.Errorf("unable to find port zip with code %s", "3002") + } + return portLocation.UsPostRegionCity.UsprZipID, nil + } return "", fmt.Errorf("unable to find port zip for service item id: %s", p.ServiceItem.ID) } var portLocation models.PortLocation diff --git a/pkg/payment_request/service_param_value_lookups/service_param_value_lookups.go b/pkg/payment_request/service_param_value_lookups/service_param_value_lookups.go index 6c5fc73c42a..545d95ad5ce 100644 --- a/pkg/payment_request/service_param_value_lookups/service_param_value_lookups.go +++ b/pkg/payment_request/service_param_value_lookups/service_param_value_lookups.go @@ -27,7 +27,7 @@ type ServiceItemParamKeyData struct { paramCache *ServiceParamsCache } -func NewServiceItemParamKeyData(planner route.Planner, lookups map[models.ServiceItemParamName]ServiceItemParamKeyLookup, mtoServiceItem models.MTOServiceItem, mtoShipment models.MTOShipment, contractCode string) ServiceItemParamKeyData { +func NewServiceItemParamKeyData(planner route.Planner, lookups map[models.ServiceItemParamName]ServiceItemParamKeyLookup, mtoServiceItem models.MTOServiceItem, mtoShipment models.MTOShipment, contractCode string, contractID uuid.UUID) ServiceItemParamKeyData { return ServiceItemParamKeyData{ planner: planner, lookups: lookups, @@ -36,6 +36,7 @@ func NewServiceItemParamKeyData(planner route.Planner, lookups map[models.Servic mtoShipmentID: &mtoShipment.ID, MoveTaskOrderID: mtoShipment.MoveTaskOrderID, ContractCode: contractCode, + ContractID: contractID, } } @@ -211,8 +212,11 @@ func ServiceParamLookupInitialize( mtoShipment.DestinationAddress = &destinationAddress switch mtoServiceItem.ReService.Code { - case models.ReServiceCodeDDASIT, models.ReServiceCodeDDDSIT, models.ReServiceCodeDDFSIT, models.ReServiceCodeDDSFSC, models.ReServiceCodeDOASIT, models.ReServiceCodeDOPSIT, models.ReServiceCodeDOFSIT, models.ReServiceCodeDOSFSC: - err := appCtx.DB().Load(&mtoShipment, "SITDurationUpdates") + case models.ReServiceCodeDDASIT, models.ReServiceCodeDDDSIT, models.ReServiceCodeDDFSIT, + models.ReServiceCodeDDSFSC, models.ReServiceCodeDOASIT, models.ReServiceCodeDOPSIT, + models.ReServiceCodeDOFSIT, models.ReServiceCodeDOSFSC, models.ReServiceCodeIOFSIT, + models.ReServiceCodeIOASIT, models.ReServiceCodeIDFSIT, models.ReServiceCodeIDASIT: + err := appCtx.DB().Load(&mtoShipment, "SITDurationUpdates", "PPMShipment.PickupAddress", "PPMShipment.DestinationAddress") if err != nil { return nil, err } diff --git a/pkg/payment_request/service_param_value_lookups/service_param_value_lookups_test.go b/pkg/payment_request/service_param_value_lookups/service_param_value_lookups_test.go index 48ba8bb1c7b..fb5c9b78efa 100644 --- a/pkg/payment_request/service_param_value_lookups/service_param_value_lookups_test.go +++ b/pkg/payment_request/service_param_value_lookups/service_param_value_lookups_test.go @@ -174,7 +174,7 @@ func (suite *ServiceParamValueLookupsSuite) setupTestMTOServiceItemWithEstimated // i don't think this function gets called for PPMs, but need to verify //paramLookup, err := ServiceParamLookupInitialize(suite.AppContextForTest(), suite.planner, mtoServiceItem, paymentRequest.ID, paymentRequest.MoveTaskOrderID, nil) //suite.FatalNoError(err) - paramLookup := NewServiceItemParamKeyData(suite.planner, serviceItemLookups, mtoServiceItem, mtoShipment, testdatagen.DefaultContractCode) + paramLookup := NewServiceItemParamKeyData(suite.planner, serviceItemLookups, mtoServiceItem, mtoShipment, testdatagen.DefaultContractCode, uuid.Nil) return mtoServiceItem, paymentRequest, ¶mLookup } diff --git a/pkg/services/ppm_closeout/ppm_closeout.go b/pkg/services/ppm_closeout/ppm_closeout.go index 9deab808a96..55707b91190 100644 --- a/pkg/services/ppm_closeout/ppm_closeout.go +++ b/pkg/services/ppm_closeout/ppm_closeout.go @@ -36,6 +36,9 @@ type serviceItemPrices struct { haulPrice *unit.Cents haulFSC *unit.Cents haulType models.HaulType + intlPackPrice *unit.Cents + intlUnpackPrice *unit.Cents + intlLinehaulPrice *unit.Cents } func NewPPMCloseoutFetcher(planner route.Planner, paymentRequestHelper paymentrequesthelper.Helper, estimator services.PPMEstimator) services.PPMCloseoutFetcher { @@ -119,6 +122,9 @@ func (p *ppmCloseoutFetcher) GetPPMCloseout(appCtx appcontext.AppContext, ppmShi ppmCloseoutObj.DDP = serviceItems.ddp ppmCloseoutObj.PackPrice = serviceItems.packPrice ppmCloseoutObj.UnpackPrice = serviceItems.unpackPrice + ppmCloseoutObj.IntlLinehaulPrice = serviceItems.intlLinehaulPrice + ppmCloseoutObj.IntlUnpackPrice = serviceItems.intlUnpackPrice + ppmCloseoutObj.IntlPackPrice = serviceItems.intlPackPrice ppmCloseoutObj.SITReimbursement = serviceItems.storageReimbursementCosts return &ppmCloseoutObj, nil @@ -317,12 +323,13 @@ func (p *ppmCloseoutFetcher) getServiceItemPrices(appCtx appcontext.AppContext, return serviceItemPrices{}, err } - serviceItemsToPrice = ppmshipment.BaseServiceItems(ppmShipment.ShipmentID) + isInternationalShipment := ppmShipment.Shipment.MarketCode == models.MarketCodeInternational + serviceItemsToPrice = ppmshipment.BaseServiceItems(ppmShipment) - // Change DLH to DSH if move within same Zip3 actualPickupPostal := *ppmShipment.ActualPickupPostalCode actualDestPostal := *ppmShipment.ActualDestinationPostalCode - if actualPickupPostal[0:3] == actualDestPostal[0:3] { + // Change DLH to DSH if move within same Zip3 + if !isInternationalShipment && actualPickupPostal[0:3] == actualDestPostal[0:3] { serviceItemsToPrice[0] = models.MTOServiceItem{ReService: models.ReService{Code: models.ReServiceCodeDSH}, MTOShipmentID: &ppmShipment.ShipmentID} } contractDate := ppmShipment.ExpectedDepartureDate @@ -335,7 +342,7 @@ func (p *ppmCloseoutFetcher) getServiceItemPrices(appCtx appcontext.AppContext, if paramErr != nil { return serviceItemPrices{}, paramErr } - var totalPrice, packPrice, unpackPrice, destinationPrice, originPrice, haulPrice, haulFSC unit.Cents + var totalPrice, packPrice, unpackPrice, destinationPrice, originPrice, haulPrice, haulFSC, intlPackPrice, intlUnpackPrice, intlLinehaulPrice unit.Cents var totalWeight unit.Pound var ppmToMtoShipment models.MTOShipment @@ -374,13 +381,16 @@ func (p *ppmCloseoutFetcher) getServiceItemPrices(appCtx appcontext.AppContext, } validCodes := map[models.ReServiceCode]string{ - models.ReServiceCodeDPK: "DPK", - models.ReServiceCodeDUPK: "DUPK", - models.ReServiceCodeDOP: "DOP", - models.ReServiceCodeDDP: "DDP", - models.ReServiceCodeDSH: "DSH", - models.ReServiceCodeDLH: "DLH", - models.ReServiceCodeFSC: "FSC", + models.ReServiceCodeDPK: "DPK", + models.ReServiceCodeDUPK: "DUPK", + models.ReServiceCodeDOP: "DOP", + models.ReServiceCodeDDP: "DDP", + models.ReServiceCodeDSH: "DSH", + models.ReServiceCodeDLH: "DLH", + models.ReServiceCodeFSC: "FSC", + models.ReServiceCodeISLH: "ISLH", + models.ReServiceCodeIHPK: "IHPK", + models.ReServiceCodeIHUPK: "IHUPK", } // If service item is of a type we need for a specific calculation, get its price @@ -402,11 +412,11 @@ func (p *ppmCloseoutFetcher) getServiceItemPrices(appCtx appcontext.AppContext, serviceItemLookups := serviceparamvaluelookups.InitializeLookups(appCtx, ppmToMtoShipment, serviceItem) // This is the struct that gets passed to every param lookup() method that was initialized above - keyData := serviceparamvaluelookups.NewServiceItemParamKeyData(p.planner, serviceItemLookups, serviceItem, ppmToMtoShipment, contract.Code) + keyData := serviceparamvaluelookups.NewServiceItemParamKeyData(p.planner, serviceItemLookups, serviceItem, ppmToMtoShipment, contract.Code, contract.ID) // The distance value gets saved to the mto shipment model to reduce repeated api calls. var shipmentWithDistance models.MTOShipment - err = appCtx.DB().Find(&shipmentWithDistance, ppmShipment.Shipment.ID) + err = appCtx.DB().Eager("PPMShipment").Find(&shipmentWithDistance, ppmShipment.Shipment.ID) if err != nil { logger.Error("could not find shipment in the database") return serviceItemPrices{}, err @@ -419,7 +429,7 @@ func (p *ppmCloseoutFetcher) getServiceItemPrices(appCtx appcontext.AppContext, for _, param := range paramsForServiceCode(serviceItem.ReService.Code, paramsForServiceItems) { paramKey := param.ServiceItemParamKey // This is where the lookup() method of each service item param is actually evaluated - paramValue, serviceParamErr := keyData.ServiceParamValue(appCtx, paramKey.Key) // Fails with "DistanceZip" param? + paramValue, serviceParamErr := keyData.ServiceParamValue(appCtx, paramKey.Key) if serviceParamErr != nil { logger.Error("could not calculate param value lookup", zap.Error(serviceParamErr)) return serviceItemPrices{}, serviceParamErr @@ -452,6 +462,12 @@ func (p *ppmCloseoutFetcher) getServiceItemPrices(appCtx appcontext.AppContext, totalPrice = totalPrice.AddCents(centsValue) switch serviceItem.ReService.Code { + case models.ReServiceCodeIHPK: + intlPackPrice += centsValue + case models.ReServiceCodeIHUPK: + intlUnpackPrice += centsValue + case models.ReServiceCodeISLH: + intlLinehaulPrice += centsValue case models.ReServiceCodeDPK: packPrice += centsValue case models.ReServiceCodeDUPK: @@ -488,6 +504,9 @@ func (p *ppmCloseoutFetcher) getServiceItemPrices(appCtx appcontext.AppContext, returnPriceObj.storageReimbursementCosts = &sitCosts returnPriceObj.haulPrice = &haulPrice returnPriceObj.haulFSC = &haulFSC + returnPriceObj.intlLinehaulPrice = &intlLinehaulPrice + returnPriceObj.intlPackPrice = &intlPackPrice + returnPriceObj.intlUnpackPrice = &intlUnpackPrice return returnPriceObj, nil } diff --git a/pkg/services/ppmshipment/ppm_estimator.go b/pkg/services/ppmshipment/ppm_estimator.go index 81c76f99734..04378ce97dc 100644 --- a/pkg/services/ppmshipment/ppm_estimator.go +++ b/pkg/services/ppmshipment/ppm_estimator.go @@ -209,23 +209,25 @@ func (f *estimatePPM) estimateIncentive(appCtx appcontext.AppContext, oldPPMShip return nil, nil, err } - // if the PPM is international, we will use a db func - if newPPMShipment.Shipment.MarketCode != models.MarketCodeInternational { + calculateSITEstimate := shouldCalculateSITCost(newPPMShipment, &oldPPMShipment) - calculateSITEstimate := shouldCalculateSITCost(newPPMShipment, &oldPPMShipment) + // Clear out any previously calculated SIT estimated costs, if SIT is no longer expected + if newPPMShipment.SITExpected != nil && !*newPPMShipment.SITExpected { + newPPMShipment.SITEstimatedCost = nil + } - // Clear out any previously calculated SIT estimated costs, if SIT is no longer expected - if newPPMShipment.SITExpected != nil && !*newPPMShipment.SITExpected { - newPPMShipment.SITEstimatedCost = nil - } + skipCalculatingEstimatedIncentive := shouldSkipEstimatingIncentive(newPPMShipment, &oldPPMShipment) - skipCalculatingEstimatedIncentive := shouldSkipEstimatingIncentive(newPPMShipment, &oldPPMShipment) + if skipCalculatingEstimatedIncentive && !calculateSITEstimate { + return oldPPMShipment.EstimatedIncentive, newPPMShipment.SITEstimatedCost, nil + } - if skipCalculatingEstimatedIncentive && !calculateSITEstimate { - return oldPPMShipment.EstimatedIncentive, newPPMShipment.SITEstimatedCost, nil - } + estimatedIncentive := oldPPMShipment.EstimatedIncentive + estimatedSITCost := oldPPMShipment.SITEstimatedCost + + // if the PPM is international, we will use a db func + if newPPMShipment.Shipment.MarketCode != models.MarketCodeInternational { - estimatedIncentive := oldPPMShipment.EstimatedIncentive if !skipCalculatingEstimatedIncentive { // Clear out advance and advance requested fields when the estimated incentive is reset. newPPMShipment.HasRequestedAdvance = nil @@ -237,7 +239,6 @@ func (f *estimatePPM) estimateIncentive(appCtx appcontext.AppContext, oldPPMShip } } - estimatedSITCost := oldPPMShipment.SITEstimatedCost if calculateSITEstimate { estimatedSITCost, err = CalculateSITCost(appCtx, newPPMShipment, contract) if err != nil { @@ -251,12 +252,35 @@ func (f *estimatePPM) estimateIncentive(appCtx appcontext.AppContext, oldPPMShip pickupAddress := newPPMShipment.PickupAddress destinationAddress := newPPMShipment.DestinationAddress - estimatedIncentive, err := f.calculateOCONUSIncentive(appCtx, newPPMShipment.ID, *pickupAddress, *destinationAddress, contractDate, newPPMShipment.EstimatedWeight.Int(), false, false, true) - if err != nil { - return nil, nil, fmt.Errorf("failed to calculate estimated PPM incentive: %w", err) + if !skipCalculatingEstimatedIncentive { + // Clear out advance and advance requested fields when the estimated incentive is reset. + newPPMShipment.HasRequestedAdvance = nil + newPPMShipment.AdvanceAmountRequested = nil + + estimatedIncentive, err = f.calculateOCONUSIncentive(appCtx, newPPMShipment.ID, *pickupAddress, *destinationAddress, contractDate, newPPMShipment.EstimatedWeight.Int(), false, false, true) + if err != nil { + return nil, nil, fmt.Errorf("failed to calculate estimated PPM incentive: %w", err) + } } - return estimatedIncentive, nil, nil + if calculateSITEstimate { + var sitAddress models.Address + isOrigin := *newPPMShipment.SITLocation == models.SITLocationTypeOrigin + if isOrigin { + sitAddress = *newPPMShipment.PickupAddress + } else if !isOrigin { + sitAddress = *newPPMShipment.DestinationAddress + } else { + return estimatedIncentive, estimatedSITCost, nil + } + daysInSIT := additionalDaysInSIT(*newPPMShipment.SITEstimatedEntryDate, *newPPMShipment.SITEstimatedDepartureDate) + estimatedSITCost, err = f.calculateOCONUSSITCosts(appCtx, newPPMShipment.ID, sitAddress.ID, isOrigin, contractDate, newPPMShipment.EstimatedWeight.Int(), daysInSIT) + if err != nil { + return nil, nil, fmt.Errorf("failed to calculate estimated PPM incentive: %w", err) + } + } + + return estimatedIncentive, estimatedSITCost, nil } } @@ -419,7 +443,7 @@ func (f estimatePPM) calculatePrice(appCtx appcontext.AppContext, ppmShipment *m logger := appCtx.Logger() zeroTotal := false - serviceItemsToPrice := BaseServiceItems(ppmShipment.ShipmentID) + serviceItemsToPrice := BaseServiceItems(*ppmShipment) var move models.Move err := appCtx.DB().Q().Eager( @@ -509,7 +533,7 @@ func (f estimatePPM) calculatePrice(appCtx appcontext.AppContext, ppmShipment *m serviceItemLookups := serviceparamvaluelookups.InitializeLookups(appCtx, mtoShipment, serviceItem) // This is the struct that gets passed to every param lookup() method that was initialized above - keyData := serviceparamvaluelookups.NewServiceItemParamKeyData(f.planner, serviceItemLookups, serviceItem, mtoShipment, contract.Code) + keyData := serviceparamvaluelookups.NewServiceItemParamKeyData(f.planner, serviceItemLookups, serviceItem, mtoShipment, contract.Code, contract.ID) // The distance value gets saved to the mto shipment model to reduce repeated api calls. var shipmentWithDistance models.MTOShipment @@ -587,7 +611,7 @@ func (f estimatePPM) priceBreakdown(appCtx appcontext.AppContext, ppmShipment *m var unpacking unit.Cents var storage unit.Cents - serviceItemsToPrice := BaseServiceItems(ppmShipment.ShipmentID) + serviceItemsToPrice := BaseServiceItems(*ppmShipment) // Replace linehaul pricer with shorthaul pricer if move is within the same Zip3 var pickupPostal, destPostal string @@ -672,7 +696,7 @@ func (f estimatePPM) priceBreakdown(appCtx appcontext.AppContext, ppmShipment *m serviceItemLookups := serviceparamvaluelookups.InitializeLookups(appCtx, mtoShipment, serviceItem) // This is the struct that gets passed to every param lookup() method that was initialized above - keyData := serviceparamvaluelookups.NewServiceItemParamKeyData(f.planner, serviceItemLookups, serviceItem, mtoShipment, contract.Code) + keyData := serviceparamvaluelookups.NewServiceItemParamKeyData(f.planner, serviceItemLookups, serviceItem, mtoShipment, contract.Code, contract.ID) // The distance value gets saved to the mto shipment model to reduce repeated api calls. var shipmentWithDistance models.MTOShipment @@ -783,12 +807,29 @@ func (f *estimatePPM) calculateOCONUSIncentive(appCtx appcontext.AppContext, ppm return (*unit.Cents)(&incentive.TotalIncentive), nil } +func (f *estimatePPM) calculateOCONUSSITCosts(appCtx appcontext.AppContext, ppmID uuid.UUID, addressID uuid.UUID, isOrigin bool, moveDate time.Time, weight int, sitDays int) (*unit.Cents, error) { + if sitDays <= 0 { + return nil, fmt.Errorf("SIT days must be greater than zero") + } + + if weight <= 0 { + return nil, fmt.Errorf("weight must be greater than zero") + } + + sitCosts, err := models.CalculatePPMSITCost(appCtx.DB(), ppmID, addressID, isOrigin, moveDate, weight, sitDays) + if err != nil { + return nil, fmt.Errorf("failed to calculate SIT costs: %w", err) + } + + return (*unit.Cents)(&sitCosts.TotalSITCost), nil +} + func CalculateSITCost(appCtx appcontext.AppContext, ppmShipment *models.PPMShipment, contract models.ReContract) (*unit.Cents, error) { logger := appCtx.Logger() additionalDaysInSIT := additionalDaysInSIT(*ppmShipment.SITEstimatedEntryDate, *ppmShipment.SITEstimatedDepartureDate) - serviceItemsToPrice := StorageServiceItems(ppmShipment.ShipmentID, *ppmShipment.SITLocation, additionalDaysInSIT) + serviceItemsToPrice := StorageServiceItems(*ppmShipment, *ppmShipment.SITLocation, additionalDaysInSIT) totalPrice := unit.Cents(0) for _, serviceItem := range serviceItemsToPrice { @@ -826,7 +867,7 @@ func CalculateSITCostBreakdown(appCtx appcontext.AppContext, ppmShipment *models additionalDaysInSIT := additionalDaysInSIT(*ppmShipment.SITEstimatedEntryDate, *ppmShipment.SITEstimatedDepartureDate) - serviceItemsToPrice := StorageServiceItems(ppmShipment.ShipmentID, *ppmShipment.SITLocation, additionalDaysInSIT) + serviceItemsToPrice := StorageServiceItems(*ppmShipment, *ppmShipment.SITLocation, additionalDaysInSIT) totalPrice := unit.Cents(0) for _, serviceItem := range serviceItemsToPrice { @@ -1037,10 +1078,14 @@ func priceAdditionalDaySIT(appCtx appcontext.AppContext, pricer services.ParamsP // expect to find them on the MTOShipment model. This is only in-memory and shouldn't get saved to the database. func MapPPMShipmentEstimatedFields(appCtx appcontext.AppContext, ppmShipment models.PPMShipment) (models.MTOShipment, error) { + ppmShipment.Shipment.PPMShipment = &ppmShipment + ppmShipment.Shipment.ShipmentType = models.MTOShipmentTypePPM ppmShipment.Shipment.ActualPickupDate = &ppmShipment.ExpectedDepartureDate ppmShipment.Shipment.RequestedPickupDate = &ppmShipment.ExpectedDepartureDate - ppmShipment.Shipment.PickupAddress = &models.Address{PostalCode: ppmShipment.PickupAddress.PostalCode} - ppmShipment.Shipment.DestinationAddress = &models.Address{PostalCode: ppmShipment.DestinationAddress.PostalCode} + ppmShipment.Shipment.PickupAddress = ppmShipment.PickupAddress + ppmShipment.Shipment.PickupAddress = &models.Address{PostalCode: *ppmShipment.ActualPickupPostalCode} + ppmShipment.Shipment.DestinationAddress = ppmShipment.DestinationAddress + ppmShipment.Shipment.DestinationAddress = &models.Address{PostalCode: *ppmShipment.ActualDestinationPostalCode} ppmShipment.Shipment.PrimeActualWeight = ppmShipment.EstimatedWeight return ppmShipment.Shipment, nil @@ -1076,9 +1121,13 @@ func MapPPMShipmentMaxIncentiveFields(appCtx appcontext.AppContext, ppmShipment // expect to find them on the MTOShipment model. This is only in-memory and shouldn't get saved to the database. func MapPPMShipmentFinalFields(ppmShipment models.PPMShipment, totalWeight unit.Pound) models.MTOShipment { + ppmShipment.Shipment.PPMShipment = &ppmShipment + ppmShipment.Shipment.ShipmentType = models.MTOShipmentTypePPM ppmShipment.Shipment.ActualPickupDate = ppmShipment.ActualMoveDate ppmShipment.Shipment.RequestedPickupDate = ppmShipment.ActualMoveDate + ppmShipment.Shipment.PickupAddress = ppmShipment.PickupAddress ppmShipment.Shipment.PickupAddress = &models.Address{PostalCode: *ppmShipment.ActualPickupPostalCode} + ppmShipment.Shipment.DestinationAddress = ppmShipment.DestinationAddress ppmShipment.Shipment.DestinationAddress = &models.Address{PostalCode: *ppmShipment.ActualDestinationPostalCode} ppmShipment.Shipment.PrimeActualWeight = &totalWeight @@ -1087,19 +1136,35 @@ func MapPPMShipmentFinalFields(ppmShipment models.PPMShipment, totalWeight unit. // baseServiceItems returns a list of the MTOServiceItems that makeup the price of the estimated incentive. These // are the same non-accesorial service items that get auto-created and approved when the TOO approves an HHG shipment. -func BaseServiceItems(mtoShipmentID uuid.UUID) []models.MTOServiceItem { - return []models.MTOServiceItem{ - {ReService: models.ReService{Code: models.ReServiceCodeDLH}, MTOShipmentID: &mtoShipmentID}, - {ReService: models.ReService{Code: models.ReServiceCodeFSC}, MTOShipmentID: &mtoShipmentID}, - {ReService: models.ReService{Code: models.ReServiceCodeDOP}, MTOShipmentID: &mtoShipmentID}, - {ReService: models.ReService{Code: models.ReServiceCodeDDP}, MTOShipmentID: &mtoShipmentID}, - {ReService: models.ReService{Code: models.ReServiceCodeDPK}, MTOShipmentID: &mtoShipmentID}, - {ReService: models.ReService{Code: models.ReServiceCodeDUPK}, MTOShipmentID: &mtoShipmentID}, +func BaseServiceItems(ppmShipment models.PPMShipment) []models.MTOServiceItem { + mtoShipmentID := ppmShipment.ShipmentID + isInternationalShipment := ppmShipment.Shipment.MarketCode == models.MarketCodeInternational + + if isInternationalShipment { + return []models.MTOServiceItem{ + {ReService: models.ReService{Code: models.ReServiceCodeFSC}, MTOShipmentID: &mtoShipmentID}, + {ReService: models.ReService{Code: models.ReServiceCodeIHPK}, MTOShipmentID: &mtoShipmentID}, + {ReService: models.ReService{Code: models.ReServiceCodeIHUPK}, MTOShipmentID: &mtoShipmentID}, + {ReService: models.ReService{Code: models.ReServiceCodeISLH}, MTOShipmentID: &mtoShipmentID}, + } + } else { + return []models.MTOServiceItem{ + {ReService: models.ReService{Code: models.ReServiceCodeDLH}, MTOShipmentID: &mtoShipmentID}, + {ReService: models.ReService{Code: models.ReServiceCodeFSC}, MTOShipmentID: &mtoShipmentID}, + {ReService: models.ReService{Code: models.ReServiceCodeDOP}, MTOShipmentID: &mtoShipmentID}, + {ReService: models.ReService{Code: models.ReServiceCodeDDP}, MTOShipmentID: &mtoShipmentID}, + {ReService: models.ReService{Code: models.ReServiceCodeDPK}, MTOShipmentID: &mtoShipmentID}, + {ReService: models.ReService{Code: models.ReServiceCodeDUPK}, MTOShipmentID: &mtoShipmentID}, + } } } -func StorageServiceItems(mtoShipmentID uuid.UUID, locationType models.SITLocationType, additionalDaysInSIT int) []models.MTOServiceItem { - if locationType == models.SITLocationTypeOrigin { +func StorageServiceItems(ppmShipment models.PPMShipment, locationType models.SITLocationType, additionalDaysInSIT int) []models.MTOServiceItem { + mtoShipmentID := ppmShipment.ShipmentID + isInternationalShipment := ppmShipment.Shipment.MarketCode == models.MarketCodeInternational + + // domestic shipments + if locationType == models.SITLocationTypeOrigin && !isInternationalShipment { if additionalDaysInSIT > 0 { return []models.MTOServiceItem{ {ReService: models.ReService{Code: models.ReServiceCodeDOFSIT}, MTOShipmentID: &mtoShipmentID}, @@ -1110,15 +1175,41 @@ func StorageServiceItems(mtoShipmentID uuid.UUID, locationType models.SITLocatio {ReService: models.ReService{Code: models.ReServiceCodeDOFSIT}, MTOShipmentID: &mtoShipmentID}} } - if additionalDaysInSIT > 0 { + if locationType == models.SITLocationTypeDestination && !isInternationalShipment { + if additionalDaysInSIT > 0 { + return []models.MTOServiceItem{ + {ReService: models.ReService{Code: models.ReServiceCodeDDFSIT}, MTOShipmentID: &mtoShipmentID}, + {ReService: models.ReService{Code: models.ReServiceCodeDDASIT}, MTOShipmentID: &mtoShipmentID}, + } + } + return []models.MTOServiceItem{ + {ReService: models.ReService{Code: models.ReServiceCodeDDFSIT}, MTOShipmentID: &mtoShipmentID}} + } + + // international shipments + if locationType == models.SITLocationTypeOrigin && isInternationalShipment { + if additionalDaysInSIT > 0 { + return []models.MTOServiceItem{ + {ReService: models.ReService{Code: models.ReServiceCodeIOFSIT}, MTOShipmentID: &mtoShipmentID}, + {ReService: models.ReService{Code: models.ReServiceCodeIOASIT}, MTOShipmentID: &mtoShipmentID}, + } + } return []models.MTOServiceItem{ - {ReService: models.ReService{Code: models.ReServiceCodeDDFSIT}, MTOShipmentID: &mtoShipmentID}, - {ReService: models.ReService{Code: models.ReServiceCodeDDASIT}, MTOShipmentID: &mtoShipmentID}, + {ReService: models.ReService{Code: models.ReServiceCodeIOFSIT}, MTOShipmentID: &mtoShipmentID}} + } + + if locationType == models.SITLocationTypeDestination && isInternationalShipment { + if additionalDaysInSIT > 0 { + return []models.MTOServiceItem{ + {ReService: models.ReService{Code: models.ReServiceCodeIDFSIT}, MTOShipmentID: &mtoShipmentID}, + {ReService: models.ReService{Code: models.ReServiceCodeIDASIT}, MTOShipmentID: &mtoShipmentID}, + } } + return []models.MTOServiceItem{ + {ReService: models.ReService{Code: models.ReServiceCodeDDFSIT}, MTOShipmentID: &mtoShipmentID}} } - return []models.MTOServiceItem{ - {ReService: models.ReService{Code: models.ReServiceCodeDDFSIT}, MTOShipmentID: &mtoShipmentID}} + return nil } // paramsForServiceCode filters the list of all service params for service items, to only those matching the service diff --git a/src/components/Office/PPM/PPMHeaderSummary/HeaderSection.jsx b/src/components/Office/PPM/PPMHeaderSummary/HeaderSection.jsx index 603305cd7c2..2344a110ab2 100644 --- a/src/components/Office/PPM/PPMHeaderSummary/HeaderSection.jsx +++ b/src/components/Office/PPM/PPMHeaderSummary/HeaderSection.jsx @@ -62,6 +62,9 @@ const getSectionMarkup = (sectionInfo, handleEditOnClick, isFetchingItems, updat const isCivilian = grade === 'CIVILIAN_EMPLOYEE'; const renderHaulType = (haulType) => { + if (haulType === '') { + return null; + } return haulType === HAUL_TYPES.LINEHAUL ? 'Linehaul' : 'Shorthaul'; }; // check if the itemName is one of the items recalulated after item edit(updatedItemName). @@ -268,16 +271,18 @@ const getSectionMarkup = (sectionInfo, handleEditOnClick, isFetchingItems, updat case sectionTypes.incentiveFactors: return (
-
- - - {isFetchingItems && isRecalulatedItem('haulPrice') ? ( - - ) : ( - `$${formatCents(sectionInfo.haulPrice)}` - )} - -
+ {sectionInfo.haulPrice > 0 ?? ( +
+ + + {isFetchingItems && isRecalulatedItem('haulPrice') ? ( + + ) : ( + `$${formatCents(sectionInfo.haulPrice)}` + )} + +
+ )}
@@ -291,52 +296,92 @@ const getSectionMarkup = (sectionInfo, handleEditOnClick, isFetchingItems, updat )}
+ {sectionInfo.packPrice > 0 ?? ( +
+ + + {isFetchingItems && isRecalulatedItem('packPrice') ? ( + + ) : ( + `$${formatCents(sectionInfo.packPrice)}` + )} + +
+ )} + {sectionInfo.unpackPrice > 0 ?? ( +
+ + + {isFetchingItems && isRecalulatedItem('unpackPrice') ? ( + + ) : ( + `$${formatCents(sectionInfo.unpackPrice)}` + )} + +
+ )} + {sectionInfo.dop > 0 ?? ( +
+ + + {isFetchingItems && isRecalulatedItem('dop') ? ( + + ) : ( + `$${formatCents(sectionInfo.dop)}` + )} + +
+ )} + {sectionInfo.ddp > 0 ?? ( +
+ + + {isFetchingItems && isRecalulatedItem('ddp') ? ( + + ) : ( + `$${formatCents(sectionInfo.ddp)}` + )} + +
+ )}
- - - {isFetchingItems && isRecalulatedItem('packPrice') ? ( - - ) : ( - `$${formatCents(sectionInfo.packPrice)}` - )} - -
-
- - - {isFetchingItems && isRecalulatedItem('unpackPrice') ? ( + + + {isFetchingItems && isRecalulatedItem('intlPackPrice') ? ( ) : ( - `$${formatCents(sectionInfo.unpackPrice)}` + `$${formatCents(sectionInfo.intlPackPrice)}` )}
- - - {isFetchingItems && isRecalulatedItem('dop') ? ( + + + {isFetchingItems && isRecalulatedItem('intlUnpackPrice') ? ( ) : ( - `$${formatCents(sectionInfo.dop)}` + `$${formatCents(sectionInfo.intlUnpackPrice)}` )}
- - - {isFetchingItems && isRecalulatedItem('ddp') ? ( + + + {isFetchingItems && isRecalulatedItem('intlLinehaulPrice') ? ( ) : ( - `$${formatCents(sectionInfo.ddp)}` + `$${formatCents(sectionInfo.intlLinehaulPrice)}` )}
-
- - - ${formatCents(sectionInfo.sitReimbursement)} - -
+ {sectionInfo.sitReimbursement > 0 ?? ( +
+ + + ${formatCents(sectionInfo.sitReimbursement)} + +
+ )}
); diff --git a/src/components/Office/PPM/PPMHeaderSummary/PPMHeaderSummary.jsx b/src/components/Office/PPM/PPMHeaderSummary/PPMHeaderSummary.jsx index 955389774cd..03a81392151 100644 --- a/src/components/Office/PPM/PPMHeaderSummary/PPMHeaderSummary.jsx +++ b/src/components/Office/PPM/PPMHeaderSummary/PPMHeaderSummary.jsx @@ -10,6 +10,7 @@ import LoadingPlaceholder from 'shared/LoadingPlaceholder'; import SomethingWentWrong from 'shared/SomethingWentWrong'; import { usePPMCloseoutQuery } from 'hooks/queries'; import { formatCustomerContactFullAddress } from 'utils/formatters'; +import { INTL_PPM_PORT_INFO } from 'shared/constants'; const GCCAndIncentiveInfo = ({ ppmShipmentInfo, updatedItemName, setUpdatedItemName, readOnly }) => { const { ppmCloseout, isLoading, isError } = usePPMCloseoutQuery(ppmShipmentInfo.id); @@ -36,6 +37,9 @@ const GCCAndIncentiveInfo = ({ ppmShipmentInfo, updatedItemName, setUpdatedItemN dop: ppmCloseout.dop, ddp: ppmCloseout.ddp, sitReimbursement: ppmCloseout.SITReimbursement, + intlPackPrice: ppmCloseout.intlPackPrice, + intlUnpackPrice: ppmCloseout.intlUnpackPrice, + intlLinehaulPrice: ppmCloseout.intlLinehaulPrice, }; return ( @@ -75,6 +79,7 @@ export default function PPMHeaderSummary({ ppmShipmentInfo, order, ppmNumber, sh : '—', pickupAddressObj: ppmShipmentInfo.pickupAddress, destinationAddressObj: ppmShipmentInfo.destinationAddress, + port: INTL_PPM_PORT_INFO, miles: ppmShipmentInfo.miles, estimatedWeight: ppmShipmentInfo.estimatedWeight, actualWeight: ppmShipmentInfo.actualWeight, diff --git a/src/shared/constants.js b/src/shared/constants.js index 884691d5c3c..6f2752d8016 100644 --- a/src/shared/constants.js +++ b/src/shared/constants.js @@ -236,4 +236,9 @@ const ADDRESS_LABELS_MAP = { [ADDRESS_TYPES.THIRD_DESTINATION]: 'Third Delivery Address', }; +export const INTL_PPM_PORT_INFO = { + portName: 'Tacoma, WA', + portZip: '98424', +}; + export const getAddressLabel = (type) => ADDRESS_LABELS_MAP[type]; diff --git a/swagger-def/definitions/PPMCloseout.yaml b/swagger-def/definitions/PPMCloseout.yaml index 20203d0e4e7..f7b420a0e7b 100644 --- a/swagger-def/definitions/PPMCloseout.yaml +++ b/swagger-def/definitions/PPMCloseout.yaml @@ -6,7 +6,6 @@ properties: format: uuid type: string readOnly: true - plannedMoveDate: description: > Date the customer expects to begin their move. @@ -27,32 +26,27 @@ properties: type: integer x-nullable: true x-omitempty: false - estimatedWeight: description: The estimated weight of the PPM shipment goods being moved. type: integer example: 4200 x-nullable: true x-omitempty: false - actualWeight: example: 2000 type: integer x-nullable: true x-omitempty: false - proGearWeightCustomer: description: The estimated weight of the pro-gear being moved belonging to the service member. type: integer x-nullable: true x-omitempty: false - proGearWeightSpouse: description: The estimated weight of the pro-gear being moved belonging to a spouse. type: integer x-nullable: true x-omitempty: false - grossIncentive: description: > The final calculated incentive for the PPM shipment. This does not include **SIT** as it is a reimbursement. @@ -61,7 +55,6 @@ properties: x-nullable: true x-omitempty: false readOnly: true - gcc: description: Government Constructive Cost (GCC) type: integer @@ -69,75 +62,82 @@ properties: format: cents x-nullable: true x-omitempty: false - aoa: description: Advance Operating Allowance (AOA). type: integer format: cents x-nullable: true x-omitempty: false - remainingIncentive: description: The remaining reimbursement amount that is still owed to the customer. type: integer format: cents x-nullable: true x-omitempty: false - haulType: description: The type of haul calculation used for this shipment (shorthaul or linehaul). type: string x-nullable: true x-omitempty: false - haulPrice: description: The price of the linehaul or shorthaul. type: integer format: cents x-nullable: true x-omitempty: false - haulFSC: description: The linehaul/shorthaul Fuel Surcharge (FSC). type: integer format: cents x-nullable: true x-omitempty: false - dop: description: The Domestic Origin Price (DOP). type: integer format: cents x-nullable: true x-omitempty: false - ddp: description: The Domestic Destination Price (DDP). type: integer format: cents x-nullable: true x-omitempty: false - packPrice: description: The full price of all packing/unpacking services. type: integer format: cents x-nullable: true x-omitempty: false - unpackPrice: description: The full price of all packing/unpacking services. type: integer format: cents x-nullable: true x-omitempty: false - + intlPackPrice: + description: The full price of international packing (IHPK) + type: integer + format: cents + x-nullable: true + x-omitempty: false + intlUnpackPrice: + description: The full price of international unpacking (IHUPK) + type: integer + format: cents + x-nullable: true + x-omitempty: false + intlLinehaulPrice: + description: The full price of international shipping and linehaul (ISLH) + type: integer + format: cents + x-nullable: true + x-omitempty: false SITReimbursement: description: The estimated amount that the government will pay the service member to put their goods into storage. This estimated storage cost is separate from the estimated incentive. type: integer format: cents x-nullable: true x-omitempty: false - required: - id diff --git a/swagger/ghc.yaml b/swagger/ghc.yaml index e40e0aaa1eb..945523f20de 100644 --- a/swagger/ghc.yaml +++ b/swagger/ghc.yaml @@ -10929,6 +10929,24 @@ definitions: format: cents x-nullable: true x-omitempty: false + intlPackPrice: + description: The full price of international packing (IHPK) + type: integer + format: cents + x-nullable: true + x-omitempty: false + intlUnpackPrice: + description: The full price of international unpacking (IHUPK) + type: integer + format: cents + x-nullable: true + x-omitempty: false + intlLinehaulPrice: + description: The full price of international shipping and linehaul (ISLH) + type: integer + format: cents + x-nullable: true + x-omitempty: false SITReimbursement: description: >- The estimated amount that the government will pay the service member