From 1dc04d48adf1b236608b6f046096bdccf6068bc4 Mon Sep 17 00:00:00 2001 From: Daniel Jordan Date: Tue, 14 Jan 2025 21:51:28 +0000 Subject: [PATCH 01/18] initial commit, have the function working but need to address max incentive check since it is erroring out --- migrations/app/migrations_manifest.txt | 1 + ...52_add_ppm_estimated_incentive_proc.up.sql | 117 ++++++++++++++++++ pkg/models/ppm_shipment.go | 15 +++ .../mto_shipment/mto_shipment_updater.go | 4 +- pkg/services/ppmshipment/ppm_estimator.go | 66 ++++++---- 5 files changed, 175 insertions(+), 28 deletions(-) create mode 100644 migrations/app/schema/20250114164752_add_ppm_estimated_incentive_proc.up.sql diff --git a/migrations/app/migrations_manifest.txt b/migrations/app/migrations_manifest.txt index bacd3130cac..c4ddda302b5 100644 --- a/migrations/app/migrations_manifest.txt +++ b/migrations/app/migrations_manifest.txt @@ -1069,3 +1069,4 @@ 20250103180420_update_pricing_proc_to_use_local_price_variable.up.sql 20250110214012_homesafeconnect_cert.up.sql 20250113201232_update_estimated_pricing_procs_add_is_peak_func.up.sql +20250114164752_add_ppm_estimated_incentive_proc.up.sql 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 new file mode 100644 index 00000000000..e6925e05eb0 --- /dev/null +++ b/migrations/app/schema/20250114164752_add_ppm_estimated_incentive_proc.up.sql @@ -0,0 +1,117 @@ +CREATE OR REPLACE FUNCTION calculate_ppm_incentive( + ppm_id UUID, + mileage INT, + weight INT, + is_estimated BOOLEAN, + is_actual BOOLEAN +) RETURNS NUMERIC AS +$$ +DECLARE + ppm RECORD; + escalated_price NUMERIC; + estimated_price_islh NUMERIC; + estimated_price_ihpk NUMERIC; + estimated_price_ihupk NUMERIC; + estimated_price_fsc NUMERIC; + total_incentive NUMERIC := 0; + contract_id UUID; + o_rate_area_id UUID; + d_rate_area_id UUID; + service_id UUID; + estimated_fsc_multiplier NUMERIC; + fuel_price NUMERIC; + price_difference NUMERIC; + cents_above_baseline NUMERIC; +BEGIN + + IF NOT is_estimated AND NOT is_actual THEN + RAISE EXCEPTION 'Both is_estimated and is_actual cannot be FALSE. No update will be performed.'; + END IF; + + SELECT ppms.id, ppms.pickup_postal_address_id, ppms.destination_postal_address_id, ppms.expected_departure_date + 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; + + contract_id := get_contract_id(ppm.expected_departure_date); + IF contract_id IS NULL THEN + RAISE EXCEPTION 'Contract not found for date: %', ppm.expected_departure_date; + END IF; + + o_rate_area_id := get_rate_area_id(ppm.pickup_postal_address_id, NULL, contract_id); + IF o_rate_area_id IS NULL THEN + RAISE EXCEPTION 'Origin rate area is NULL for address ID %', ppm.pickup_postal_address_id; + END IF; + + d_rate_area_id := get_rate_area_id(ppm.destination_postal_address_id, NULL, contract_id); + IF d_rate_area_id IS NULL THEN + RAISE EXCEPTION 'Destination rate area is NULL for address ID %', ppm.destination_postal_address_id; + END IF; + + -- ISLH calculation + SELECT rs.id INTO service_id FROM re_services rs WHERE rs.code = 'ISLH'; + estimated_price_islh := ROUND( + calculate_escalated_price( + o_rate_area_id, + d_rate_area_id, + service_id, + contract_id, + 'ISLH', + ppm.expected_departure_date + ) * (weight / 100)::NUMERIC * 100, 0 + ); + RAISE NOTICE 'Estimated price for ISLH: % cents', estimated_price_islh; + + -- IHPK calculation + SELECT rs.id INTO service_id FROM re_services rs WHERE rs.code = 'IHPK'; + estimated_price_ihpk := ROUND( + calculate_escalated_price( + o_rate_area_id, + NULL, + service_id, + contract_id, + 'IHPK', + ppm.expected_departure_date + ) * (weight / 100)::NUMERIC * 100, 0 + ); + RAISE NOTICE 'Estimated price for IHPK: % cents', estimated_price_ihpk; + + -- IHUPK calculation + SELECT rs.id INTO service_id FROM re_services rs WHERE rs.code = 'IHUPK'; + estimated_price_ihupk := ROUND( + calculate_escalated_price( + NULL, + d_rate_area_id, + service_id, + contract_id, + 'IHUPK', + ppm.expected_departure_date + ) * (weight / 100)::NUMERIC * 100, 0 + ); + RAISE NOTICE 'Estimated price for IHUPK: % cents', estimated_price_ihupk; + + -- FSC calculation + estimated_fsc_multiplier := get_fsc_multiplier(weight); + fuel_price := get_fuel_price(ppm.expected_departure_date); + price_difference := calculate_price_difference(fuel_price); + cents_above_baseline := mileage * estimated_fsc_multiplier; + estimated_price_fsc := ROUND((cents_above_baseline * price_difference) * 100); + RAISE NOTICE 'Estimated price for FSC: % cents', estimated_price_fsc; + + -- total + total_incentive := estimated_price_islh + estimated_price_ihpk + estimated_price_ihupk + estimated_price_fsc; + RAISE NOTICE 'Total PPM Incentive: % cents', total_incentive; + + -- now update the incentive value + 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 + WHERE id = ppm_id; + + RETURN total_incentive; +END; +$$ LANGUAGE plpgsql; diff --git a/pkg/models/ppm_shipment.go b/pkg/models/ppm_shipment.go index 0737417207c..40719f45d98 100644 --- a/pkg/models/ppm_shipment.go +++ b/pkg/models/ppm_shipment.go @@ -1,6 +1,7 @@ package models import ( + "fmt" "time" "github.com/gobuffalo/pop/v6" @@ -319,3 +320,17 @@ func FetchPPMShipmentByPPMShipmentID(db *pop.Connection, ppmShipmentID uuid.UUID } return &ppmShipment, nil } + +// a db stored proc 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, mileage int, weight int, isEstimated bool, isActual bool) (int, error) { + var incentive int + + err := db.RawQuery("SELECT calculate_ppm_incentive($1, $2, $3, $4, $5)", ppmID, mileage, weight, isEstimated, isActual). + First(&incentive) + if err != nil { + return 0, fmt.Errorf("error calculating PPM incentive for PPM ID %s: %w", ppmID, err) + } + + return incentive, nil +} diff --git a/pkg/services/mto_shipment/mto_shipment_updater.go b/pkg/services/mto_shipment/mto_shipment_updater.go index 3fa1f6ed832..cf3336d0261 100644 --- a/pkg/services/mto_shipment/mto_shipment_updater.go +++ b/pkg/services/mto_shipment/mto_shipment_updater.go @@ -844,7 +844,9 @@ func (f *mtoShipmentUpdater) updateShipmentRecord(appCtx appcontext.AppContext, } // when populating the market_code column, it is considered domestic if both pickup & dest are CONUS addresses - newShipment = models.DetermineShipmentMarketCode(newShipment) + if newShipment.ShipmentType != models.MTOShipmentTypePPM { + newShipment = models.DetermineShipmentMarketCode(newShipment) + } if err := txnAppCtx.DB().Update(newShipment); err != nil { return err diff --git a/pkg/services/ppmshipment/ppm_estimator.go b/pkg/services/ppmshipment/ppm_estimator.go index e49d7846bbe..38f3b5aa832 100644 --- a/pkg/services/ppmshipment/ppm_estimator.go +++ b/pkg/services/ppmshipment/ppm_estimator.go @@ -203,46 +203,58 @@ func (f *estimatePPM) estimateIncentive(appCtx appcontext.AppContext, oldPPMShip } } - calculateSITEstimate := shouldCalculateSITCost(newPPMShipment, &oldPPMShipment) + // if the PPM is international, we will use a db stored proc + if newPPMShipment.Shipment.MarketCode != models.MarketCodeInternational { - // 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) + calculateSITEstimate := shouldCalculateSITCost(newPPMShipment, &oldPPMShipment) - if skipCalculatingEstimatedIncentive && !calculateSITEstimate { - return oldPPMShipment.EstimatedIncentive, 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 + } - contractDate := newPPMShipment.ExpectedDepartureDate - contract, err := serviceparamvaluelookups.FetchContract(appCtx, contractDate) - if err != nil { - return nil, nil, err - } + skipCalculatingEstimatedIncentive := shouldSkipEstimatingIncentive(newPPMShipment, &oldPPMShipment) - estimatedIncentive := oldPPMShipment.EstimatedIncentive - if !skipCalculatingEstimatedIncentive { - // Clear out advance and advance requested fields when the estimated incentive is reset. - newPPMShipment.HasRequestedAdvance = nil - newPPMShipment.AdvanceAmountRequested = nil + if skipCalculatingEstimatedIncentive && !calculateSITEstimate { + return oldPPMShipment.EstimatedIncentive, newPPMShipment.SITEstimatedCost, nil + } - estimatedIncentive, err = f.calculatePrice(appCtx, newPPMShipment, 0, contract, false) + contractDate := newPPMShipment.ExpectedDepartureDate + contract, err := serviceparamvaluelookups.FetchContract(appCtx, contractDate) if err != nil { return nil, nil, err } - } - estimatedSITCost := oldPPMShipment.SITEstimatedCost - if calculateSITEstimate { - estimatedSITCost, err = CalculateSITCost(appCtx, newPPMShipment, contract) + estimatedIncentive := oldPPMShipment.EstimatedIncentive + if !skipCalculatingEstimatedIncentive { + // Clear out advance and advance requested fields when the estimated incentive is reset. + newPPMShipment.HasRequestedAdvance = nil + newPPMShipment.AdvanceAmountRequested = nil + + estimatedIncentive, err = f.calculatePrice(appCtx, newPPMShipment, 0, contract, false) + if err != nil { + return nil, nil, err + } + } + + estimatedSITCost := oldPPMShipment.SITEstimatedCost + if calculateSITEstimate { + estimatedSITCost, err = CalculateSITCost(appCtx, newPPMShipment, contract) + if err != nil { + return nil, nil, err + } + } + + return estimatedIncentive, estimatedSITCost, nil + + } else { + estimatedIncentive, err := models.CalculatePPMIncentive(appCtx.DB(), newPPMShipment.ID, 1000, newPPMShipment.EstimatedWeight.Int(), true, false) if err != nil { return nil, nil, err } - } - return estimatedIncentive, estimatedSITCost, nil + return (*unit.Cents)(&estimatedIncentive), nil, nil + } } func (f *estimatePPM) maxIncentive(appCtx appcontext.AppContext, oldPPMShipment models.PPMShipment, newPPMShipment *models.PPMShipment, checks ...ppmShipmentValidator) (*unit.Cents, error) { From edbbb04e64299ef399b8010d5bf147035f4926da Mon Sep 17 00:00:00 2001 From: Daniel Jordan Date: Wed, 15 Jan 2025 14:50:15 +0000 Subject: [PATCH 02/18] added port check to estimator, need to address max incentive since it is also being checked --- pkg/models/port_location.go | 12 ++++++++ pkg/services/ppmshipment/ppm_estimator.go | 37 +++++++++++++++++++++-- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/pkg/models/port_location.go b/pkg/models/port_location.go index 4d514a2a545..414006d9c82 100644 --- a/pkg/models/port_location.go +++ b/pkg/models/port_location.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 PortLocation struct { @@ -24,3 +27,12 @@ type PortLocation struct { func (l PortLocation) TableName() string { return "port_locations" } + +func FetchPortLocationByCode(db *pop.Connection, portCode string) (*PortLocation, error) { + portLocation := PortLocation{} + err := db.Eager("Port", "UsPostRegionCity").Where("is_active = TRUE").InnerJoin("ports p", "port_id = p.id").Where("p.port_code = $1", portCode).First(&portLocation) + if err != nil { + return nil, apperror.NewQueryError("PortLocation", err, "") + } + return &portLocation, err +} diff --git a/pkg/services/ppmshipment/ppm_estimator.go b/pkg/services/ppmshipment/ppm_estimator.go index 38f3b5aa832..ef775c587d2 100644 --- a/pkg/services/ppmshipment/ppm_estimator.go +++ b/pkg/services/ppmshipment/ppm_estimator.go @@ -248,9 +248,42 @@ func (f *estimatePPM) estimateIncentive(appCtx appcontext.AppContext, oldPPMShip return estimatedIncentive, estimatedSITCost, nil } else { - estimatedIncentive, err := models.CalculatePPMIncentive(appCtx.DB(), newPPMShipment.ID, 1000, newPPMShipment.EstimatedWeight.Int(), true, false) + var mileage int + pickupAddress := newPPMShipment.PickupAddress + destinationAddress := newPPMShipment.DestinationAddress + + // get the Tacoma, WA port (code: 3002) - this is the authorized port for PPMs + ppmPort, err := models.FetchPortLocationByCode(appCtx.DB(), "3002") if err != nil { - return nil, nil, err + return nil, nil, fmt.Errorf("failed to fetch port location: %w", err) + } + + // handling OCONUS/CONUS mileage logic to determine mileage checks + isPickupOconus := pickupAddress.IsOconus != nil && *pickupAddress.IsOconus + isDestinationOconus := destinationAddress.IsOconus != nil && *destinationAddress.IsOconus + + switch { + case isPickupOconus && isDestinationOconus: + // OCONUS -> OCONUS: no mileage (set to 0) + mileage = 0 + case isPickupOconus && !isDestinationOconus: + // OCONUS -> CONUS: get mileage from port ZIP to destination ZIP + mileage, err = f.planner.ZipTransitDistance(appCtx, ppmPort.UsPostRegionCity.UsprZipID, destinationAddress.PostalCode, true, true) + if err != nil { + return nil, nil, fmt.Errorf("failed to calculate OCONUS to CONUS mileage: %w", err) + } + case !isPickupOconus && isDestinationOconus: + // CONUS -> OCONUS: get mileage from pickup ZIP to port ZIP + mileage, err = f.planner.ZipTransitDistance(appCtx, pickupAddress.PostalCode, ppmPort.UsPostRegionCity.UsprZipID, true, true) + if err != nil { + return nil, nil, fmt.Errorf("failed to calculate CONUS to OCONUS mileage: %w", err) + } + } + + // now we can calculate the incentive + estimatedIncentive, err := models.CalculatePPMIncentive(appCtx.DB(), newPPMShipment.ID, mileage, newPPMShipment.EstimatedWeight.Int(), true, false) + if err != nil { + return nil, nil, fmt.Errorf("failed to calculate estimated PPM incentive: %w", err) } return (*unit.Cents)(&estimatedIncentive), nil, nil From 26610d91970fc305a9ec440a58dcb317ca25820f Mon Sep 17 00:00:00 2001 From: Daniel Jordan Date: Wed, 15 Jan 2025 21:00:45 +0000 Subject: [PATCH 03/18] estimated, actual, and max are good for db func, need to address closeout before tackling SIT --- ...52_add_ppm_estimated_incentive_proc.up.sql | 38 ++-- pkg/models/ppm_shipment.go | 4 +- pkg/services/ppmshipment/ppm_estimator.go | 177 +++++++++++------- .../ppmshipment/ppm_shipment_updater.go | 123 ++++++------ 4 files changed, 199 insertions(+), 143 deletions(-) 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 e6925e05eb0..ce8be73a674 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,9 +1,15 @@ +-- function that calculates a ppm incentive given mileage, weight, and dates +-- this is used to calculate estimated, max, and actual incentives CREATE OR REPLACE FUNCTION calculate_ppm_incentive( ppm_id UUID, + pickup_address_id UUID, + destination_address_id UUID, + move_date DATE, mileage INT, weight INT, is_estimated BOOLEAN, - is_actual BOOLEAN + is_actual BOOLEAN, + is_max BOOLEAN ) RETURNS NUMERIC AS $$ DECLARE @@ -24,11 +30,12 @@ DECLARE cents_above_baseline NUMERIC; BEGIN - IF NOT is_estimated AND NOT is_actual THEN - RAISE EXCEPTION 'Both is_estimated and is_actual cannot be FALSE. No update will be performed.'; + IF NOT is_estimated AND NOT is_actual AND NOT is_max THEN + RAISE EXCEPTION 'is_estimated, is_actual, and is_max cannot all be FALSE. No update will be performed.'; END IF; - SELECT ppms.id, ppms.pickup_postal_address_id, ppms.destination_postal_address_id, ppms.expected_departure_date + -- validating it's a real PPM + SELECT ppms.id INTO ppm FROM ppm_shipments ppms WHERE ppms.id = ppm_id; @@ -37,19 +44,19 @@ BEGIN RAISE EXCEPTION 'PPM with ID % not found', ppm_id; END IF; - contract_id := get_contract_id(ppm.expected_departure_date); + contract_id := get_contract_id(move_date); IF contract_id IS NULL THEN - RAISE EXCEPTION 'Contract not found for date: %', ppm.expected_departure_date; + RAISE EXCEPTION 'Contract not found for date: %', move_date; END IF; - o_rate_area_id := get_rate_area_id(ppm.pickup_postal_address_id, NULL, contract_id); + o_rate_area_id := get_rate_area_id(pickup_address_id, NULL, contract_id); IF o_rate_area_id IS NULL THEN - RAISE EXCEPTION 'Origin rate area is NULL for address ID %', ppm.pickup_postal_address_id; + RAISE EXCEPTION 'Origin rate area is NULL for address ID %', pickup_address_id; END IF; - d_rate_area_id := get_rate_area_id(ppm.destination_postal_address_id, NULL, contract_id); + d_rate_area_id := get_rate_area_id(destination_address_id, NULL, contract_id); IF d_rate_area_id IS NULL THEN - RAISE EXCEPTION 'Destination rate area is NULL for address ID %', ppm.destination_postal_address_id; + RAISE EXCEPTION 'Destination rate area is NULL for address ID %', destination_address_id; END IF; -- ISLH calculation @@ -61,7 +68,7 @@ BEGIN service_id, contract_id, 'ISLH', - ppm.expected_departure_date + move_date ) * (weight / 100)::NUMERIC * 100, 0 ); RAISE NOTICE 'Estimated price for ISLH: % cents', estimated_price_islh; @@ -75,7 +82,7 @@ BEGIN service_id, contract_id, 'IHPK', - ppm.expected_departure_date + move_date ) * (weight / 100)::NUMERIC * 100, 0 ); RAISE NOTICE 'Estimated price for IHPK: % cents', estimated_price_ihpk; @@ -89,14 +96,14 @@ BEGIN service_id, contract_id, 'IHUPK', - ppm.expected_departure_date + move_date ) * (weight / 100)::NUMERIC * 100, 0 ); RAISE NOTICE 'Estimated price for IHUPK: % cents', estimated_price_ihupk; -- FSC calculation estimated_fsc_multiplier := get_fsc_multiplier(weight); - fuel_price := get_fuel_price(ppm.expected_departure_date); + fuel_price := get_fuel_price(move_date); price_difference := calculate_price_difference(fuel_price); cents_above_baseline := mileage * estimated_fsc_multiplier; estimated_price_fsc := ROUND((cents_above_baseline * price_difference) * 100); @@ -109,7 +116,8 @@ BEGIN -- now update the incentive value 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 + 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 total_incentive; diff --git a/pkg/models/ppm_shipment.go b/pkg/models/ppm_shipment.go index 40719f45d98..fab8720e125 100644 --- a/pkg/models/ppm_shipment.go +++ b/pkg/models/ppm_shipment.go @@ -323,10 +323,10 @@ func FetchPPMShipmentByPPMShipmentID(db *pop.Connection, ppmShipmentID uuid.UUID // a db stored proc 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, mileage int, weight int, isEstimated bool, isActual bool) (int, error) { +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) (int, error) { var incentive int - err := db.RawQuery("SELECT calculate_ppm_incentive($1, $2, $3, $4, $5)", ppmID, mileage, weight, isEstimated, isActual). + err := db.RawQuery("SELECT 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 { return 0, fmt.Errorf("error calculating PPM incentive for PPM ID %s: %w", ppmID, err) diff --git a/pkg/services/ppmshipment/ppm_estimator.go b/pkg/services/ppmshipment/ppm_estimator.go index ef775c587d2..971527632fc 100644 --- a/pkg/services/ppmshipment/ppm_estimator.go +++ b/pkg/services/ppmshipment/ppm_estimator.go @@ -203,7 +203,13 @@ func (f *estimatePPM) estimateIncentive(appCtx appcontext.AppContext, oldPPMShip } } - // if the PPM is international, we will use a db stored proc + contractDate := newPPMShipment.ExpectedDepartureDate + contract, err := serviceparamvaluelookups.FetchContract(appCtx, contractDate) + if err != nil { + 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) @@ -219,12 +225,6 @@ func (f *estimatePPM) estimateIncentive(appCtx appcontext.AppContext, oldPPMShip return oldPPMShipment.EstimatedIncentive, newPPMShipment.SITEstimatedCost, nil } - contractDate := newPPMShipment.ExpectedDepartureDate - contract, err := serviceparamvaluelookups.FetchContract(appCtx, contractDate) - if err != nil { - return nil, nil, err - } - estimatedIncentive := oldPPMShipment.EstimatedIncentive if !skipCalculatingEstimatedIncentive { // Clear out advance and advance requested fields when the estimated incentive is reset. @@ -248,45 +248,15 @@ func (f *estimatePPM) estimateIncentive(appCtx appcontext.AppContext, oldPPMShip return estimatedIncentive, estimatedSITCost, nil } else { - var mileage int pickupAddress := newPPMShipment.PickupAddress destinationAddress := newPPMShipment.DestinationAddress - // get the Tacoma, WA port (code: 3002) - this is the authorized port for PPMs - ppmPort, err := models.FetchPortLocationByCode(appCtx.DB(), "3002") - if err != nil { - return nil, nil, fmt.Errorf("failed to fetch port location: %w", err) - } - - // handling OCONUS/CONUS mileage logic to determine mileage checks - isPickupOconus := pickupAddress.IsOconus != nil && *pickupAddress.IsOconus - isDestinationOconus := destinationAddress.IsOconus != nil && *destinationAddress.IsOconus - - switch { - case isPickupOconus && isDestinationOconus: - // OCONUS -> OCONUS: no mileage (set to 0) - mileage = 0 - case isPickupOconus && !isDestinationOconus: - // OCONUS -> CONUS: get mileage from port ZIP to destination ZIP - mileage, err = f.planner.ZipTransitDistance(appCtx, ppmPort.UsPostRegionCity.UsprZipID, destinationAddress.PostalCode, true, true) - if err != nil { - return nil, nil, fmt.Errorf("failed to calculate OCONUS to CONUS mileage: %w", err) - } - case !isPickupOconus && isDestinationOconus: - // CONUS -> OCONUS: get mileage from pickup ZIP to port ZIP - mileage, err = f.planner.ZipTransitDistance(appCtx, pickupAddress.PostalCode, ppmPort.UsPostRegionCity.UsprZipID, true, true) - if err != nil { - return nil, nil, fmt.Errorf("failed to calculate CONUS to OCONUS mileage: %w", err) - } - } - - // now we can calculate the incentive - estimatedIncentive, err := models.CalculatePPMIncentive(appCtx.DB(), newPPMShipment.ID, mileage, newPPMShipment.EstimatedWeight.Int(), true, false) + 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 (*unit.Cents)(&estimatedIncentive), nil, nil + return estimatedIncentive, nil, nil } } @@ -306,7 +276,7 @@ func (f *estimatePPM) maxIncentive(appCtx appcontext.AppContext, oldPPMShipment // we have access to the MoveTaskOrderID in the ppmShipment object so we can use that to get the customer's maximum weight entitlement var move models.Move err = appCtx.DB().Q().Eager( - "Orders.Entitlement", + "Orders.Entitlement", "Orders.OriginDutyLocation.Address", "Orders.NewDutyLocation.Address", ).Where("show = TRUE").Find(&move, newPPMShipment.Shipment.MoveTaskOrderID) if err != nil { return nil, apperror.NewNotFoundError(newPPMShipment.ID, " error querying move") @@ -322,14 +292,27 @@ func (f *estimatePPM) maxIncentive(appCtx appcontext.AppContext, oldPPMShipment return nil, err } - // since the max incentive is based off of the authorized weight entitlement and that value CAN change - // we will calculate the max incentive each time it is called - maxIncentive, err := f.calculatePrice(appCtx, newPPMShipment, unit.Pound(*orders.Entitlement.DBAuthorizedWeight), contract, true) - if err != nil { - return nil, err - } + if newPPMShipment.Shipment.MarketCode != models.MarketCodeInternational { + + // since the max incentive is based off of the authorized weight entitlement and that value CAN change + // we will calculate the max incentive each time it is called + maxIncentive, err := f.calculatePrice(appCtx, newPPMShipment, unit.Pound(*orders.Entitlement.DBAuthorizedWeight), contract, true) + if err != nil { + return nil, err + } + + return maxIncentive, nil + } else { + pickupAddress := orders.OriginDutyLocation.Address + destinationAddress := orders.NewDutyLocation.Address - return maxIncentive, nil + maxIncentive, err := f.calculateOCONUSIncentive(appCtx, newPPMShipment.ID, pickupAddress, destinationAddress, contractDate, *orders.Entitlement.DBAuthorizedWeight, false, false, true) + if err != nil { + return nil, fmt.Errorf("failed to calculate estimated PPM incentive: %w", err) + } + + return maxIncentive, nil + } } func (f *estimatePPM) finalIncentive(appCtx appcontext.AppContext, oldPPMShipment models.PPMShipment, newPPMShipment *models.PPMShipment, checks ...ppmShipmentValidator) (*unit.Cents, error) { @@ -352,32 +335,51 @@ func (f *estimatePPM) finalIncentive(appCtx appcontext.AppContext, oldPPMShipmen newTotalWeight = *newPPMShipment.AllowableWeight } - isMissingInfo := shouldSetFinalIncentiveToNil(newPPMShipment, newTotalWeight) - var skipCalculateFinalIncentive bool - finalIncentive := oldPPMShipment.FinalIncentive + contractDate := newPPMShipment.ExpectedDepartureDate + if newPPMShipment.ActualMoveDate != nil { + contractDate = *newPPMShipment.ActualMoveDate + } + contract, err := serviceparamvaluelookups.FetchContract(appCtx, contractDate) + if err != nil { + return nil, err + } - if !isMissingInfo { - skipCalculateFinalIncentive = shouldSkipCalculatingFinalIncentive(newPPMShipment, &oldPPMShipment, originalTotalWeight, newTotalWeight) - if !skipCalculateFinalIncentive { - contractDate := newPPMShipment.ExpectedDepartureDate - if newPPMShipment.ActualMoveDate != nil { - contractDate = *newPPMShipment.ActualMoveDate - } - contract, err := serviceparamvaluelookups.FetchContract(appCtx, contractDate) - if err != nil { - return nil, err + if newPPMShipment.Shipment.MarketCode != models.MarketCodeInternational { + isMissingInfo := shouldSetFinalIncentiveToNil(newPPMShipment, newTotalWeight) + var skipCalculateFinalIncentive bool + finalIncentive := oldPPMShipment.FinalIncentive + if !isMissingInfo { + skipCalculateFinalIncentive = shouldSkipCalculatingFinalIncentive(newPPMShipment, &oldPPMShipment, originalTotalWeight, newTotalWeight) + if !skipCalculateFinalIncentive { + + finalIncentive, err := f.calculatePrice(appCtx, newPPMShipment, newTotalWeight, contract, false) + if err != nil { + return nil, err + } + return finalIncentive, nil } + } else { + finalIncentive = nil - finalIncentive, err = f.calculatePrice(appCtx, newPPMShipment, newTotalWeight, contract, false) + return finalIncentive, nil + } + + return finalIncentive, nil + } else { + pickupAddress := newPPMShipment.PickupAddress + destinationAddress := newPPMShipment.DestinationAddress + + // we can't calculate actual incentive without the weight + if newTotalWeight != 0 { + finalIncentive, err := f.calculateOCONUSIncentive(appCtx, newPPMShipment.ID, *pickupAddress, *destinationAddress, contractDate, newTotalWeight.Int(), false, true, false) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to calculate estimated PPM incentive: %w", err) } + return finalIncentive, nil + } else { + return nil, nil } - } else { - finalIncentive = nil } - - return finalIncentive, nil } // SumWeightTickets return the total weight of all weightTickets associated with a PPMShipment, returns 0 if there is no valid weight @@ -738,6 +740,49 @@ func (f estimatePPM) priceBreakdown(appCtx appcontext.AppContext, ppmShipment *m return linehaul, fuel, origin, dest, packing, unpacking, storage, nil } +// function for calculating incentives for OCONUS PPM shipments +// this uses a db function that takes in values needed to come up with the estimated/actual/max incentives +// this simulates the reimbursement for an iHHG move with ISLH, IHPK, IHUPK, and CONUS portion of FSC +func (f *estimatePPM) calculateOCONUSIncentive(appCtx appcontext.AppContext, ppmShipmentID uuid.UUID, pickupAddress models.Address, destinationAddress models.Address, moveDate time.Time, weight int, isEstimated bool, isActual bool, isMax bool) (*unit.Cents, error) { + var mileage int + ppmPort, err := models.FetchPortLocationByCode(appCtx.DB(), "3002") // Tacoma, WA port + if err != nil { + return nil, fmt.Errorf("failed to fetch port location: %w", err) + } + + // check if addresses are OCONUS or CONUS -> this determines how we check mileage to/from the authorized port + isPickupOconus := pickupAddress.IsOconus != nil && *pickupAddress.IsOconus + isDestinationOconus := destinationAddress.IsOconus != nil && *destinationAddress.IsOconus + + switch { + case isPickupOconus && isDestinationOconus: + // OCONUS -> OCONUS, we only reimburse for the CONUS mileage of the PPM + mileage = 0 + case isPickupOconus && !isDestinationOconus: + // OCONUS -> CONUS (port ZIP -> address ZIP) + mileage, err = f.planner.ZipTransitDistance(appCtx, ppmPort.UsPostRegionCity.UsprZipID, destinationAddress.PostalCode, true, true) + if err != nil { + return nil, fmt.Errorf("failed to calculate OCONUS to CONUS mileage: %w", err) + } + case !isPickupOconus && isDestinationOconus: + // CONUS -> OCONUS (address ZIP -> port ZIP) + mileage, err = f.planner.ZipTransitDistance(appCtx, pickupAddress.PostalCode, ppmPort.UsPostRegionCity.UsprZipID, true, true) + if err != nil { + return nil, fmt.Errorf("failed to calculate CONUS to OCONUS mileage: %w", err) + } + default: + // covering down on CONUS -> CONUS moves - they should not appear here + return nil, fmt.Errorf("invalid pickup and destination configuration: pickup isOconus=%v, destination isOconus=%v", isPickupOconus, isDestinationOconus) + } + + incentive, err := models.CalculatePPMIncentive(appCtx.DB(), ppmShipmentID, pickupAddress.ID, destinationAddress.ID, moveDate, mileage, weight, isEstimated, isActual, isMax) + if err != nil { + return nil, fmt.Errorf("failed to calculate PPM incentive: %w", err) + } + + return (*unit.Cents)(&incentive), nil +} + func CalculateSITCost(appCtx appcontext.AppContext, ppmShipment *models.PPMShipment, contract models.ReContract) (*unit.Cents, error) { logger := appCtx.Logger() diff --git a/pkg/services/ppmshipment/ppm_shipment_updater.go b/pkg/services/ppmshipment/ppm_shipment_updater.go index f8cc99b8d30..c52936e9e9f 100644 --- a/pkg/services/ppmshipment/ppm_shipment_updater.go +++ b/pkg/services/ppmshipment/ppm_shipment_updater.go @@ -117,66 +117,6 @@ func (f *ppmShipmentUpdater) updatePPMShipment(appCtx appcontext.AppContext, ppm } transactionError := appCtx.NewTransaction(func(txnAppCtx appcontext.AppContext) error { - // This potentially updates the MTOShipment.Distance field so include it in the transaction - estimatedIncentive, estimatedSITCost, err := f.estimator.EstimateIncentiveWithDefaultChecks(appCtx, *oldPPMShipment, updatedPPMShipment) - if err != nil { - return err - } - - updatedPPMShipment.EstimatedIncentive = estimatedIncentive - updatedPPMShipment.SITEstimatedCost = estimatedSITCost - - // if the PPM shipment is past closeout then we should not calculate the max incentive, it is already set in stone - if oldPPMShipment.Status != models.PPMShipmentStatusWaitingOnCustomer && - oldPPMShipment.Status != models.PPMShipmentStatusCloseoutComplete && - oldPPMShipment.Status != models.PPMShipmentStatusComplete && - oldPPMShipment.Status != models.PPMShipmentStatusNeedsCloseout { - maxIncentive, err := f.estimator.MaxIncentive(appCtx, *oldPPMShipment, updatedPPMShipment) - if err != nil { - return err - } - updatedPPMShipment.MaxIncentive = maxIncentive - } - - if appCtx.Session() != nil { - if appCtx.Session().IsOfficeUser() { - edited := models.PPMAdvanceStatusEdited - if oldPPMShipment.HasRequestedAdvance != nil && updatedPPMShipment.HasRequestedAdvance != nil { - if !*oldPPMShipment.HasRequestedAdvance && *updatedPPMShipment.HasRequestedAdvance { - updatedPPMShipment.AdvanceStatus = &edited - } else if *oldPPMShipment.HasRequestedAdvance && !*updatedPPMShipment.HasRequestedAdvance { - updatedPPMShipment.AdvanceStatus = &edited - } - } - if oldPPMShipment.AdvanceAmountRequested != nil && updatedPPMShipment.AdvanceAmountRequested != nil { - if *oldPPMShipment.AdvanceAmountRequested != *updatedPPMShipment.AdvanceAmountRequested { - updatedPPMShipment.AdvanceStatus = &edited - } - } - } - if appCtx.Session().IsMilApp() { - if isPrimeCounseled && updatedPPMShipment.HasRequestedAdvance != nil { - received := models.PPMAdvanceStatusReceived - notReceived := models.PPMAdvanceStatusNotReceived - - if updatedPPMShipment.HasReceivedAdvance != nil && *updatedPPMShipment.HasRequestedAdvance { - if *updatedPPMShipment.HasReceivedAdvance { - updatedPPMShipment.AdvanceStatus = &received - } - if !*updatedPPMShipment.HasReceivedAdvance { - updatedPPMShipment.AdvanceStatus = ¬Received - } - } - } - } - } - - finalIncentive, err := f.estimator.FinalIncentiveWithDefaultChecks(appCtx, *oldPPMShipment, updatedPPMShipment) - if err != nil { - return err - } - updatedPPMShipment.FinalIncentive = finalIncentive - if updatedPPMShipment.W2Address != nil { var updatedAddress *models.Address var createOrUpdateErr error @@ -282,6 +222,69 @@ func (f *ppmShipmentUpdater) updatePPMShipment(appCtx appcontext.AppContext, ppm updatedPPMShipment.TertiaryDestinationAddress = updatedAddress } + // if the actual move date is being provided, we no longer need to calculate the estimate - it has already happened + if updatedPPMShipment.ActualMoveDate == nil { + estimatedIncentive, estimatedSITCost, err := f.estimator.EstimateIncentiveWithDefaultChecks(appCtx, *oldPPMShipment, updatedPPMShipment) + if err != nil { + return err + } + updatedPPMShipment.EstimatedIncentive = estimatedIncentive + updatedPPMShipment.SITEstimatedCost = estimatedSITCost + } + + // if the PPM shipment is past closeout then we should not calculate the max incentive, it is already set in stone + if oldPPMShipment.Status != models.PPMShipmentStatusWaitingOnCustomer && + oldPPMShipment.Status != models.PPMShipmentStatusCloseoutComplete && + oldPPMShipment.Status != models.PPMShipmentStatusComplete && + oldPPMShipment.Status != models.PPMShipmentStatusNeedsCloseout { + maxIncentive, err := f.estimator.MaxIncentive(appCtx, *oldPPMShipment, updatedPPMShipment) + if err != nil { + return err + } + updatedPPMShipment.MaxIncentive = maxIncentive + } + + if appCtx.Session() != nil { + if appCtx.Session().IsOfficeUser() { + edited := models.PPMAdvanceStatusEdited + if oldPPMShipment.HasRequestedAdvance != nil && updatedPPMShipment.HasRequestedAdvance != nil { + if !*oldPPMShipment.HasRequestedAdvance && *updatedPPMShipment.HasRequestedAdvance { + updatedPPMShipment.AdvanceStatus = &edited + } else if *oldPPMShipment.HasRequestedAdvance && !*updatedPPMShipment.HasRequestedAdvance { + updatedPPMShipment.AdvanceStatus = &edited + } + } + if oldPPMShipment.AdvanceAmountRequested != nil && updatedPPMShipment.AdvanceAmountRequested != nil { + if *oldPPMShipment.AdvanceAmountRequested != *updatedPPMShipment.AdvanceAmountRequested { + updatedPPMShipment.AdvanceStatus = &edited + } + } + } + if appCtx.Session().IsMilApp() { + if isPrimeCounseled && updatedPPMShipment.HasRequestedAdvance != nil { + received := models.PPMAdvanceStatusReceived + notReceived := models.PPMAdvanceStatusNotReceived + + if updatedPPMShipment.HasReceivedAdvance != nil && *updatedPPMShipment.HasRequestedAdvance { + if *updatedPPMShipment.HasReceivedAdvance { + updatedPPMShipment.AdvanceStatus = &received + } + if !*updatedPPMShipment.HasReceivedAdvance { + updatedPPMShipment.AdvanceStatus = ¬Received + } + } + } + } + } + + if updatedPPMShipment.ActualMoveDate != nil { + finalIncentive, err := f.estimator.FinalIncentiveWithDefaultChecks(appCtx, *oldPPMShipment, updatedPPMShipment) + if err != nil { + return err + } + updatedPPMShipment.FinalIncentive = finalIncentive + } + verrs, err := appCtx.DB().ValidateAndUpdate(updatedPPMShipment) if verrs != nil && verrs.HasAny() { return apperror.NewInvalidInputError(updatedPPMShipment.ID, err, verrs, "Invalid input found while updating the PPMShipments.") From 9e80db22af3ef444bf85c7f09aef4979b82a2f8d Mon Sep 17 00:00:00 2001 From: Daniel Jordan Date: Wed, 15 Jan 2025 21:43:45 +0000 Subject: [PATCH 04/18] changin proc to return a table, working on closeout --- ...52_add_ppm_estimated_incentive_proc.up.sql | 46 ++++++++----------- .../internal/payloads/model_to_payload.go | 2 +- pkg/models/ppm_shipment.go | 25 +++++++--- pkg/services/ppm_closeout/ppm_closeout.go | 12 ++--- pkg/services/ppmshipment/ppm_estimator.go | 2 +- 5 files changed, 45 insertions(+), 42 deletions(-) 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 ce8be73a674..47436e133a9 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,5 +1,3 @@ --- function that calculates a ppm incentive given mileage, weight, and dates --- this is used to calculate estimated, max, and actual incentives CREATE OR REPLACE FUNCTION calculate_ppm_incentive( ppm_id UUID, pickup_address_id UUID, @@ -10,16 +8,16 @@ CREATE OR REPLACE FUNCTION calculate_ppm_incentive( is_estimated BOOLEAN, is_actual BOOLEAN, is_max BOOLEAN -) RETURNS NUMERIC AS +) RETURNS TABLE ( + total_incentive NUMERIC, + price_islh NUMERIC, + price_ihpk NUMERIC, + price_ihupk NUMERIC, + price_fsc NUMERIC +) AS $$ DECLARE ppm RECORD; - escalated_price NUMERIC; - estimated_price_islh NUMERIC; - estimated_price_ihpk NUMERIC; - estimated_price_ihupk NUMERIC; - estimated_price_fsc NUMERIC; - total_incentive NUMERIC := 0; contract_id UUID; o_rate_area_id UUID; d_rate_area_id UUID; @@ -34,12 +32,8 @@ 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 - SELECT ppms.id - INTO ppm - FROM ppm_shipments ppms - WHERE ppms.id = ppm_id; - + -- 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; END IF; @@ -61,7 +55,7 @@ BEGIN -- ISLH calculation SELECT rs.id INTO service_id FROM re_services rs WHERE rs.code = 'ISLH'; - estimated_price_islh := ROUND( + price_islh := ROUND( calculate_escalated_price( o_rate_area_id, d_rate_area_id, @@ -71,11 +65,10 @@ BEGIN move_date ) * (weight / 100)::NUMERIC * 100, 0 ); - RAISE NOTICE 'Estimated price for ISLH: % cents', estimated_price_islh; -- IHPK calculation SELECT rs.id INTO service_id FROM re_services rs WHERE rs.code = 'IHPK'; - estimated_price_ihpk := ROUND( + price_ihpk := ROUND( calculate_escalated_price( o_rate_area_id, NULL, @@ -85,11 +78,10 @@ BEGIN move_date ) * (weight / 100)::NUMERIC * 100, 0 ); - RAISE NOTICE 'Estimated price for IHPK: % cents', estimated_price_ihpk; -- IHUPK calculation SELECT rs.id INTO service_id FROM re_services rs WHERE rs.code = 'IHUPK'; - estimated_price_ihupk := ROUND( + price_ihupk := ROUND( calculate_escalated_price( NULL, d_rate_area_id, @@ -99,27 +91,25 @@ BEGIN move_date ) * (weight / 100)::NUMERIC * 100, 0 ); - RAISE NOTICE 'Estimated price for IHUPK: % cents', estimated_price_ihupk; -- FSC calculation estimated_fsc_multiplier := get_fsc_multiplier(weight); fuel_price := get_fuel_price(move_date); price_difference := calculate_price_difference(fuel_price); cents_above_baseline := mileage * estimated_fsc_multiplier; - estimated_price_fsc := ROUND((cents_above_baseline * price_difference) * 100); - RAISE NOTICE 'Estimated price for FSC: % cents', estimated_price_fsc; + price_fsc := ROUND((cents_above_baseline * price_difference) * 100); - -- total - total_incentive := estimated_price_islh + estimated_price_ihpk + estimated_price_ihupk + estimated_price_fsc; - RAISE NOTICE 'Total PPM Incentive: % cents', total_incentive; + -- Total incentive + total_incentive := price_islh + price_ihpk + price_ihupk + price_fsc; - -- now update the incentive value + -- 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 total_incentive; + -- Return all values + RETURN QUERY SELECT total_incentive, price_islh, price_ihpk, price_ihupk, price_fsc; END; $$ LANGUAGE plpgsql; diff --git a/pkg/handlers/ghcapi/internal/payloads/model_to_payload.go b/pkg/handlers/ghcapi/internal/payloads/model_to_payload.go index 08879e080a0..89a6290a7ff 100644 --- a/pkg/handlers/ghcapi/internal/payloads/model_to_payload.go +++ b/pkg/handlers/ghcapi/internal/payloads/model_to_payload.go @@ -1306,7 +1306,7 @@ func PPMCloseout(ppmCloseout *models.PPMCloseout) *ghcmessages.PPMCloseout { Gcc: handlers.FmtCost(ppmCloseout.GCC), Aoa: handlers.FmtCost(ppmCloseout.AOA), RemainingIncentive: handlers.FmtCost(ppmCloseout.RemainingIncentive), - HaulType: (*string)(&ppmCloseout.HaulType), + HaulType: (*string)(ppmCloseout.HaulType), HaulPrice: handlers.FmtCost(ppmCloseout.HaulPrice), HaulFSC: handlers.FmtCost(ppmCloseout.HaulFSC), Dop: handlers.FmtCost(ppmCloseout.DOP), diff --git a/pkg/models/ppm_shipment.go b/pkg/models/ppm_shipment.go index fab8720e125..7989b973119 100644 --- a/pkg/models/ppm_shipment.go +++ b/pkg/models/ppm_shipment.go @@ -35,11 +35,15 @@ type PPMCloseout struct { RemainingIncentive *unit.Cents HaulPrice *unit.Cents HaulFSC *unit.Cents - HaulType HaulType + HaulType *HaulType DOP *unit.Cents DDP *unit.Cents PackPrice *unit.Cents UnpackPrice *unit.Cents + IHPKPrice *unit.Cents + IHUPKPrice *unit.Cents + ISLHPrice *unit.Cents + FSCPrice *unit.Cents SITReimbursement *unit.Cents } @@ -321,16 +325,25 @@ func FetchPPMShipmentByPPMShipmentID(db *pop.Connection, ppmShipmentID uuid.UUID return &ppmShipment, nil } +type PPMIncentive struct { + TotalIncentive int `db:"total_incentive"` + PriceISLH int `db:"price_islh"` + PriceIHPK int `db:"price_ihpk"` + PriceIHUPK int `db:"price_ihupk"` + PriceFSC int `db:"price_fsc"` +} + // a db stored proc 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) (int, error) { - var incentive int +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 - err := db.RawQuery("SELECT calculate_ppm_incentive($1, $2, $3, $4, $5, $6, $7, $8, $9)", ppmID, pickupAddressID, destAddressID, moveDate, mileage, weight, isEstimated, isActual, isMax). + // 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 { - return 0, fmt.Errorf("error calculating PPM incentive for PPM ID %s: %w", ppmID, err) + return nil, fmt.Errorf("error calculating PPM incentive for PPM ID %s: %w", ppmID, err) } - return incentive, nil + return &incentive, nil } diff --git a/pkg/services/ppm_closeout/ppm_closeout.go b/pkg/services/ppm_closeout/ppm_closeout.go index 925385d079b..9deab808a96 100644 --- a/pkg/services/ppm_closeout/ppm_closeout.go +++ b/pkg/services/ppm_closeout/ppm_closeout.go @@ -60,11 +60,6 @@ func (p *ppmCloseoutFetcher) GetPPMCloseout(appCtx appcontext.AppContext, ppmShi proGearWeightCustomer, proGearWeightSpouse := p.GetProGearWeights(*ppmShipment) - serviceItems, err := p.getServiceItemPrices(appCtx, *ppmShipment) - if err != nil { - return nil, err - } - var remainingIncentive unit.Cents // Most moves generated by `make db_dev_e2e_populate` skip the part that generates the FinalIncentive for the move, so just return 0. // Moves created through the UI shouldn't be able to skip this part @@ -96,6 +91,11 @@ func (p *ppmCloseoutFetcher) GetPPMCloseout(appCtx appcontext.AppContext, ppmShi fullWeightGCCShipment.ProGearWeight = &proGearCustomerMax fullWeightGCCShipment.SpouseProGearWeight = &proGearSpouseMax gcc, _ := p.calculateGCC(appCtx, *fullWeightGCCShipment, fullAllowableWeight) + + serviceItems, err := p.getServiceItemPrices(appCtx, *ppmShipment) + if err != nil { + return nil, err + } if serviceItems.storageReimbursementCosts != nil { gcc = gcc.AddCents(*serviceItems.storageReimbursementCosts) } @@ -114,7 +114,7 @@ func (p *ppmCloseoutFetcher) GetPPMCloseout(appCtx appcontext.AppContext, ppmShi ppmCloseoutObj.RemainingIncentive = &remainingIncentive ppmCloseoutObj.HaulPrice = serviceItems.haulPrice ppmCloseoutObj.HaulFSC = serviceItems.haulFSC - ppmCloseoutObj.HaulType = serviceItems.haulType + ppmCloseoutObj.HaulType = &serviceItems.haulType ppmCloseoutObj.DOP = serviceItems.dop ppmCloseoutObj.DDP = serviceItems.ddp ppmCloseoutObj.PackPrice = serviceItems.packPrice diff --git a/pkg/services/ppmshipment/ppm_estimator.go b/pkg/services/ppmshipment/ppm_estimator.go index 971527632fc..81c76f99734 100644 --- a/pkg/services/ppmshipment/ppm_estimator.go +++ b/pkg/services/ppmshipment/ppm_estimator.go @@ -780,7 +780,7 @@ func (f *estimatePPM) calculateOCONUSIncentive(appCtx appcontext.AppContext, ppm return nil, fmt.Errorf("failed to calculate PPM incentive: %w", err) } - return (*unit.Cents)(&incentive), nil + return (*unit.Cents)(&incentive.TotalIncentive), nil } func CalculateSITCost(appCtx appcontext.AppContext, ppmShipment *models.PPMShipment, contract models.ReContract) (*unit.Cents, error) { From 2d64f350bbecf25a0f929aba0128fb6ee1fe15f3 Mon Sep 17 00:00:00 2001 From: Daniel Jordan Date: Fri, 17 Jan 2025 21:00:20 +0000 Subject: [PATCH 05/18] seem to have most of it working, need to refine and add tests, figure out UI display for iPPMs --- ...52_add_ppm_estimated_incentive_proc.up.sql | 136 +++++++++++++- pkg/gen/ghcapi/embedded_spec.go | 42 +++++ pkg/gen/ghcmessages/p_p_m_closeout.go | 9 + .../internal/payloads/model_to_payload.go | 3 + pkg/models/ppm_shipment.go | 29 ++- pkg/models/re_service_item.go | 12 ++ .../distance_zip_lookup.go | 53 ++++-- .../per_unit_cents_lookup.go | 153 ++++++++++++++-- .../port_zip_lookup.go | 14 +- .../service_param_value_lookups.go | 10 +- .../service_param_value_lookups_test.go | 2 +- pkg/services/ppm_closeout/ppm_closeout.go | 47 +++-- pkg/services/ppmshipment/ppm_estimator.go | 171 ++++++++++++++---- .../PPM/PPMHeaderSummary/HeaderSection.jsx | 121 +++++++++---- .../PPM/PPMHeaderSummary/PPMHeaderSummary.jsx | 5 + src/shared/constants.js | 5 + swagger-def/definitions/PPMCloseout.yaml | 36 ++-- swagger/ghc.yaml | 18 ++ 18 files changed, 713 insertions(+), 153 deletions(-) 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 From 4ef9c368e45f17959964626639826c9cb98e1da6 Mon Sep 17 00:00:00 2001 From: Daniel Jordan Date: Mon, 20 Jan 2025 14:32:32 +0000 Subject: [PATCH 06/18] some minor cleanup --- ...52_add_ppm_estimated_incentive_proc.up.sql | 6 ++ pkg/models/ppm_shipment.go | 6 +- pkg/services/ppm_closeout/ppm_closeout.go | 63 +++---------------- pkg/services/ppmshipment/ppm_estimator.go | 12 ++-- 4 files changed, 22 insertions(+), 65 deletions(-) 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 5aff4a61dd3..8b03c4fefed 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 @@ -22,10 +22,16 @@ INSERT INTO service_params (id,service_id,service_item_param_key_id,created_at,u DELETE FROM service_params WHERE service_item_param_key_id = '6d44624c-b91b-4226-8fcd-98046e2f433d'; +DELETE FROM service_item_param_keys +WHERE key = 'PriceAreaIntlOrigin'; + -- remove PriceAreaIntlDest, we don't need it DELETE FROM service_params WHERE service_item_param_key_id = '4736f489-dfda-4df1-a303-8c434a120d5d'; +DELETE FROM service_item_param_keys +WHERE key = 'PriceAreaIntlDest'; + -- 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 diff --git a/pkg/models/ppm_shipment.go b/pkg/models/ppm_shipment.go index 4506451053a..d53bdb5b3ac 100644 --- a/pkg/models/ppm_shipment.go +++ b/pkg/models/ppm_shipment.go @@ -324,7 +324,7 @@ func FetchPPMShipmentByPPMShipmentID(db *pop.Connection, ppmShipmentID uuid.UUID return &ppmShipment, nil } -type PPMIncentive struct { +type PPMIncentiveOCONUS struct { TotalIncentive int `db:"total_incentive"` PriceISLH int `db:"price_islh"` PriceIHPK int `db:"price_ihpk"` @@ -334,8 +334,8 @@ type PPMIncentive struct { // 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 +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) (*PPMIncentiveOCONUS, error) { + var incentive PPMIncentiveOCONUS 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) diff --git a/pkg/services/ppm_closeout/ppm_closeout.go b/pkg/services/ppm_closeout/ppm_closeout.go index 55707b91190..7ced6a8c257 100644 --- a/pkg/services/ppm_closeout/ppm_closeout.go +++ b/pkg/services/ppm_closeout/ppm_closeout.go @@ -255,53 +255,6 @@ func (p *ppmCloseoutFetcher) GetExpenseStoragePrice(appCtx appcontext.AppContext return storageExpensePrice, err } -func (p *ppmCloseoutFetcher) GetEntitlement(appCtx appcontext.AppContext, moveID uuid.UUID) (*models.Entitlement, error) { - var moveModel models.Move - err := appCtx.DB().EagerPreload( - "OrdersID", - ).Find(&moveModel, moveID) - - if err != nil { - switch err { - case sql.ErrNoRows: - return nil, apperror.NewNotFoundError(moveID, "while looking for Move") - default: - return nil, apperror.NewQueryError("Move", err, "unable to find Move") - } - } - - var order models.Order - orderID := &moveModel.OrdersID - errOrder := appCtx.DB().EagerPreload( - "EntitlementID", - ).Find(&order, orderID) - - if errOrder != nil { - switch errOrder { - case sql.ErrNoRows: - return nil, apperror.NewNotFoundError(*orderID, "while looking for Order") - default: - return nil, apperror.NewQueryError("Order", errOrder, "unable to find Order") - } - } - - var entitlement models.Entitlement - entitlementID := order.EntitlementID - errEntitlement := appCtx.DB().EagerPreload( - "DBAuthorizedWeight", - ).Find(&entitlement, entitlementID) - - if errEntitlement != nil { - switch errEntitlement { - case sql.ErrNoRows: - return nil, apperror.NewNotFoundError(*entitlementID, "while looking for Entitlement") - default: - return nil, apperror.NewQueryError("Entitlement", errEntitlement, "unable to find Entitlement") - } - } - return &entitlement, nil -} - func paramsForServiceCode(code models.ReServiceCode, serviceParams models.ServiceParams) models.ServiceParams { var serviceItemParams models.ServiceParams for _, serviceParam := range serviceParams { @@ -318,17 +271,12 @@ func (p *ppmCloseoutFetcher) getServiceItemPrices(appCtx appcontext.AppContext, var returnPriceObj serviceItemPrices logger := appCtx.Logger() - err := appCtx.DB().Where("mto_shipment_id = ?", ppmShipment.ShipmentID).All(&serviceItemsToPrice) - if err != nil { - return serviceItemPrices{}, err - } - isInternationalShipment := ppmShipment.Shipment.MarketCode == models.MarketCodeInternational serviceItemsToPrice = ppmshipment.BaseServiceItems(ppmShipment) actualPickupPostal := *ppmShipment.ActualPickupPostalCode actualDestPostal := *ppmShipment.ActualDestinationPostalCode - // Change DLH to DSH if move within same Zip3 + // Change DLH to DSH if move within same Zip3 (only for domestic shipments - intl uses ISLH) if !isInternationalShipment && actualPickupPostal[0:3] == actualDestPostal[0:3] { serviceItemsToPrice[0] = models.MTOServiceItem{ReService: models.ReService{Code: models.ReServiceCodeDSH}, MTOShipmentID: &ppmShipment.ShipmentID} } @@ -342,10 +290,12 @@ func (p *ppmCloseoutFetcher) getServiceItemPrices(appCtx appcontext.AppContext, if paramErr != nil { return serviceItemPrices{}, paramErr } + var totalPrice, packPrice, unpackPrice, destinationPrice, originPrice, haulPrice, haulFSC, intlPackPrice, intlUnpackPrice, intlLinehaulPrice unit.Cents var totalWeight unit.Pound var ppmToMtoShipment models.MTOShipment + // adding all the weight tickets together to get the total weight of the moved PPM if len(ppmShipment.WeightTickets) >= 1 { for _, weightTicket := range ppmShipment.WeightTickets { if weightTicket.Status != nil && *weightTicket.Status == models.PPMDocumentStatusRejected { @@ -380,6 +330,7 @@ func (p *ppmCloseoutFetcher) getServiceItemPrices(appCtx appcontext.AppContext, return serviceItemPrices{}, err } + // combo of domestic & int'l service items validCodes := map[models.ReServiceCode]string{ models.ReServiceCodeDPK: "DPK", models.ReServiceCodeDUPK: "DUPK", @@ -462,11 +413,11 @@ func (p *ppmCloseoutFetcher) getServiceItemPrices(appCtx appcontext.AppContext, totalPrice = totalPrice.AddCents(centsValue) switch serviceItem.ReService.Code { - case models.ReServiceCodeIHPK: + case models.ReServiceCodeIHPK: // Int'l pack intlPackPrice += centsValue - case models.ReServiceCodeIHUPK: + case models.ReServiceCodeIHUPK: // Int'l unpack intlUnpackPrice += centsValue - case models.ReServiceCodeISLH: + case models.ReServiceCodeISLH: // Int'l shipping & linehaul intlLinehaulPrice += centsValue case models.ReServiceCodeDPK: packPrice += centsValue diff --git a/pkg/services/ppmshipment/ppm_estimator.go b/pkg/services/ppmshipment/ppm_estimator.go index 04378ce97dc..c6890497e87 100644 --- a/pkg/services/ppmshipment/ppm_estimator.go +++ b/pkg/services/ppmshipment/ppm_estimator.go @@ -257,7 +257,7 @@ func (f *estimatePPM) estimateIncentive(appCtx appcontext.AppContext, oldPPMShip newPPMShipment.HasRequestedAdvance = nil newPPMShipment.AdvanceAmountRequested = nil - estimatedIncentive, err = f.calculateOCONUSIncentive(appCtx, newPPMShipment.ID, *pickupAddress, *destinationAddress, contractDate, newPPMShipment.EstimatedWeight.Int(), false, false, true) + 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) } @@ -274,7 +274,7 @@ func (f *estimatePPM) estimateIncentive(appCtx appcontext.AppContext, oldPPMShip 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) + 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) } @@ -330,7 +330,7 @@ func (f *estimatePPM) maxIncentive(appCtx appcontext.AppContext, oldPPMShipment pickupAddress := orders.OriginDutyLocation.Address destinationAddress := orders.NewDutyLocation.Address - maxIncentive, err := f.calculateOCONUSIncentive(appCtx, newPPMShipment.ID, pickupAddress, destinationAddress, contractDate, *orders.Entitlement.DBAuthorizedWeight, false, false, true) + maxIncentive, err := f.CalculateOCONUSIncentive(appCtx, newPPMShipment.ID, pickupAddress, destinationAddress, contractDate, *orders.Entitlement.DBAuthorizedWeight, false, false, true) if err != nil { return nil, fmt.Errorf("failed to calculate estimated PPM incentive: %w", err) } @@ -395,7 +395,7 @@ func (f *estimatePPM) finalIncentive(appCtx appcontext.AppContext, oldPPMShipmen // we can't calculate actual incentive without the weight if newTotalWeight != 0 { - finalIncentive, err := f.calculateOCONUSIncentive(appCtx, newPPMShipment.ID, *pickupAddress, *destinationAddress, contractDate, newTotalWeight.Int(), false, true, false) + finalIncentive, err := f.CalculateOCONUSIncentive(appCtx, newPPMShipment.ID, *pickupAddress, *destinationAddress, contractDate, newTotalWeight.Int(), false, true, false) if err != nil { return nil, fmt.Errorf("failed to calculate estimated PPM incentive: %w", err) } @@ -767,7 +767,7 @@ func (f estimatePPM) priceBreakdown(appCtx appcontext.AppContext, ppmShipment *m // function for calculating incentives for OCONUS PPM shipments // this uses a db function that takes in values needed to come up with the estimated/actual/max incentives // this simulates the reimbursement for an iHHG move with ISLH, IHPK, IHUPK, and CONUS portion of FSC -func (f *estimatePPM) calculateOCONUSIncentive(appCtx appcontext.AppContext, ppmShipmentID uuid.UUID, pickupAddress models.Address, destinationAddress models.Address, moveDate time.Time, weight int, isEstimated bool, isActual bool, isMax bool) (*unit.Cents, error) { +func (f *estimatePPM) CalculateOCONUSIncentive(appCtx appcontext.AppContext, ppmShipmentID uuid.UUID, pickupAddress models.Address, destinationAddress models.Address, moveDate time.Time, weight int, isEstimated bool, isActual bool, isMax bool) (*unit.Cents, error) { var mileage int ppmPort, err := models.FetchPortLocationByCode(appCtx.DB(), "3002") // Tacoma, WA port if err != nil { @@ -807,7 +807,7 @@ 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) { +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") } From 2f1bd06218f8a8e53aee5e4a76f8b7b2a7c9fdde Mon Sep 17 00:00:00 2001 From: Daniel Jordan Date: Mon, 20 Jan 2025 21:56:34 +0000 Subject: [PATCH 07/18] should all be working and good to go, need to add tests and cleanup --- ...52_add_ppm_estimated_incentive_proc.up.sql | 11 +- pkg/models/re_intl_other_price.go | 34 ++ .../port_zip_lookup.go | 3 +- pkg/services/ghc_rate_engine.go | 32 ++ ..._destination_additional_days_sit_pricer.go | 50 +++ .../intl_destination_first_day_sit_pricer.go | 45 +++ .../intl_origin_additional_days_sit_pricer.go | 50 +++ .../intl_origin_first_day_sit_pricer.go | 45 +++ .../ghcrateengine/pricer_helpers_intl.go | 108 ++++++ .../ghcrateengine/service_item_pricer.go | 8 + pkg/services/ppmshipment/ppm_estimator.go | 350 +++++++++++++----- .../PPM/SitCostBreakdown/SitCostBreakdown.jsx | 28 +- 12 files changed, 652 insertions(+), 112 deletions(-) create mode 100644 pkg/services/ghcrateengine/intl_destination_additional_days_sit_pricer.go create mode 100644 pkg/services/ghcrateengine/intl_destination_first_day_sit_pricer.go create mode 100644 pkg/services/ghcrateengine/intl_origin_additional_days_sit_pricer.go create mode 100644 pkg/services/ghcrateengine/intl_origin_first_day_sit_pricer.go 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 8b03c4fefed..4aecb8a3656 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,20 +1,21 @@ --- inserting params for IDFSIT +-- IDFSIT PerUnitCents 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 +-- IDASIT PerUnitCents 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 +-- IOFSIT PerUnitCents 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 +-- IOASIT PerUnitCents 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 +-- inserting PortZip param for FSC +-- we need this for international PPMs since they only get reimbursed for the CONUS -> Port portion 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 diff --git a/pkg/models/re_intl_other_price.go b/pkg/models/re_intl_other_price.go index b8dce673214..b49efa94712 100644 --- a/pkg/models/re_intl_other_price.go +++ b/pkg/models/re_intl_other_price.go @@ -1,6 +1,7 @@ package models import ( + "fmt" "time" "github.com/gobuffalo/pop/v6" @@ -45,3 +46,36 @@ func (r *ReIntlOtherPrice) Validate(_ *pop.Connection) (*validate.Errors, error) &validators.IntIsGreaterThan{Field: r.PerUnitCents.Int(), Name: "PerUnitCents", Compared: -1}, ), nil } + +// fetches a row from re_intl_other_prices using passed in parameters +// gets the rate_area_id & is_peak_period based on values provided +func FetchReIntlOtherPrice(db *pop.Connection, addressID uuid.UUID, serviceID uuid.UUID, contractID uuid.UUID, referenceDate *time.Time) (*ReIntlOtherPrice, error) { + if addressID != uuid.Nil && serviceID != uuid.Nil && contractID != uuid.Nil && referenceDate != nil { + // need to get the rate area first + rateAreaID, err := FetchRateAreaID(db, addressID, serviceID, contractID) + if err != nil { + return nil, fmt.Errorf("error fetching rate area id for shipment ID: %s, service ID %s, and contract ID: %s: %s", addressID, serviceID, contractID, err) + } + + var isPeakPeriod bool + err = db.RawQuery("SELECT is_peak_period($1)", referenceDate).First(&isPeakPeriod) + if err != nil { + return nil, fmt.Errorf("error checking if date is peak period with date: %s: %s", contractID, err) + } + + var reIntlOtherPrice ReIntlOtherPrice + err = db.Q(). + Where("contract_id = ?", contractID). + Where("service_id = ?", serviceID). + Where("is_peak_period = ?", isPeakPeriod). + Where("rate_area_id = ?", rateAreaID). + First(&reIntlOtherPrice) + if err != nil { + return nil, fmt.Errorf("error fetching row from re_int_other_prices using rateAreaID %s, service ID %s, and contract ID: %s: %s", rateAreaID, serviceID, contractID, err) + } + + return &reIntlOtherPrice, nil + } + + return nil, fmt.Errorf("error value from re_intl_other_prices - required parameters not provided") +} 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 bf4971f9db2..bade82c04b9 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 @@ -33,8 +33,9 @@ func (p PortZipLookup) lookup(appCtx appcontext.AppContext, keyData *ServiceItem return "", fmt.Errorf("unable to find port zip with code %s", "3002") } return portLocation.UsPostRegionCity.UsprZipID, nil + } else { + return "", nil } - return "", fmt.Errorf("unable to find port zip for service item id: %s", p.ServiceItem.ID) } var portLocation models.PortLocation err := appCtx.DB().Q(). diff --git a/pkg/services/ghc_rate_engine.go b/pkg/services/ghc_rate_engine.go index 2247e3d7426..052956c0c0b 100644 --- a/pkg/services/ghc_rate_engine.go +++ b/pkg/services/ghc_rate_engine.go @@ -264,3 +264,35 @@ type IntlPortFuelSurchargePricer interface { Price(appCtx appcontext.AppContext, actualPickupDate time.Time, distance unit.Miles, weight unit.Pound, fscWeightBasedDistanceMultiplier float64, eiaFuelPrice unit.Millicents) (unit.Cents, PricingDisplayParams, error) ParamsPricer } + +// IntlOriginFirstDaySITPricer prices international origin first day SIT +// +//go:generate mockery --name IntlOriginFirstDaySITPricer +type IntlOriginFirstDaySITPricer interface { + Price(appCtx appcontext.AppContext, contractCode string, requestedPickupDate time.Time, weight unit.Pound, perUnitCents int) (unit.Cents, PricingDisplayParams, error) + ParamsPricer +} + +// IntlOriginAdditionalDaySITPricer prices international origin additional days of SIT +// +//go:generate mockery --name IntlOriginAdditionalDaySITPricer +type IntlOriginAdditionalDaySITPricer interface { + Price(appCtx appcontext.AppContext, contractCode string, requestedPickupDate time.Time, numberOfDaysInSIT int, weight unit.Pound, perUnitCents int) (unit.Cents, PricingDisplayParams, error) + ParamsPricer +} + +// IntlDestinationFirstDaySITPricer prices international destination first day SIT +// +//go:generate mockery --name IntlDestinationFirstDaySITPricer +type IntlDestinationFirstDaySITPricer interface { + Price(appCtx appcontext.AppContext, contractCode string, requestedPickupDate time.Time, weight unit.Pound, perUnitCents int) (unit.Cents, PricingDisplayParams, error) + ParamsPricer +} + +// IntlDestinationAdditionalDaySITPricer prices international destination additional days of SIT +// +//go:generate mockery --name IntlDestinationAdditionalDaySITPricer +type IntlDestinationAdditionalDaySITPricer interface { + Price(appCtx appcontext.AppContext, contractCode string, requestedPickupDate time.Time, numberOfDaysInSIT int, weight unit.Pound, perUnitCents int) (unit.Cents, PricingDisplayParams, error) + ParamsPricer +} diff --git a/pkg/services/ghcrateengine/intl_destination_additional_days_sit_pricer.go b/pkg/services/ghcrateengine/intl_destination_additional_days_sit_pricer.go new file mode 100644 index 00000000000..465099cf73f --- /dev/null +++ b/pkg/services/ghcrateengine/intl_destination_additional_days_sit_pricer.go @@ -0,0 +1,50 @@ +package ghcrateengine + +import ( + "time" + + "github.com/transcom/mymove/pkg/appcontext" + "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/services" + "github.com/transcom/mymove/pkg/unit" +) + +type intlDestinationAdditionalDaySITPricer struct { +} + +func NewIntlDestinationAdditionalDaySITPricer() services.IntlDestinationAdditionalDaySITPricer { + return &intlDestinationAdditionalDaySITPricer{} +} + +func (p intlDestinationAdditionalDaySITPricer) Price(appCtx appcontext.AppContext, contractCode string, referenceDate time.Time, numberOfDaysInSIT int, weight unit.Pound, perUnitCents int) (unit.Cents, services.PricingDisplayParams, error) { + return priceIntlAdditionalDaySIT(appCtx, models.ReServiceCodeIDASIT, contractCode, referenceDate, numberOfDaysInSIT, weight, perUnitCents) +} + +func (p intlDestinationAdditionalDaySITPricer) PriceUsingParams(appCtx appcontext.AppContext, params models.PaymentServiceItemParams) (unit.Cents, services.PricingDisplayParams, error) { + contractCode, err := getParamString(params, models.ServiceItemParamNameContractCode) + if err != nil { + return unit.Cents(0), nil, err + } + + numberOfDaysInSIT, err := getParamInt(params, models.ServiceItemParamNameNumberDaysSIT) + if err != nil { + return unit.Cents(0), nil, err + } + + referenceDate, err := getParamTime(params, models.ServiceItemParamNameReferenceDate) + if err != nil { + return unit.Cents(0), nil, err + } + + perUnitCents, err := getParamInt(params, models.ServiceItemParamNamePerUnitCents) + if err != nil { + return unit.Cents(0), nil, err + } + + weightBilled, err := getParamInt(params, models.ServiceItemParamNameWeightBilled) + if err != nil { + return unit.Cents(0), nil, err + } + + return p.Price(appCtx, contractCode, referenceDate, numberOfDaysInSIT, unit.Pound(weightBilled), perUnitCents) +} diff --git a/pkg/services/ghcrateengine/intl_destination_first_day_sit_pricer.go b/pkg/services/ghcrateengine/intl_destination_first_day_sit_pricer.go new file mode 100644 index 00000000000..eb1354ebbd0 --- /dev/null +++ b/pkg/services/ghcrateengine/intl_destination_first_day_sit_pricer.go @@ -0,0 +1,45 @@ +package ghcrateengine + +import ( + "time" + + "github.com/transcom/mymove/pkg/appcontext" + "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/services" + "github.com/transcom/mymove/pkg/unit" +) + +type intlDestinationFirstDaySITPricer struct { +} + +func NewIntlDestinationFirstDaySITPricer() services.IntlDestinationFirstDaySITPricer { + return &intlDestinationFirstDaySITPricer{} +} + +func (p intlDestinationFirstDaySITPricer) Price(appCtx appcontext.AppContext, contractCode string, referenceDate time.Time, weight unit.Pound, perUnitCents int) (unit.Cents, services.PricingDisplayParams, error) { + return priceIntlFirstDaySIT(appCtx, models.ReServiceCodeIDFSIT, contractCode, referenceDate, weight, perUnitCents) +} + +func (p intlDestinationFirstDaySITPricer) PriceUsingParams(appCtx appcontext.AppContext, params models.PaymentServiceItemParams) (unit.Cents, services.PricingDisplayParams, error) { + contractCode, err := getParamString(params, models.ServiceItemParamNameContractCode) + if err != nil { + return unit.Cents(0), nil, err + } + + referenceDate, err := getParamTime(params, models.ServiceItemParamNameReferenceDate) + if err != nil { + return unit.Cents(0), nil, err + } + + perUnitCents, err := getParamInt(params, models.ServiceItemParamNamePerUnitCents) + if err != nil { + return unit.Cents(0), nil, err + } + + weightBilled, err := getParamInt(params, models.ServiceItemParamNameWeightBilled) + if err != nil { + return unit.Cents(0), nil, err + } + + return p.Price(appCtx, contractCode, referenceDate, unit.Pound(weightBilled), perUnitCents) +} diff --git a/pkg/services/ghcrateengine/intl_origin_additional_days_sit_pricer.go b/pkg/services/ghcrateengine/intl_origin_additional_days_sit_pricer.go new file mode 100644 index 00000000000..e9b4dc22478 --- /dev/null +++ b/pkg/services/ghcrateengine/intl_origin_additional_days_sit_pricer.go @@ -0,0 +1,50 @@ +package ghcrateengine + +import ( + "time" + + "github.com/transcom/mymove/pkg/appcontext" + "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/services" + "github.com/transcom/mymove/pkg/unit" +) + +type intlOriginAdditionalDaySITPricer struct { +} + +func NewIntlOriginAdditionalDaySITPricer() services.IntlOriginAdditionalDaySITPricer { + return &intlOriginAdditionalDaySITPricer{} +} + +func (p intlOriginAdditionalDaySITPricer) Price(appCtx appcontext.AppContext, contractCode string, referenceDate time.Time, numberOfDaysInSIT int, weight unit.Pound, perUnitCents int) (unit.Cents, services.PricingDisplayParams, error) { + return priceIntlAdditionalDaySIT(appCtx, models.ReServiceCodeIOASIT, contractCode, referenceDate, numberOfDaysInSIT, weight, perUnitCents) +} + +func (p intlOriginAdditionalDaySITPricer) PriceUsingParams(appCtx appcontext.AppContext, params models.PaymentServiceItemParams) (unit.Cents, services.PricingDisplayParams, error) { + contractCode, err := getParamString(params, models.ServiceItemParamNameContractCode) + if err != nil { + return unit.Cents(0), nil, err + } + + referenceDate, err := getParamTime(params, models.ServiceItemParamNameReferenceDate) + if err != nil { + return unit.Cents(0), nil, err + } + + numberOfDaysInSIT, err := getParamInt(params, models.ServiceItemParamNameNumberDaysSIT) + if err != nil { + return unit.Cents(0), nil, err + } + + perUnitCents, err := getParamInt(params, models.ServiceItemParamNamePerUnitCents) + if err != nil { + return unit.Cents(0), nil, err + } + + weightBilled, err := getParamInt(params, models.ServiceItemParamNameWeightBilled) + if err != nil { + return unit.Cents(0), nil, err + } + + return p.Price(appCtx, contractCode, referenceDate, numberOfDaysInSIT, unit.Pound(weightBilled), perUnitCents) +} diff --git a/pkg/services/ghcrateengine/intl_origin_first_day_sit_pricer.go b/pkg/services/ghcrateengine/intl_origin_first_day_sit_pricer.go new file mode 100644 index 00000000000..2070d13835b --- /dev/null +++ b/pkg/services/ghcrateengine/intl_origin_first_day_sit_pricer.go @@ -0,0 +1,45 @@ +package ghcrateengine + +import ( + "time" + + "github.com/transcom/mymove/pkg/appcontext" + "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/services" + "github.com/transcom/mymove/pkg/unit" +) + +type intlOriginFirstDaySITPricer struct { +} + +func NewIntlOriginFirstDaySITPricer() services.IntlOriginFirstDaySITPricer { + return &intlOriginFirstDaySITPricer{} +} + +func (p intlOriginFirstDaySITPricer) Price(appCtx appcontext.AppContext, contractCode string, referenceDate time.Time, weight unit.Pound, perUnitCents int) (unit.Cents, services.PricingDisplayParams, error) { + return priceIntlFirstDaySIT(appCtx, models.ReServiceCodeIOFSIT, contractCode, referenceDate, weight, perUnitCents) +} + +func (p intlOriginFirstDaySITPricer) PriceUsingParams(appCtx appcontext.AppContext, params models.PaymentServiceItemParams) (unit.Cents, services.PricingDisplayParams, error) { + contractCode, err := getParamString(params, models.ServiceItemParamNameContractCode) + if err != nil { + return unit.Cents(0), nil, err + } + + referenceDate, err := getParamTime(params, models.ServiceItemParamNameReferenceDate) + if err != nil { + return unit.Cents(0), nil, err + } + + perUnitCents, err := getParamInt(params, models.ServiceItemParamNamePerUnitCents) + if err != nil { + return unit.Cents(0), nil, err + } + + weightBilled, err := getParamInt(params, models.ServiceItemParamNameWeightBilled) + if err != nil { + return unit.Cents(0), nil, err + } + + return p.Price(appCtx, contractCode, referenceDate, unit.Pound(weightBilled), perUnitCents) +} diff --git a/pkg/services/ghcrateengine/pricer_helpers_intl.go b/pkg/services/ghcrateengine/pricer_helpers_intl.go index 924dad55537..bb49759504f 100644 --- a/pkg/services/ghcrateengine/pricer_helpers_intl.go +++ b/pkg/services/ghcrateengine/pricer_helpers_intl.go @@ -64,3 +64,111 @@ func priceIntlPackUnpack(appCtx appcontext.AppContext, packUnpackCode models.ReS return totalCost, displayParams, nil } + +func priceIntlFirstDaySIT(appCtx appcontext.AppContext, firstDaySITCode models.ReServiceCode, contractCode string, referenceDate time.Time, weight unit.Pound, perUnitCents int) (unit.Cents, services.PricingDisplayParams, error) { + if firstDaySITCode != models.ReServiceCodeIOFSIT && firstDaySITCode != models.ReServiceCodeIDFSIT { + return 0, nil, fmt.Errorf("unsupported pack/unpack code of %s", firstDaySITCode) + } + if len(contractCode) == 0 { + return 0, nil, errors.New("ContractCode is required") + } + if referenceDate.IsZero() { + return 0, nil, errors.New("ReferenceDate is required") + } + if perUnitCents == 0 { + return 0, nil, errors.New("PerUnitCents is required") + } + + isPeakPeriod := IsPeakPeriod(referenceDate) + + contract, err := fetchContractByContractCode(appCtx, contractCode) + if err != nil { + return 0, nil, fmt.Errorf("could not find contract with code: %s: %w", contractCode, err) + } + + basePrice := float64(perUnitCents) + escalatedPrice, contractYear, err := escalatePriceForContractYear(appCtx, contract.ID, referenceDate, false, basePrice) + if err != nil { + return 0, nil, fmt.Errorf("could not calculate escalated price: %w", err) + } + + escalatedPrice = escalatedPrice * weight.ToCWTFloat64() + totalCost := unit.Cents(math.Round(escalatedPrice)) + + displayParams := services.PricingDisplayParams{ + { + Key: models.ServiceItemParamNameContractYearName, + Value: contractYear.Name, + }, + { + Key: models.ServiceItemParamNamePriceRateOrFactor, + Value: FormatCents(unit.Cents(perUnitCents)), + }, + { + Key: models.ServiceItemParamNameIsPeak, + Value: FormatBool(isPeakPeriod), + }, + { + Key: models.ServiceItemParamNameEscalationCompounded, + Value: FormatEscalation(contractYear.EscalationCompounded), + }, + } + + return totalCost, displayParams, nil +} + +func priceIntlAdditionalDaySIT(appCtx appcontext.AppContext, additionalDaySITCode models.ReServiceCode, contractCode string, referenceDate time.Time, numberOfDaysInSIT int, weight unit.Pound, perUnitCents int) (unit.Cents, services.PricingDisplayParams, error) { + if additionalDaySITCode != models.ReServiceCodeIOASIT && additionalDaySITCode != models.ReServiceCodeIDASIT { + return 0, nil, fmt.Errorf("unsupported additional day of SIT code of %s", additionalDaySITCode) + } + if len(contractCode) == 0 { + return 0, nil, errors.New("ContractCode is required") + } + if referenceDate.IsZero() { + return 0, nil, errors.New("ReferenceDate is required") + } + if numberOfDaysInSIT == 0 { + return 0, nil, errors.New("NumberDaysSIT is required") + } + if perUnitCents == 0 { + return 0, nil, errors.New("PerUnitCents is required") + } + + isPeakPeriod := IsPeakPeriod(referenceDate) + + contract, err := fetchContractByContractCode(appCtx, contractCode) + if err != nil { + return 0, nil, fmt.Errorf("could not find contract with code: %s: %w", contractCode, err) + } + + basePrice := float64(perUnitCents) + escalatedPrice, contractYear, err := escalatePriceForContractYear(appCtx, contract.ID, referenceDate, false, basePrice) + if err != nil { + return 0, nil, fmt.Errorf("could not calculate escalated price: %w", err) + } + + escalatedPrice = escalatedPrice * weight.ToCWTFloat64() + totalForNumberOfDaysPrice := escalatedPrice * float64(numberOfDaysInSIT) + totalCost := unit.Cents(math.Round(totalForNumberOfDaysPrice)) + + displayParams := services.PricingDisplayParams{ + { + Key: models.ServiceItemParamNameContractYearName, + Value: contractYear.Name, + }, + { + Key: models.ServiceItemParamNamePriceRateOrFactor, + Value: FormatCents(unit.Cents(perUnitCents)), + }, + { + Key: models.ServiceItemParamNameIsPeak, + Value: FormatBool(isPeakPeriod), + }, + { + Key: models.ServiceItemParamNameEscalationCompounded, + Value: FormatEscalation(contractYear.EscalationCompounded), + }, + } + + return totalCost, displayParams, nil +} diff --git a/pkg/services/ghcrateengine/service_item_pricer.go b/pkg/services/ghcrateengine/service_item_pricer.go index a673f832b63..9f34aea64e4 100644 --- a/pkg/services/ghcrateengine/service_item_pricer.go +++ b/pkg/services/ghcrateengine/service_item_pricer.go @@ -103,6 +103,14 @@ func PricerForServiceItem(serviceCode models.ReServiceCode) (services.ParamsPric return NewPortFuelSurchargePricer(), nil case models.ReServiceCodePODFSC: return NewPortFuelSurchargePricer(), nil + case models.ReServiceCodeIOFSIT: + return NewIntlOriginFirstDaySITPricer(), nil + case models.ReServiceCodeIOASIT: + return NewIntlOriginAdditionalDaySITPricer(), nil + case models.ReServiceCodeIDFSIT: + return NewIntlDestinationFirstDaySITPricer(), nil + case models.ReServiceCodeIDASIT: + return NewIntlDestinationAdditionalDaySITPricer(), nil default: // TODO: We may want a different error type here after all pricers have been implemented return nil, apperror.NewNotImplementedError(fmt.Sprintf("pricer not found for code %s", serviceCode)) diff --git a/pkg/services/ppmshipment/ppm_estimator.go b/pkg/services/ppmshipment/ppm_estimator.go index c6890497e87..1d3d4916f43 100644 --- a/pkg/services/ppmshipment/ppm_estimator.go +++ b/pkg/services/ppmshipment/ppm_estimator.go @@ -825,39 +825,56 @@ func (f *estimatePPM) CalculateOCONUSSITCosts(appCtx appcontext.AppContext, ppmI } 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, *ppmShipment.SITLocation, additionalDaysInSIT) + if ppmShipment.Shipment.MarketCode != models.MarketCodeInternational { + logger := appCtx.Logger() - totalPrice := unit.Cents(0) - for _, serviceItem := range serviceItemsToPrice { - pricer, err := ghcrateengine.PricerForServiceItem(serviceItem.ReService.Code) - if err != nil { - logger.Error("unable to find pricer for service item", zap.Error(err)) - return nil, err + serviceItemsToPrice := StorageServiceItems(*ppmShipment, *ppmShipment.SITLocation, additionalDaysInSIT) + + totalPrice := unit.Cents(0) + for _, serviceItem := range serviceItemsToPrice { + pricer, err := ghcrateengine.PricerForServiceItem(serviceItem.ReService.Code) + if err != nil { + logger.Error("unable to find pricer for service item", zap.Error(err)) + return nil, err + } + + var price *unit.Cents + switch serviceItemPricer := pricer.(type) { + case services.DomesticOriginFirstDaySITPricer, services.DomesticDestinationFirstDaySITPricer: + price, _, err = priceFirstDaySIT(appCtx, serviceItemPricer, serviceItem, ppmShipment, contract) + case services.DomesticOriginAdditionalDaysSITPricer, services.DomesticDestinationAdditionalDaysSITPricer: + price, _, err = priceAdditionalDaySIT(appCtx, serviceItemPricer, serviceItem, ppmShipment, additionalDaysInSIT, contract) + default: + return nil, fmt.Errorf("unknown SIT pricer type found for service item code %s", serviceItem.ReService.Code) + } + + if err != nil { + return nil, err + } + + logger.Debug(fmt.Sprintf("Price of service item %s %d", serviceItem.ReService.Code, *price)) + totalPrice += *price } - var price *unit.Cents - switch serviceItemPricer := pricer.(type) { - case services.DomesticOriginFirstDaySITPricer, services.DomesticDestinationFirstDaySITPricer: - price, _, err = priceFirstDaySIT(appCtx, serviceItemPricer, serviceItem, ppmShipment, contract) - case services.DomesticOriginAdditionalDaysSITPricer, services.DomesticDestinationAdditionalDaysSITPricer: - price, _, err = priceAdditionalDaySIT(appCtx, serviceItemPricer, serviceItem, ppmShipment, additionalDaysInSIT, contract) - default: - return nil, fmt.Errorf("unknown SIT pricer type found for service item code %s", serviceItem.ReService.Code) + return &totalPrice, nil + } else { + var sitAddress models.Address + isOrigin := *ppmShipment.SITLocation == models.SITLocationTypeOrigin + if isOrigin { + sitAddress = *ppmShipment.PickupAddress + } else { + sitAddress = *ppmShipment.DestinationAddress } + contractDate := ppmShipment.ExpectedDepartureDate + totalSITCost, err := models.CalculatePPMSITCost(appCtx.DB(), ppmShipment.ID, sitAddress.ID, isOrigin, contractDate, ppmShipment.SITEstimatedWeight.Int(), additionalDaysInSIT) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to calculate PPM SIT incentive: %w", err) } - - logger.Debug(fmt.Sprintf("Price of service item %s %d", serviceItem.ReService.Code, *price)) - totalPrice += *price + return (*unit.Cents)(&totalSITCost.TotalSITCost), nil } - - return &totalPrice, nil } func CalculateSITCostBreakdown(appCtx appcontext.AppContext, ppmShipment *models.PPMShipment, contract models.ReContract) (*models.PPMSITEstimatedCostInfo, error) { @@ -881,8 +898,12 @@ func CalculateSITCostBreakdown(appCtx appcontext.AppContext, ppmShipment *models switch serviceItemPricer := pricer.(type) { case services.DomesticOriginFirstDaySITPricer, services.DomesticDestinationFirstDaySITPricer: price, ppmSITEstimatedCostInfoData, err = calculateFirstDaySITCostBreakdown(appCtx, serviceItemPricer, serviceItem, ppmShipment, contract, ppmSITEstimatedCostInfoData, logger) + case services.IntlOriginFirstDaySITPricer, services.IntlDestinationFirstDaySITPricer: + price, ppmSITEstimatedCostInfoData, err = calculateIntlFirstDaySITCostBreakdown(appCtx, serviceItemPricer, serviceItem, ppmShipment, contract, ppmSITEstimatedCostInfoData, logger) case services.DomesticOriginAdditionalDaysSITPricer, services.DomesticDestinationAdditionalDaysSITPricer: price, ppmSITEstimatedCostInfoData, err = calculateAdditionalDaySITCostBreakdown(appCtx, serviceItemPricer, serviceItem, ppmShipment, contract, additionalDaysInSIT, ppmSITEstimatedCostInfoData, logger) + case services.IntlOriginAdditionalDaySITPricer, services.IntlDestinationAdditionalDaySITPricer: + price, ppmSITEstimatedCostInfoData, err = calculateIntlAdditionalDaySITCostBreakdown(appCtx, serviceItemPricer, serviceItem, ppmShipment, contract, additionalDaysInSIT, ppmSITEstimatedCostInfoData, logger) default: return nil, fmt.Errorf("unknown SIT pricer type found for service item code %s", serviceItem.ReService.Code) } @@ -926,6 +947,33 @@ func calculateFirstDaySITCostBreakdown(appCtx appcontext.AppContext, serviceItem return price, ppmSITEstimatedCostInfoData, nil } +func calculateIntlFirstDaySITCostBreakdown(appCtx appcontext.AppContext, serviceItemPricer services.ParamsPricer, serviceItem models.MTOServiceItem, ppmShipment *models.PPMShipment, contract models.ReContract, ppmSITEstimatedCostInfoData *models.PPMSITEstimatedCostInfo, logger *zap.Logger) (*unit.Cents, *models.PPMSITEstimatedCostInfo, error) { + price, priceParams, err := priceFirstDaySIT(appCtx, serviceItemPricer, serviceItem, ppmShipment, contract) + if err != nil { + return nil, nil, err + } + ppmSITEstimatedCostInfoData.PriceFirstDaySIT = price + for _, param := range priceParams { + switch param.Key { + case models.ServiceItemParamNameServiceAreaOrigin: + ppmSITEstimatedCostInfoData.ParamsFirstDaySIT.ServiceAreaOrigin = param.Value + case models.ServiceItemParamNameServiceAreaDest: + ppmSITEstimatedCostInfoData.ParamsFirstDaySIT.ServiceAreaDestination = param.Value + case models.ServiceItemParamNameIsPeak: + ppmSITEstimatedCostInfoData.ParamsFirstDaySIT.IsPeak = param.Value + case models.ServiceItemParamNameContractYearName: + ppmSITEstimatedCostInfoData.ParamsFirstDaySIT.ContractYearName = param.Value + case models.ServiceItemParamNamePriceRateOrFactor: + ppmSITEstimatedCostInfoData.ParamsFirstDaySIT.PriceRateOrFactor = param.Value + case models.ServiceItemParamNameEscalationCompounded: + ppmSITEstimatedCostInfoData.ParamsFirstDaySIT.EscalationCompounded = param.Value + default: + logger.Debug(fmt.Sprintf("Unexpected ServiceItemParam in PPM First Day SIT: %s, %s", param.Key, param.Value)) + } + } + return price, ppmSITEstimatedCostInfoData, nil +} + func calculateAdditionalDaySITCostBreakdown(appCtx appcontext.AppContext, serviceItemPricer services.ParamsPricer, serviceItem models.MTOServiceItem, ppmShipment *models.PPMShipment, contract models.ReContract, additionalDaysInSIT int, ppmSITEstimatedCostInfoData *models.PPMSITEstimatedCostInfo, logger *zap.Logger) (*unit.Cents, *models.PPMSITEstimatedCostInfo, error) { price, priceParams, err := priceAdditionalDaySIT(appCtx, serviceItemPricer, serviceItem, ppmShipment, additionalDaysInSIT, contract) if err != nil { @@ -955,56 +1003,119 @@ func calculateAdditionalDaySITCostBreakdown(appCtx appcontext.AppContext, servic return price, ppmSITEstimatedCostInfoData, nil } -func priceFirstDaySIT(appCtx appcontext.AppContext, pricer services.ParamsPricer, serviceItem models.MTOServiceItem, ppmShipment *models.PPMShipment, contract models.ReContract) (*unit.Cents, services.PricingDisplayParams, error) { - firstDayPricer, ok := pricer.(services.DomesticFirstDaySITPricer) - if !ok { - return nil, nil, errors.New("ppm estimate pricer for SIT service item does not implement the first day pricer interface") - } - - // Need to declare if origin or destination for the serviceAreaLookup, otherwise we already have it - serviceAreaPostalCode := ppmShipment.PickupAddress.PostalCode - serviceAreaKey := models.ServiceItemParamNameServiceAreaOrigin - if serviceItem.ReService.Code == models.ReServiceCodeDDFSIT { - serviceAreaPostalCode = ppmShipment.DestinationAddress.PostalCode - serviceAreaKey = models.ServiceItemParamNameServiceAreaDest - } - - serviceAreaLookup := serviceparamvaluelookups.ServiceAreaLookup{ - Address: models.Address{PostalCode: serviceAreaPostalCode}, - } - serviceArea, err := serviceAreaLookup.ParamValue(appCtx, contract.Code) +func calculateIntlAdditionalDaySITCostBreakdown(appCtx appcontext.AppContext, serviceItemPricer services.ParamsPricer, serviceItem models.MTOServiceItem, ppmShipment *models.PPMShipment, contract models.ReContract, additionalDaysInSIT int, ppmSITEstimatedCostInfoData *models.PPMSITEstimatedCostInfo, logger *zap.Logger) (*unit.Cents, *models.PPMSITEstimatedCostInfo, error) { + price, priceParams, err := priceAdditionalDaySIT(appCtx, serviceItemPricer, serviceItem, ppmShipment, additionalDaysInSIT, contract) if err != nil { return nil, nil, err } - - serviceAreaParam := services.PricingDisplayParam{ - Key: serviceAreaKey, - Value: serviceArea, + ppmSITEstimatedCostInfoData.PriceAdditionalDaySIT = price + for _, param := range priceParams { + switch param.Key { + case models.ServiceItemParamNameServiceAreaOrigin: + ppmSITEstimatedCostInfoData.ParamsAdditionalDaySIT.ServiceAreaOrigin = param.Value + case models.ServiceItemParamNameServiceAreaDest: + ppmSITEstimatedCostInfoData.ParamsAdditionalDaySIT.ServiceAreaDestination = param.Value + case models.ServiceItemParamNameIsPeak: + ppmSITEstimatedCostInfoData.ParamsAdditionalDaySIT.IsPeak = param.Value + case models.ServiceItemParamNameContractYearName: + ppmSITEstimatedCostInfoData.ParamsAdditionalDaySIT.ContractYearName = param.Value + case models.ServiceItemParamNamePriceRateOrFactor: + ppmSITEstimatedCostInfoData.ParamsAdditionalDaySIT.PriceRateOrFactor = param.Value + case models.ServiceItemParamNameEscalationCompounded: + ppmSITEstimatedCostInfoData.ParamsAdditionalDaySIT.EscalationCompounded = param.Value + case models.ServiceItemParamNameNumberDaysSIT: + ppmSITEstimatedCostInfoData.ParamsAdditionalDaySIT.NumberDaysSIT = param.Value + default: + logger.Debug(fmt.Sprintf("Unexpected ServiceItemParam in PPM Additional Day SIT: %s, %s", param.Key, param.Value)) + } } + return price, ppmSITEstimatedCostInfoData, nil +} - // Since this function may be ran before closeout, we need to account for if there's no actual move date yet. - if ppmShipment.ActualMoveDate != nil { - price, pricingParams, err := firstDayPricer.Price(appCtx, contract.Code, *ppmShipment.ActualMoveDate, *ppmShipment.SITEstimatedWeight, serviceArea, true) +func priceFirstDaySIT(appCtx appcontext.AppContext, pricer services.ParamsPricer, serviceItem models.MTOServiceItem, ppmShipment *models.PPMShipment, contract models.ReContract) (*unit.Cents, services.PricingDisplayParams, error) { + if serviceItem.ReService.Code == models.ReServiceCodeIOFSIT || serviceItem.ReService.Code == models.ReServiceCodeIDFSIT { + var addressID uuid.UUID + if serviceItem.ReService.Code == models.ReServiceCodeIOFSIT { + addressID = *ppmShipment.PickupAddressID + } else { + addressID = *ppmShipment.DestinationAddressID + } + reServiceID, _ := models.FetchReServiceByCode(appCtx.DB(), serviceItem.ReService.Code) + intlOtherPrice, _ := models.FetchReIntlOtherPrice(appCtx.DB(), addressID, reServiceID.ID, contract.ID, &ppmShipment.ExpectedDepartureDate) + firstDayPricer, ok := pricer.(services.IntlOriginFirstDaySITPricer) + if !ok { + return nil, nil, errors.New("ppm estimate pricer for SIT service item does not implement the first day pricer interface") + } + if ppmShipment.ActualMoveDate != nil { + price, pricingParams, err := firstDayPricer.Price(appCtx, contract.Code, *ppmShipment.ActualMoveDate, *ppmShipment.SITEstimatedWeight, intlOtherPrice.PerUnitCents.Int()) + if err != nil { + return nil, nil, err + } + + appCtx.Logger().Debug(fmt.Sprintf("Pricing params for first day SIT %+v", pricingParams), zap.String("shipmentId", ppmShipment.ShipmentID.String())) + + return &price, pricingParams, nil + } + + price, pricingParams, err := firstDayPricer.Price(appCtx, contract.Code, ppmShipment.ExpectedDepartureDate, *ppmShipment.SITEstimatedWeight, intlOtherPrice.PerUnitCents.Int()) if err != nil { return nil, nil, err } - pricingParams = append(pricingParams, serviceAreaParam) - appCtx.Logger().Debug(fmt.Sprintf("Pricing params for first day SIT %+v", pricingParams), zap.String("shipmentId", ppmShipment.ShipmentID.String())) return &price, pricingParams, nil - } - price, pricingParams, err := firstDayPricer.Price(appCtx, contract.Code, ppmShipment.ExpectedDepartureDate, *ppmShipment.SITEstimatedWeight, serviceArea, true) - if err != nil { - return nil, nil, err - } + } else { + firstDayPricer, ok := pricer.(services.DomesticFirstDaySITPricer) + if !ok { + return nil, nil, errors.New("ppm estimate pricer for SIT service item does not implement the first day pricer interface") + } + + // Need to declare if origin or destination for the serviceAreaLookup, otherwise we already have it + serviceAreaPostalCode := ppmShipment.PickupAddress.PostalCode + serviceAreaKey := models.ServiceItemParamNameServiceAreaOrigin + if serviceItem.ReService.Code == models.ReServiceCodeDDFSIT { + serviceAreaPostalCode = ppmShipment.DestinationAddress.PostalCode + serviceAreaKey = models.ServiceItemParamNameServiceAreaDest + } + + serviceAreaLookup := serviceparamvaluelookups.ServiceAreaLookup{ + Address: models.Address{PostalCode: serviceAreaPostalCode}, + } + serviceArea, err := serviceAreaLookup.ParamValue(appCtx, contract.Code) + if err != nil { + return nil, nil, err + } + + serviceAreaParam := services.PricingDisplayParam{ + Key: serviceAreaKey, + Value: serviceArea, + } + + // Since this function may be ran before closeout, we need to account for if there's no actual move date yet. + if ppmShipment.ActualMoveDate != nil { + price, pricingParams, err := firstDayPricer.Price(appCtx, contract.Code, *ppmShipment.ActualMoveDate, *ppmShipment.SITEstimatedWeight, serviceArea, true) + if err != nil { + return nil, nil, err + } - pricingParams = append(pricingParams, serviceAreaParam) + pricingParams = append(pricingParams, serviceAreaParam) - appCtx.Logger().Debug(fmt.Sprintf("Pricing params for first day SIT %+v", pricingParams), zap.String("shipmentId", ppmShipment.ShipmentID.String())) + appCtx.Logger().Debug(fmt.Sprintf("Pricing params for first day SIT %+v", pricingParams), zap.String("shipmentId", ppmShipment.ShipmentID.String())) - return &price, pricingParams, nil + return &price, pricingParams, nil + } + price, pricingParams, err := firstDayPricer.Price(appCtx, contract.Code, ppmShipment.ExpectedDepartureDate, *ppmShipment.SITEstimatedWeight, serviceArea, true) + if err != nil { + return nil, nil, err + } + + pricingParams = append(pricingParams, serviceAreaParam) + + appCtx.Logger().Debug(fmt.Sprintf("Pricing params for first day SIT %+v", pricingParams), zap.String("shipmentId", ppmShipment.ShipmentID.String())) + + return &price, pricingParams, nil + } } func additionalDaysInSIT(sitEntryDate time.Time, sitDepartureDate time.Time) int { @@ -1018,60 +1129,103 @@ func additionalDaysInSIT(sitEntryDate time.Time, sitDepartureDate time.Time) int } func priceAdditionalDaySIT(appCtx appcontext.AppContext, pricer services.ParamsPricer, serviceItem models.MTOServiceItem, ppmShipment *models.PPMShipment, additionalDaysInSIT int, contract models.ReContract) (*unit.Cents, services.PricingDisplayParams, error) { - additionalDaysPricer, ok := pricer.(services.DomesticAdditionalDaysSITPricer) - if !ok { - return nil, nil, errors.New("ppm estimate pricer for SIT service item does not implement the additional days pricer interface") - } + // international shipment logic + if serviceItem.ReService.Code == models.ReServiceCodeIOASIT || serviceItem.ReService.Code == models.ReServiceCodeIDASIT { + // address we need for the per_unit_cents is dependent on if it's origin/destination SIT + var addressID uuid.UUID + if serviceItem.ReService.Code == models.ReServiceCodeIOASIT { + addressID = *ppmShipment.PickupAddressID + } else { + addressID = *ppmShipment.DestinationAddressID + } - // Need to declare if origin or destination for the serviceAreaLookup, otherwise we already have it - serviceAreaPostalCode := ppmShipment.PickupAddress.PostalCode - serviceAreaKey := models.ServiceItemParamNameServiceAreaOrigin - if serviceItem.ReService.Code == models.ReServiceCodeDDASIT { - serviceAreaPostalCode = ppmShipment.DestinationAddress.PostalCode - serviceAreaKey = models.ServiceItemParamNameServiceAreaDest - } - serviceAreaLookup := serviceparamvaluelookups.ServiceAreaLookup{ - Address: models.Address{PostalCode: serviceAreaPostalCode}, - } + var moveDate time.Time + if ppmShipment.ActualMoveDate != nil { + moveDate = *ppmShipment.ActualMoveDate + } else { + moveDate = ppmShipment.ExpectedDepartureDate + } - serviceArea, err := serviceAreaLookup.ParamValue(appCtx, contract.Code) - if err != nil { - return nil, nil, err - } + reServiceID, _ := models.FetchReServiceByCode(appCtx.DB(), serviceItem.ReService.Code) + intlOtherPrice, _ := models.FetchReIntlOtherPrice(appCtx.DB(), addressID, reServiceID.ID, contract.ID, &moveDate) - serviceAreaParam := services.PricingDisplayParam{ - Key: serviceAreaKey, - Value: serviceArea, - } + sitDaysParam := services.PricingDisplayParam{ + Key: models.ServiceItemParamNameNumberDaysSIT, + Value: strconv.Itoa(additionalDaysInSIT), + } - sitDaysParam := services.PricingDisplayParam{ - Key: models.ServiceItemParamNameNumberDaysSIT, - Value: strconv.Itoa(additionalDaysInSIT), - } + additionalDayPricer, ok := pricer.(services.IntlOriginAdditionalDaySITPricer) + if !ok { + return nil, nil, errors.New("ppm estimate pricer for SIT service item does not implement the first day pricer interface") + } - // Since this function may be ran before closeout, we need to account for if there's no actual move date yet. - if ppmShipment.ActualMoveDate != nil { - price, pricingParams, err := additionalDaysPricer.Price(appCtx, contract.Code, *ppmShipment.ActualMoveDate, *ppmShipment.SITEstimatedWeight, serviceArea, additionalDaysInSIT, true) + price, pricingParams, err := additionalDayPricer.Price(appCtx, contract.Code, moveDate, additionalDaysInSIT, *ppmShipment.SITEstimatedWeight, intlOtherPrice.PerUnitCents.Int()) if err != nil { return nil, nil, err } - pricingParams = append(pricingParams, serviceAreaParam, sitDaysParam) + pricingParams = append(pricingParams, sitDaysParam) appCtx.Logger().Debug(fmt.Sprintf("Pricing params for additional day SIT %+v", pricingParams), zap.String("shipmentId", ppmShipment.ShipmentID.String())) return &price, pricingParams, nil - } - price, pricingParams, err := additionalDaysPricer.Price(appCtx, contract.Code, ppmShipment.ExpectedDepartureDate, *ppmShipment.SITEstimatedWeight, serviceArea, additionalDaysInSIT, true) - if err != nil { - return nil, nil, err - } + } else { + // domestic PPMs + additionalDaysPricer, ok := pricer.(services.DomesticAdditionalDaysSITPricer) + if !ok { + return nil, nil, errors.New("ppm estimate pricer for SIT service item does not implement the additional days pricer interface") + } + + // Need to declare if origin or destination for the serviceAreaLookup, otherwise we already have it + serviceAreaPostalCode := ppmShipment.PickupAddress.PostalCode + serviceAreaKey := models.ServiceItemParamNameServiceAreaOrigin + if serviceItem.ReService.Code == models.ReServiceCodeDDASIT { + serviceAreaPostalCode = ppmShipment.DestinationAddress.PostalCode + serviceAreaKey = models.ServiceItemParamNameServiceAreaDest + } + serviceAreaLookup := serviceparamvaluelookups.ServiceAreaLookup{ + Address: models.Address{PostalCode: serviceAreaPostalCode}, + } + + serviceArea, err := serviceAreaLookup.ParamValue(appCtx, contract.Code) + if err != nil { + return nil, nil, err + } + + serviceAreaParam := services.PricingDisplayParam{ + Key: serviceAreaKey, + Value: serviceArea, + } + + sitDaysParam := services.PricingDisplayParam{ + Key: models.ServiceItemParamNameNumberDaysSIT, + Value: strconv.Itoa(additionalDaysInSIT), + } - pricingParams = append(pricingParams, serviceAreaParam, sitDaysParam) + // Since this function may be ran before closeout, we need to account for if there's no actual move date yet. + if ppmShipment.ActualMoveDate != nil { + price, pricingParams, err := additionalDaysPricer.Price(appCtx, contract.Code, *ppmShipment.ActualMoveDate, *ppmShipment.SITEstimatedWeight, serviceArea, additionalDaysInSIT, true) + if err != nil { + return nil, nil, err + } - appCtx.Logger().Debug(fmt.Sprintf("Pricing params for additional day SIT %+v", pricingParams), zap.String("shipmentId", ppmShipment.ShipmentID.String())) + pricingParams = append(pricingParams, serviceAreaParam, sitDaysParam) - return &price, pricingParams, nil + appCtx.Logger().Debug(fmt.Sprintf("Pricing params for additional day SIT %+v", pricingParams), zap.String("shipmentId", ppmShipment.ShipmentID.String())) + + return &price, pricingParams, nil + } + price, pricingParams, err := additionalDaysPricer.Price(appCtx, contract.Code, ppmShipment.ExpectedDepartureDate, *ppmShipment.SITEstimatedWeight, serviceArea, additionalDaysInSIT, true) + if err != nil { + return nil, nil, err + } + + pricingParams = append(pricingParams, serviceAreaParam, sitDaysParam) + + appCtx.Logger().Debug(fmt.Sprintf("Pricing params for additional day SIT %+v", pricingParams), zap.String("shipmentId", ppmShipment.ShipmentID.String())) + + return &price, pricingParams, nil + } } // mapPPMShipmentEstimatedFields remaps our PPMShipment specific information into the fields where the service param lookups @@ -1083,9 +1237,9 @@ func MapPPMShipmentEstimatedFields(appCtx appcontext.AppContext, ppmShipment mod ppmShipment.Shipment.ActualPickupDate = &ppmShipment.ExpectedDepartureDate ppmShipment.Shipment.RequestedPickupDate = &ppmShipment.ExpectedDepartureDate ppmShipment.Shipment.PickupAddress = ppmShipment.PickupAddress - ppmShipment.Shipment.PickupAddress = &models.Address{PostalCode: *ppmShipment.ActualPickupPostalCode} + ppmShipment.Shipment.PickupAddress = &models.Address{PostalCode: ppmShipment.PickupAddress.PostalCode} ppmShipment.Shipment.DestinationAddress = ppmShipment.DestinationAddress - ppmShipment.Shipment.DestinationAddress = &models.Address{PostalCode: *ppmShipment.ActualDestinationPostalCode} + ppmShipment.Shipment.DestinationAddress = &models.Address{PostalCode: ppmShipment.DestinationAddress.PostalCode} ppmShipment.Shipment.PrimeActualWeight = ppmShipment.EstimatedWeight return ppmShipment.Shipment, nil diff --git a/src/components/Office/PPM/SitCostBreakdown/SitCostBreakdown.jsx b/src/components/Office/PPM/SitCostBreakdown/SitCostBreakdown.jsx index f275eb0f3a6..0b6412ddfe4 100644 --- a/src/components/Office/PPM/SitCostBreakdown/SitCostBreakdown.jsx +++ b/src/components/Office/PPM/SitCostBreakdown/SitCostBreakdown.jsx @@ -24,6 +24,11 @@ export default function SitCostBreakdown({ actualWeight, ); + const isEitherAddressOconus = (ppm) => { + return ppm?.destinationAddress?.isOconus || ppm?.pickupAddress?.isOconus; + }; + const isInternationalShipment = isEitherAddressOconus(ppmShipmentInfo); + setEstimatedCost(estimatedCost?.sitCost || 0); return (
@@ -36,18 +41,25 @@ export default function SitCostBreakdown({ SIT Information:
-
- - {estimatedCost.paramsFirstDaySIT.serviceAreaOrigin - ? `Origin service area: ${estimatedCost?.paramsFirstDaySIT.serviceAreaOrigin}` - : `Destination service area: ${estimatedCost?.paramsFirstDaySIT.serviceAreaDestination}`} - -
+ {(estimatedCost.paramsFirstDaySIT.serviceAreaOrigin || + estimatedCost.paramsFirstDaySIT.serviceAreaDestination) && ( +
+ + {estimatedCost.paramsFirstDaySIT.serviceAreaOrigin + ? `Origin service area: ${estimatedCost.paramsFirstDaySIT.serviceAreaOrigin}` + : `Destination service area: ${estimatedCost.paramsFirstDaySIT.serviceAreaDestination}`} + +
+ )}
Actual move date: {formatDate(ppmShipmentInfo.actualMoveDate)}
- {estimatedCost.paramsFirstDaySIT.isPeak ? 'Domestic peak' : 'Domestic non-peak'} + + {isInternationalShipment + ? `${estimatedCost.paramsFirstDaySIT.isPeak ? 'International peak' : 'International non-peak'}` + : `${estimatedCost.paramsFirstDaySIT.isPeak ? 'Domestic peak' : 'Domestic non-peak'}`} +
From 0332de784a3b4cb65ad093ace8b8a45ebc71cae5 Mon Sep 17 00:00:00 2001 From: Daniel Jordan Date: Mon, 20 Jan 2025 23:02:17 +0000 Subject: [PATCH 08/18] tests for model package added --- .../payloads/model_to_payload_test.go | 77 +++++++++ pkg/models/port_location_test.go | 26 +++ pkg/models/ppm_shipment_test.go | 159 ++++++++++++++++++ pkg/models/re_intl_other_price_test.go | 77 +++++++++ pkg/models/re_service.go | 16 ++ pkg/models/re_service_item.go | 12 -- pkg/models/re_service_test.go | 16 ++ 7 files changed, 371 insertions(+), 12 deletions(-) diff --git a/pkg/handlers/ghcapi/internal/payloads/model_to_payload_test.go b/pkg/handlers/ghcapi/internal/payloads/model_to_payload_test.go index 7e201ca5212..ac3c3ac9f65 100644 --- a/pkg/handlers/ghcapi/internal/payloads/model_to_payload_test.go +++ b/pkg/handlers/ghcapi/internal/payloads/model_to_payload_test.go @@ -1354,3 +1354,80 @@ func (suite *PayloadsSuite) TestMTOShipment_POE_POD_Locations() { suite.Nil(payload.PoeLocation, "Expected PODLocation to be nil when PODLocation is set") }) } + +func (suite *PayloadsSuite) TestPPMCloseout() { + plannedMoveDate := time.Now() + actualMoveDate := time.Now() + miles := 1200 + estimatedWeight := unit.Pound(5000) + actualWeight := unit.Pound(5200) + proGearWeightCustomer := unit.Pound(300) + proGearWeightSpouse := unit.Pound(100) + grossIncentive := unit.Cents(100000) + gcc := unit.Cents(50000) + aoa := unit.Cents(20000) + remainingIncentive := unit.Cents(30000) + haulType := "Linehaul" + haulPrice := unit.Cents(40000) + haulFSC := unit.Cents(5000) + dop := unit.Cents(10000) + ddp := unit.Cents(8000) + packPrice := unit.Cents(7000) + unpackPrice := unit.Cents(6000) + intlPackPrice := unit.Cents(15000) + intlUnpackPrice := unit.Cents(14000) + intlLinehaulPrice := unit.Cents(13000) + sitReimbursement := unit.Cents(12000) + + ppmCloseout := models.PPMCloseout{ + ID: models.UUIDPointer(uuid.Must(uuid.NewV4())), + PlannedMoveDate: &plannedMoveDate, + ActualMoveDate: &actualMoveDate, + Miles: &miles, + EstimatedWeight: &estimatedWeight, + ActualWeight: &actualWeight, + ProGearWeightCustomer: &proGearWeightCustomer, + ProGearWeightSpouse: &proGearWeightSpouse, + GrossIncentive: &grossIncentive, + GCC: &gcc, + AOA: &aoa, + RemainingIncentive: &remainingIncentive, + HaulType: (*models.HaulType)(&haulType), + HaulPrice: &haulPrice, + HaulFSC: &haulFSC, + DOP: &dop, + DDP: &ddp, + PackPrice: &packPrice, + UnpackPrice: &unpackPrice, + IntlPackPrice: &intlPackPrice, + IntlUnpackPrice: &intlUnpackPrice, + IntlLinehaulPrice: &intlLinehaulPrice, + SITReimbursement: &sitReimbursement, + } + + payload := PPMCloseout(&ppmCloseout) + suite.NotNil(payload) + suite.Equal(ppmCloseout.ID.String(), payload.ID.String()) + suite.Equal(handlers.FmtDatePtr(ppmCloseout.PlannedMoveDate), payload.PlannedMoveDate) + suite.Equal(handlers.FmtDatePtr(ppmCloseout.ActualMoveDate), payload.ActualMoveDate) + suite.Equal(handlers.FmtIntPtrToInt64(ppmCloseout.Miles), payload.Miles) + suite.Equal(handlers.FmtPoundPtr(ppmCloseout.EstimatedWeight), payload.EstimatedWeight) + suite.Equal(handlers.FmtPoundPtr(ppmCloseout.ActualWeight), payload.ActualWeight) + suite.Equal(handlers.FmtPoundPtr(ppmCloseout.ProGearWeightCustomer), payload.ProGearWeightCustomer) + suite.Equal(handlers.FmtPoundPtr(ppmCloseout.ProGearWeightSpouse), payload.ProGearWeightSpouse) + suite.Equal(handlers.FmtCost(ppmCloseout.GrossIncentive), payload.GrossIncentive) + suite.Equal(handlers.FmtCost(ppmCloseout.GCC), payload.Gcc) + suite.Equal(handlers.FmtCost(ppmCloseout.AOA), payload.Aoa) + suite.Equal(handlers.FmtCost(ppmCloseout.RemainingIncentive), payload.RemainingIncentive) + suite.Equal((*string)(ppmCloseout.HaulType), payload.HaulType) + suite.Equal(handlers.FmtCost(ppmCloseout.HaulPrice), payload.HaulPrice) + suite.Equal(handlers.FmtCost(ppmCloseout.HaulFSC), payload.HaulFSC) + suite.Equal(handlers.FmtCost(ppmCloseout.DOP), payload.Dop) + suite.Equal(handlers.FmtCost(ppmCloseout.DDP), payload.Ddp) + suite.Equal(handlers.FmtCost(ppmCloseout.PackPrice), payload.PackPrice) + suite.Equal(handlers.FmtCost(ppmCloseout.UnpackPrice), payload.UnpackPrice) + suite.Equal(handlers.FmtCost(ppmCloseout.IntlPackPrice), payload.IntlPackPrice) + suite.Equal(handlers.FmtCost(ppmCloseout.IntlUnpackPrice), payload.IntlUnpackPrice) + suite.Equal(handlers.FmtCost(ppmCloseout.IntlLinehaulPrice), payload.IntlLinehaulPrice) + suite.Equal(handlers.FmtCost(ppmCloseout.SITReimbursement), payload.SITReimbursement) +} diff --git a/pkg/models/port_location_test.go b/pkg/models/port_location_test.go index c63a4e34e29..ae50c68880f 100644 --- a/pkg/models/port_location_test.go +++ b/pkg/models/port_location_test.go @@ -2,6 +2,7 @@ package models_test import ( "github.com/transcom/mymove/pkg/factory" + "github.com/transcom/mymove/pkg/models" ) func (suite *ModelSuite) TestPortLocation() { @@ -24,3 +25,28 @@ func (suite *ModelSuite) TestPortLocation() { suite.Equal("port_locations", portLocation.TableName()) }) } + +func (suite *ModelSuite) TestFetchPortLocationByCode() { + suite.Run("Port location can be fetched when it exists", func() { + + portLocation := factory.FetchPortLocation(suite.DB(), []factory.Customization{ + { + Model: models.Port{ + PortCode: "SEA", + }, + }, + }, nil) + suite.NotNil(portLocation) + + result, err := models.FetchPortLocationByCode(suite.AppContextForTest().DB(), "SEA") + suite.NotNil(result) + suite.NoError(err) + suite.Equal(portLocation.ID, result.ID) + }) + + suite.Run("Sends back an error when it does not exist", func() { + result, err := models.FetchPortLocationByCode(suite.AppContextForTest().DB(), "123") + suite.Nil(result) + suite.Error(err) + }) +} diff --git a/pkg/models/ppm_shipment_test.go b/pkg/models/ppm_shipment_test.go index 4def2567968..7dbeb27f545 100644 --- a/pkg/models/ppm_shipment_test.go +++ b/pkg/models/ppm_shipment_test.go @@ -7,6 +7,7 @@ import ( "github.com/gofrs/uuid" + "github.com/transcom/mymove/pkg/factory" "github.com/transcom/mymove/pkg/models" "github.com/transcom/mymove/pkg/testdatagen" "github.com/transcom/mymove/pkg/unit" @@ -125,3 +126,161 @@ func (suite *ModelSuite) TestPPMShipmentValidation() { }) } } + +func (suite *ModelSuite) TestCalculatePPMIncentive() { + suite.Run("success - receive PPM incentive when all values exist", func() { + ppmShipment := factory.BuildPPMShipment(suite.DB(), nil, nil) + pickupUSPRC, err := models.FindByZipCode(suite.AppContextForTest().DB(), "74135") + suite.FatalNoError(err) + pickupAddress := factory.BuildAddress(suite.DB(), []factory.Customization{ + { + Model: models.Address{ + StreetAddress1: "Tester Address", + City: "Tulsa", + State: "OK", + PostalCode: "74133", + IsOconus: models.BoolPointer(false), + UsPostRegionCityID: &pickupUSPRC.ID, + }, + }, + }, nil) + + destUSPRC, err := models.FindByZipCode(suite.AppContextForTest().DB(), "99505") + suite.FatalNoError(err) + destAddress := factory.BuildAddress(suite.DB(), []factory.Customization{ + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "JBER", + State: "AK", + PostalCode: "99505", + IsOconus: models.BoolPointer(true), + UsPostRegionCityID: &destUSPRC.ID, + }, + }, + }, nil) + + testdatagen.FetchOrMakeReContractYear(suite.DB(), testdatagen.Assertions{ + ReContractYear: models.ReContractYear{ + StartDate: time.Now().Add(-24 * time.Hour), + EndDate: time.Now().Add(24 * time.Hour), + }, + }) + moveDate := time.Now() + mileage := 1000 + weight := 2000 + + incentives, err := models.CalculatePPMIncentive(suite.DB(), ppmShipment.ID, pickupAddress.ID, destAddress.ID, moveDate, mileage, weight, true, false, false) + suite.NoError(err) + suite.NotNil(incentives) + suite.NotNil(incentives.PriceFSC) + suite.NotNil(incentives.PriceIHPK) + suite.NotNil(incentives.PriceIHUPK) + suite.NotNil(incentives.PriceISLH) + suite.NotNil(incentives.TotalIncentive) + }) + + suite.Run("failure - contract doesn't exist", func() { + ppmShipment := factory.BuildPPMShipment(suite.DB(), nil, nil) + pickupUSPRC, err := models.FindByZipCode(suite.AppContextForTest().DB(), "74135") + suite.FatalNoError(err) + pickupAddress := factory.BuildAddress(suite.DB(), []factory.Customization{ + { + Model: models.Address{ + StreetAddress1: "Tester Address", + City: "Tulsa", + State: "OK", + PostalCode: "74133", + IsOconus: models.BoolPointer(false), + UsPostRegionCityID: &pickupUSPRC.ID, + }, + }, + }, nil) + + destUSPRC, err := models.FindByZipCode(suite.AppContextForTest().DB(), "99505") + suite.FatalNoError(err) + destAddress := factory.BuildAddress(suite.DB(), []factory.Customization{ + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "JBER", + State: "AK", + PostalCode: "99505", + IsOconus: models.BoolPointer(true), + UsPostRegionCityID: &destUSPRC.ID, + }, + }, + }, nil) + + moveDate := time.Now() + mileage := 1000 + weight := 2000 + + incentives, err := models.CalculatePPMIncentive(suite.DB(), ppmShipment.ID, pickupAddress.ID, destAddress.ID, moveDate, mileage, weight, true, false, false) + suite.Error(err) + suite.Nil(incentives) + }) +} + +func (suite *ModelSuite) TestCalculatePPMSITCost() { + suite.Run("success - receive PPM SIT costs when all values exist", func() { + ppmShipment := factory.BuildPPMShipment(suite.DB(), nil, nil) + destUSPRC, err := models.FindByZipCode(suite.AppContextForTest().DB(), "99505") + suite.FatalNoError(err) + address := factory.BuildAddress(suite.DB(), []factory.Customization{ + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "JBER", + State: "AK", + PostalCode: "99505", + IsOconus: models.BoolPointer(true), + UsPostRegionCityID: &destUSPRC.ID, + }, + }, + }, nil) + + testdatagen.FetchOrMakeReContractYear(suite.DB(), testdatagen.Assertions{ + ReContractYear: models.ReContractYear{ + StartDate: time.Now().Add(-24 * time.Hour), + EndDate: time.Now().Add(24 * time.Hour), + }, + }) + moveDate := time.Now() + sitDays := 7 + weight := 2000 + + sitCost, err := models.CalculatePPMSITCost(suite.DB(), ppmShipment.ID, address.ID, false, moveDate, weight, sitDays) + suite.NoError(err) + suite.NotNil(sitCost) + suite.NotNil(sitCost.PriceAddlDaySIT) + suite.NotNil(sitCost.PriceFirstDaySIT) + suite.NotNil(sitCost.TotalSITCost) + }) + + suite.Run("failure - contract doesn't exist", func() { + ppmShipment := factory.BuildPPMShipment(suite.DB(), nil, nil) + destUSPRC, err := models.FindByZipCode(suite.AppContextForTest().DB(), "99505") + suite.FatalNoError(err) + address := factory.BuildAddress(suite.DB(), []factory.Customization{ + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "JBER", + State: "AK", + PostalCode: "99505", + IsOconus: models.BoolPointer(true), + UsPostRegionCityID: &destUSPRC.ID, + }, + }, + }, nil) + + moveDate := time.Now() + sitDays := 7 + weight := 2000 + + sitCost, err := models.CalculatePPMSITCost(suite.DB(), ppmShipment.ID, address.ID, false, moveDate, weight, sitDays) + suite.Error(err) + suite.Nil(sitCost) + }) +} diff --git a/pkg/models/re_intl_other_price_test.go b/pkg/models/re_intl_other_price_test.go index 69e9770c47a..8cde04c61d4 100644 --- a/pkg/models/re_intl_other_price_test.go +++ b/pkg/models/re_intl_other_price_test.go @@ -1,9 +1,13 @@ package models_test import ( + "time" + "github.com/gofrs/uuid" + "github.com/transcom/mymove/pkg/factory" "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/testdatagen" ) func (suite *ModelSuite) TestReIntlOtherPriceValidation() { @@ -43,3 +47,76 @@ func (suite *ModelSuite) TestReIntlOtherPriceValidation() { suite.verifyValidationErrors(&intlOtherPrice, expErrors) }) } + +func (suite *ModelSuite) TestFetchReIntlOtherPrice() { + suite.Run("success - receive ReIntlOtherPrice when all values exist and are found", func() { + address := factory.BuildAddress(suite.DB(), []factory.Customization{ + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "JBER", + State: "AK", + PostalCode: "99505", + IsOconus: models.BoolPointer(true), + }, + }, + }, nil) + + reService, err := models.FetchReServiceByCode(suite.DB(), models.ReServiceCodeIHPK) + suite.NoError(err) + suite.NotNil(reService) + + contract := testdatagen.FetchOrMakeReContract(suite.DB(), testdatagen.Assertions{}) + moveDate := time.Now() + + reIntlOtherPrice, err := models.FetchReIntlOtherPrice(suite.DB(), address.ID, reService.ID, contract.ID, &moveDate) + suite.NoError(err) + suite.NotNil(reIntlOtherPrice) + suite.NotNil(reIntlOtherPrice.PerUnitCents) + }) + + suite.Run("failure - receive error when values aren't provided", func() { + address := factory.BuildAddress(suite.DB(), []factory.Customization{ + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "JBER", + State: "AK", + PostalCode: "99505", + IsOconus: models.BoolPointer(true), + }, + }, + }, nil) + + reService, err := models.FetchReServiceByCode(suite.DB(), models.ReServiceCodeIHPK) + suite.NoError(err) + suite.NotNil(reService) + + contract := testdatagen.FetchOrMakeReContractYear(suite.DB(), testdatagen.Assertions{}) + moveDate := time.Now() + + // no address + reIntlOtherPrice, err := models.FetchReIntlOtherPrice(suite.DB(), uuid.Nil, reService.ID, contract.ID, &moveDate) + suite.Error(err) + suite.Nil(reIntlOtherPrice) + suite.Contains(err.Error(), "error value from re_intl_other_prices - required parameters not provided") + + // no service ID + reIntlOtherPrice, err = models.FetchReIntlOtherPrice(suite.DB(), address.ID, uuid.Nil, contract.ID, &moveDate) + suite.Error(err) + suite.Nil(reIntlOtherPrice) + suite.Contains(err.Error(), "error value from re_intl_other_prices - required parameters not provided") + + // no contract ID + reIntlOtherPrice, err = models.FetchReIntlOtherPrice(suite.DB(), address.ID, reService.ID, uuid.Nil, &moveDate) + suite.Error(err) + suite.Nil(reIntlOtherPrice) + suite.Contains(err.Error(), "error value from re_intl_other_prices - required parameters not provided") + + // no move date + reIntlOtherPrice, err = models.FetchReIntlOtherPrice(suite.DB(), address.ID, reService.ID, contract.ID, nil) + suite.Error(err) + suite.Nil(reIntlOtherPrice) + suite.Contains(err.Error(), "error value from re_intl_other_prices - required parameters not provided") + }) +} diff --git a/pkg/models/re_service.go b/pkg/models/re_service.go index 5fc9d9b3e75..3cf29ce33fa 100644 --- a/pkg/models/re_service.go +++ b/pkg/models/re_service.go @@ -1,12 +1,15 @@ package models import ( + "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/apperror" ) // ReServiceCode is the code of service @@ -223,3 +226,16 @@ func (r *ReService) Validate(_ *pop.Connection) (*validate.Errors, error) { &validators.StringIsPresent{Field: r.Name, Name: "Name"}, ), nil } + +func FetchReServiceByCode(db *pop.Connection, code ReServiceCode) (*ReService, error) { + var reServiceCode ReServiceCode + if code != reServiceCode { + reService := ReService{} + err := db.Where("code = ?", code).First(&reService) + if err != nil { + return nil, apperror.NewQueryError("ReService", err, "") + } + return &reService, err + } + return nil, fmt.Errorf("error fetching from re_services - required code not provided") +} diff --git a/pkg/models/re_service_item.go b/pkg/models/re_service_item.go index 298c8dfa26a..f06ee0990a2 100644 --- a/pkg/models/re_service_item.go +++ b/pkg/models/re_service_item.go @@ -3,10 +3,7 @@ package models import ( "time" - "github.com/gobuffalo/pop/v6" "github.com/gofrs/uuid" - - "github.com/transcom/mymove/pkg/apperror" ) type ReServiceItem struct { @@ -27,12 +24,3 @@ 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/models/re_service_test.go b/pkg/models/re_service_test.go index 929677faab7..41e60c55d73 100644 --- a/pkg/models/re_service_test.go +++ b/pkg/models/re_service_test.go @@ -23,3 +23,19 @@ func (suite *ModelSuite) TestReServiceValidation() { suite.verifyValidationErrors(&emptyReService, expErrors) }) } + +func (suite *ModelSuite) TestFetchReServiceBycode() { + suite.Run("success - receive ReService when code is provided", func() { + reService, err := models.FetchReServiceByCode(suite.DB(), models.ReServiceCodeIHPK) + suite.NoError(err) + suite.NotNil(reService) + }) + + suite.Run("failure - receive error when code is not provided", func() { + var blankReServiceCode models.ReServiceCode + reService, err := models.FetchReServiceByCode(suite.DB(), blankReServiceCode) + suite.Error(err) + suite.Nil(reService) + suite.Contains(err.Error(), "error fetching from re_services - required code not provided") + }) +} From a4abea0f852e6a6811bd6c4347ddd1bb855b43f1 Mon Sep 17 00:00:00 2001 From: Daniel Jordan Date: Tue, 21 Jan 2025 15:59:57 +0000 Subject: [PATCH 09/18] added lookup tests, mocks generated --- pkg/factory/ppm_shipment_factory.go | 90 +++++--- .../distance_zip_lookup.go | 2 +- .../distance_zip_lookup_test.go | 50 +++++ .../per_unit_cents_lookup.go | 8 +- .../per_unit_cents_lookup_test.go | 208 ++++++++++++++++++ .../port_zip_lookup_test.go | 76 ++++++- .../IntlDestinationAdditionalDaySITPricer.go | 109 +++++++++ .../mocks/IntlDestinationFirstDaySITPricer.go | 109 +++++++++ .../mocks/IntlOriginAdditionalDaySITPricer.go | 109 +++++++++ .../mocks/IntlOriginFirstDaySITPricer.go | 109 +++++++++ 10 files changed, 826 insertions(+), 44 deletions(-) create mode 100644 pkg/services/mocks/IntlDestinationAdditionalDaySITPricer.go create mode 100644 pkg/services/mocks/IntlDestinationFirstDaySITPricer.go create mode 100644 pkg/services/mocks/IntlOriginAdditionalDaySITPricer.go create mode 100644 pkg/services/mocks/IntlOriginFirstDaySITPricer.go diff --git a/pkg/factory/ppm_shipment_factory.go b/pkg/factory/ppm_shipment_factory.go index 0306aebe547..cc87032285e 100644 --- a/pkg/factory/ppm_shipment_factory.go +++ b/pkg/factory/ppm_shipment_factory.go @@ -87,42 +87,60 @@ func buildPPMShipmentWithBuildType(db *pop.Connection, customs []Customization, ppmShipment.W2Address = &w2AddressResult } - oldDutyLocationAddress := ppmShipment.Shipment.MoveTaskOrder.Orders.OriginDutyLocation.Address - pickupAddress := BuildAddress(db, []Customization{ - { - Model: models.Address{ - StreetAddress1: "987 New Street", - City: oldDutyLocationAddress.City, - State: oldDutyLocationAddress.State, - PostalCode: oldDutyLocationAddress.PostalCode, + pickupAddressResult := findValidCustomization(customs, Addresses.PickupAddress) + if pickupAddressResult != nil { + pickupAddressResultCustoms := convertCustomizationInList(customs, Addresses.PickupAddress, Address) + + pickupAddressResult := BuildAddress(db, pickupAddressResultCustoms, traits) + ppmShipment.PickupAddressID = &pickupAddressResult.ID + ppmShipment.PickupAddress = &pickupAddressResult + } else { + oldDutyLocationAddress := ppmShipment.Shipment.MoveTaskOrder.Orders.OriginDutyLocation.Address + pickupAddress := BuildAddress(db, []Customization{ + { + Model: models.Address{ + StreetAddress1: "987 New Street", + City: oldDutyLocationAddress.City, + State: oldDutyLocationAddress.State, + PostalCode: oldDutyLocationAddress.PostalCode, + }, }, - }, - }, nil) - ppmShipment.PickupAddressID = &pickupAddress.ID - ppmShipment.PickupAddress = &pickupAddress + }, nil) + ppmShipment.PickupAddressID = &pickupAddress.ID + ppmShipment.PickupAddress = &pickupAddress + } - newDutyLocationAddress := ppmShipment.Shipment.MoveTaskOrder.Orders.NewDutyLocation.Address - destinationAddress := BuildAddress(db, []Customization{ - { - Model: models.Address{ - StreetAddress1: "123 New Street", - City: newDutyLocationAddress.City, - State: newDutyLocationAddress.State, - PostalCode: newDutyLocationAddress.PostalCode, + deliveryAddressResult := findValidCustomization(customs, Addresses.DeliveryAddress) + if deliveryAddressResult != nil { + deliveryAddressResultCustoms := convertCustomizationInList(customs, Addresses.DeliveryAddress, Address) + + deliveryAddressResult := BuildAddress(db, deliveryAddressResultCustoms, traits) + ppmShipment.DestinationAddressID = &deliveryAddressResult.ID + ppmShipment.DestinationAddress = &deliveryAddressResult + } else { + newDutyLocationAddress := ppmShipment.Shipment.MoveTaskOrder.Orders.NewDutyLocation.Address + destinationAddress := BuildAddress(db, []Customization{ + { + Model: models.Address{ + StreetAddress1: "123 New Street", + City: newDutyLocationAddress.City, + State: newDutyLocationAddress.State, + PostalCode: newDutyLocationAddress.PostalCode, + }, }, - }, - }, nil) - ppmShipment.DestinationAddressID = &destinationAddress.ID - ppmShipment.DestinationAddress = &destinationAddress + }, nil) + ppmShipment.DestinationAddressID = &destinationAddress.ID + ppmShipment.DestinationAddress = &destinationAddress + } if buildType == ppmBuildFullAddress { secondaryPickupAddress := BuildAddress(db, []Customization{ { Model: models.Address{ StreetAddress1: "123 Main Street", - City: pickupAddress.City, - State: pickupAddress.State, - PostalCode: pickupAddress.PostalCode, + City: ppmShipment.PickupAddress.City, + State: ppmShipment.PickupAddress.State, + PostalCode: ppmShipment.PickupAddress.PostalCode, }, }, }, nil) @@ -130,9 +148,9 @@ func buildPPMShipmentWithBuildType(db *pop.Connection, customs []Customization, { Model: models.Address{ StreetAddress1: "1234 Main Street", - City: destinationAddress.City, - State: destinationAddress.State, - PostalCode: destinationAddress.PostalCode, + City: ppmShipment.DestinationAddress.City, + State: ppmShipment.DestinationAddress.State, + PostalCode: ppmShipment.DestinationAddress.PostalCode, }, }, }, nil) @@ -140,9 +158,9 @@ func buildPPMShipmentWithBuildType(db *pop.Connection, customs []Customization, { Model: models.Address{ StreetAddress1: "123 Third Street", - City: pickupAddress.City, - State: pickupAddress.State, - PostalCode: pickupAddress.PostalCode, + City: ppmShipment.PickupAddress.City, + State: ppmShipment.PickupAddress.State, + PostalCode: ppmShipment.PickupAddress.PostalCode, }, }, }, nil) @@ -150,9 +168,9 @@ func buildPPMShipmentWithBuildType(db *pop.Connection, customs []Customization, { Model: models.Address{ StreetAddress1: "1234 Third Street", - City: destinationAddress.City, - State: destinationAddress.State, - PostalCode: destinationAddress.PostalCode, + City: ppmShipment.DestinationAddress.City, + State: ppmShipment.DestinationAddress.State, + PostalCode: ppmShipment.DestinationAddress.PostalCode, }, }, }, nil) 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 2a6e1b70a97..cd1f99edda3 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 @@ -96,7 +96,7 @@ func (r DistanceZipLookup) lookup(appCtx appcontext.AppContext, keyData *Service pickupZip = mtoShipment.PPMShipment.PickupAddress.PostalCode destinationZip = portLocation.UsPostRegionCity.UsprZipID } else { - // OCONUS -> OCONUS mileage they don't get reimbursed for + // OCONUS -> OCONUS mileage they don't get reimbursed for this return strconv.Itoa(0), nil } } else { diff --git a/pkg/payment_request/service_param_value_lookups/distance_zip_lookup_test.go b/pkg/payment_request/service_param_value_lookups/distance_zip_lookup_test.go index eeb37166850..918442f2ca5 100644 --- a/pkg/payment_request/service_param_value_lookups/distance_zip_lookup_test.go +++ b/pkg/payment_request/service_param_value_lookups/distance_zip_lookup_test.go @@ -125,6 +125,56 @@ func (suite *ServiceParamValueLookupsSuite) TestDistanceLookup() { suite.Equal(unit.Miles(defaultInternationalZipDistance), *mtoShipment.Distance) }) + suite.Run("Call ZipTransitDistance on international PPMs with CONUS -> Tacoma Port ZIP", func() { + miles := unit.Miles(defaultZipDistance) + + ppmShipment := factory.BuildPPMShipment(suite.DB(), []factory.Customization{ + { + Model: models.MTOShipment{ + Distance: &miles, + ShipmentType: models.MTOShipmentTypePPM, + MarketCode: models.MarketCodeInternational, + }, + }, + { + Model: models.Address{ + StreetAddress1: "Tester Address", + City: "Tulsa", + State: "OK", + PostalCode: "74133", + }, + Type: &factory.Addresses.PickupAddress, + }, + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "JBER", + State: "AK", + PostalCode: "99505", + IsOconus: models.BoolPointer(true), + }, + Type: &factory.Addresses.DeliveryAddress, + }, + }, nil) + + distanceZipLookup := DistanceZipLookup{ + PickupAddress: models.Address{PostalCode: ppmShipment.PickupAddress.PostalCode}, + DestinationAddress: models.Address{PostalCode: ppmShipment.DestinationAddress.PostalCode}, + } + + appContext := suite.AppContextForTest() + distance, err := distanceZipLookup.lookup(appContext, &ServiceItemParamKeyData{ + planner: suite.planner, + mtoShipmentID: &ppmShipment.ShipmentID, + }) + suite.NoError(err) + suite.NotNil(distance) + + planner := suite.planner.(*mocks.Planner) + // should be called with the 98424 ZIP of the Tacoma port and NOT 99505 + planner.AssertCalled(suite.T(), "ZipTransitDistance", appContext, ppmShipment.PickupAddress.PostalCode, "98424", false, true) + }) + suite.Run("Calculate transit zip distance with an approved Destination SIT service item", func() { testdatagen.MakeReContractYear(suite.DB(), testdatagen.Assertions{ ReContractYear: models.ReContractYear{ 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 847093df3e2..7ee8d005782 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 @@ -147,7 +147,7 @@ func (p PerUnitCentsLookup) lookup(appCtx appcontext.AppContext, s *ServiceItemP Where("contract_id = ?", contractID). Where("service_id = ?", serviceID). Where("is_peak_period = ?", isPeakPeriod). - Where("origin_rate_area_id = ?", originRateAreaID). + Where("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) @@ -166,7 +166,7 @@ func (p PerUnitCentsLookup) lookup(appCtx appcontext.AppContext, s *ServiceItemP Where("contract_id = ?", contractID). Where("service_id = ?", serviceID). Where("is_peak_period = ?", isPeakPeriod). - Where("origin_rate_area_id = ?", originRateAreaID). + Where("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) @@ -185,7 +185,7 @@ func (p PerUnitCentsLookup) lookup(appCtx appcontext.AppContext, s *ServiceItemP Where("contract_id = ?", contractID). Where("service_id = ?", serviceID). Where("is_peak_period = ?", isPeakPeriod). - Where("destination_rate_area_id = ?", destRateAreaID). + Where("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) @@ -204,7 +204,7 @@ func (p PerUnitCentsLookup) lookup(appCtx appcontext.AppContext, s *ServiceItemP Where("contract_id = ?", contractID). Where("service_id = ?", serviceID). Where("is_peak_period = ?", isPeakPeriod). - Where("destination_rate_area_id = ?", destRateAreaID). + Where("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) diff --git a/pkg/payment_request/service_param_value_lookups/per_unit_cents_lookup_test.go b/pkg/payment_request/service_param_value_lookups/per_unit_cents_lookup_test.go index 9937f86217b..dc69b69f888 100644 --- a/pkg/payment_request/service_param_value_lookups/per_unit_cents_lookup_test.go +++ b/pkg/payment_request/service_param_value_lookups/per_unit_cents_lookup_test.go @@ -30,6 +30,105 @@ func (suite *ServiceParamValueLookupsSuite) TestPerUnitCentsLookup() { } + setupTestDataPickupOCONUS := func(serviceCode models.ReServiceCode) models.Move { + testdatagen.MakeReContractYear(suite.DB(), testdatagen.Assertions{ + ReContractYear: models.ReContractYear{ + StartDate: time.Now().Add(-24 * time.Hour), + EndDate: time.Now().Add(24 * time.Hour), + }, + }) + move := factory.BuildAvailableToPrimeMove(suite.DB(), nil, nil) + address := factory.BuildAddress(suite.DB(), []factory.Customization{ + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "Anchorage", + State: "AK", + PostalCode: "99505", + IsOconus: models.BoolPointer(true), + }, + }, + }, nil) + shipment := factory.BuildMTOShipment(suite.DB(), []factory.Customization{ + { + Model: models.MTOShipment{ + PickupAddressID: &address.ID, + MarketCode: models.MarketCodeInternational, + }, + }, + { + Model: move, + LinkOnly: true, + }, + }, nil) + mtoServiceItem = factory.BuildMTOServiceItem(suite.DB(), []factory.Customization{ + { + Model: move, + LinkOnly: true, + }, + { + Model: shipment, + LinkOnly: true, + }, + { + Model: models.ReService{ + Code: serviceCode, + }, + }, + }, nil) + + return move + } + + setupTestDataDestOCONUS := func(serviceCode models.ReServiceCode) models.Move { + testdatagen.MakeReContractYear(suite.DB(), testdatagen.Assertions{ + ReContractYear: models.ReContractYear{ + StartDate: time.Now().Add(-24 * time.Hour), + EndDate: time.Now().Add(24 * time.Hour), + }, + }) + move := factory.BuildAvailableToPrimeMove(suite.DB(), nil, nil) + address := factory.BuildAddress(suite.DB(), []factory.Customization{ + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "Anchorage", + State: "AK", + PostalCode: "99505", + IsOconus: models.BoolPointer(true), + }, + }, + }, nil) + shipment := factory.BuildMTOShipment(suite.DB(), []factory.Customization{ + { + Model: models.MTOShipment{ + DestinationAddressID: &address.ID, + MarketCode: models.MarketCodeInternational, + }, + }, + { + Model: move, + LinkOnly: true, + }, + }, nil) + mtoServiceItem = factory.BuildMTOServiceItem(suite.DB(), []factory.Customization{ + { + Model: move, + LinkOnly: true, + }, + { + Model: shipment, + LinkOnly: true, + }, + { + Model: models.ReService{ + Code: serviceCode, + }, + }, + }, nil) + return move + } + suite.Run("success - returns perUnitCent value for IHPK", func() { setupTestData(models.ReServiceCodeIHPK) @@ -119,6 +218,115 @@ func (suite *ServiceParamValueLookupsSuite) TestPerUnitCentsLookup() { suite.Equal(perUnitCents, "1605") }) + suite.Run("success - returns perUnitCent value for IOFSIT", func() { + move := setupTestDataPickupOCONUS(models.ReServiceCodeIOFSIT) + + paramLookup, err := ServiceParamLookupInitialize(suite.AppContextForTest(), suite.planner, mtoServiceItem, uuid.Must(uuid.NewV4()), move.ID, nil) + suite.FatalNoError(err) + + perUnitCents, err := paramLookup.ServiceParamValue(suite.AppContextForTest(), key) + suite.FatalNoError(err) + suite.Equal(perUnitCents, "607") + }) + + suite.Run("success - returns perUnitCent value for IOASIT", func() { + move := setupTestDataPickupOCONUS(models.ReServiceCodeIOASIT) + + paramLookup, err := ServiceParamLookupInitialize(suite.AppContextForTest(), suite.planner, mtoServiceItem, uuid.Must(uuid.NewV4()), move.ID, nil) + suite.FatalNoError(err) + + perUnitCents, err := paramLookup.ServiceParamValue(suite.AppContextForTest(), key) + suite.FatalNoError(err) + suite.Equal(perUnitCents, "14") + }) + + suite.Run("success - returns perUnitCent value for IDFSIT", func() { + move := setupTestDataDestOCONUS(models.ReServiceCodeIDFSIT) + + paramLookup, err := ServiceParamLookupInitialize(suite.AppContextForTest(), suite.planner, mtoServiceItem, uuid.Must(uuid.NewV4()), move.ID, nil) + suite.FatalNoError(err) + + perUnitCents, err := paramLookup.ServiceParamValue(suite.AppContextForTest(), key) + suite.FatalNoError(err) + suite.Equal(perUnitCents, "607") + }) + + suite.Run("success - returns perUnitCent value for IDASIT", func() { + move := setupTestDataDestOCONUS(models.ReServiceCodeIDASIT) + + paramLookup, err := ServiceParamLookupInitialize(suite.AppContextForTest(), suite.planner, mtoServiceItem, uuid.Must(uuid.NewV4()), move.ID, nil) + suite.FatalNoError(err) + + perUnitCents, err := paramLookup.ServiceParamValue(suite.AppContextForTest(), key) + suite.FatalNoError(err) + suite.Equal(perUnitCents, "14") + }) + + suite.Run("success - returns perUnitCent value for IDASIT for a PPM", func() { + contractYear := testdatagen.MakeReContractYear(suite.DB(), testdatagen.Assertions{ + ReContractYear: models.ReContractYear{ + StartDate: time.Now().Add(-24 * time.Hour), + EndDate: time.Now().Add(24 * time.Hour), + }, + }) + move := factory.BuildAvailableToPrimeMove(suite.DB(), nil, nil) + ppm := factory.BuildPPMShipment(suite.DB(), []factory.Customization{ + { + Model: models.MTOShipment{ + ActualPickupDate: models.TimePointer(time.Now()), + }, + }, + { + Model: models.Address{ + StreetAddress1: "Tester Address", + City: "Tulsa", + State: "OK", + PostalCode: "74133", + }, + Type: &factory.Addresses.PickupAddress, + }, + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "JBER", + State: "AK", + PostalCode: "99505", + IsOconus: models.BoolPointer(true), + }, + Type: &factory.Addresses.DeliveryAddress, + }, + }, nil) + + mtoServiceItem = factory.BuildMTOServiceItem(suite.DB(), []factory.Customization{ + { + Model: models.ReService{ + Code: models.ReServiceCodeIDASIT, + }, + }, + { + Model: move, + LinkOnly: true, + }, + }, nil) + + _, err := ServiceParamLookupInitialize(suite.AppContextForTest(), suite.planner, mtoServiceItem, uuid.Must(uuid.NewV4()), mtoServiceItem.MoveTaskOrderID, nil) + suite.FatalNoError(err) + + perUnitCentsLookup := PerUnitCentsLookup{ + ServiceItem: mtoServiceItem, + MTOShipment: ppm.Shipment, + } + + appContext := suite.AppContextForTest() + perUnitCents, err := perUnitCentsLookup.lookup(appContext, &ServiceItemParamKeyData{ + planner: suite.planner, + mtoShipmentID: &ppm.ShipmentID, + ContractID: contractYear.ContractID, + }) + suite.NoError(err) + suite.Equal(perUnitCents, "14") + }) + suite.Run("failure - unauthorized service code", func() { setupTestData(models.ReServiceCodeDUPK) diff --git a/pkg/payment_request/service_param_value_lookups/port_zip_lookup_test.go b/pkg/payment_request/service_param_value_lookups/port_zip_lookup_test.go index 4410ba8e198..3f8776e0ecf 100644 --- a/pkg/payment_request/service_param_value_lookups/port_zip_lookup_test.go +++ b/pkg/payment_request/service_param_value_lookups/port_zip_lookup_test.go @@ -85,7 +85,76 @@ func (suite *ServiceParamValueLookupsSuite) TestPortZipLookup() { suite.Equal(portZip, port.UsPostRegionCity.UsprZipID) }) - suite.Run("failure - no port zip on service item", func() { + suite.Run("success - returns PortZip value for Port Code 3002 for PPMs", func() { + port := factory.FetchPortLocation(suite.DB(), []factory.Customization{ + { + Model: models.Port{ + PortCode: "3002", + }, + }, + }, nil) + + contractYear := testdatagen.MakeReContractYear(suite.DB(), testdatagen.Assertions{ + ReContractYear: models.ReContractYear{ + StartDate: time.Now().Add(-24 * time.Hour), + EndDate: time.Now().Add(24 * time.Hour), + }, + }) + + move := factory.BuildAvailableToPrimeMove(suite.DB(), nil, nil) + ppm := factory.BuildPPMShipment(suite.DB(), []factory.Customization{ + { + Model: models.MTOShipment{ + ActualPickupDate: models.TimePointer(time.Now()), + MarketCode: models.MarketCodeInternational, + }, + }, + { + Model: models.Address{ + StreetAddress1: "Tester Address", + City: "Tulsa", + State: "OK", + PostalCode: "74133", + }, + Type: &factory.Addresses.PickupAddress, + }, + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "JBER", + State: "AK", + PostalCode: "99505", + IsOconus: models.BoolPointer(true), + }, + Type: &factory.Addresses.DeliveryAddress, + }, + }, nil) + + mtoServiceItem = factory.BuildMTOServiceItem(suite.DB(), []factory.Customization{ + { + Model: move, + LinkOnly: true, + }, + }, nil) + + _, err := ServiceParamLookupInitialize(suite.AppContextForTest(), suite.planner, mtoServiceItem, uuid.Must(uuid.NewV4()), mtoServiceItem.MoveTaskOrderID, nil) + suite.FatalNoError(err) + + portZipLookup := PortZipLookup{ + ServiceItem: mtoServiceItem, + } + + appContext := suite.AppContextForTest() + portZip, err := portZipLookup.lookup(appContext, &ServiceItemParamKeyData{ + planner: suite.planner, + mtoShipmentID: &ppm.ShipmentID, + ContractID: contractYear.ContractID, + }) + suite.NoError(err) + suite.Equal(portZip, port.UsPostRegionCity.UsprZipID) + }) + + suite.Run("returns nothing if shipment is HHG and service item does not have port info", func() { testdatagen.MakeReContractYear(suite.DB(), testdatagen.Assertions{ ReContractYear: models.ReContractYear{ StartDate: time.Now().Add(-24 * time.Hour), @@ -108,7 +177,8 @@ func (suite *ServiceParamValueLookupsSuite) TestPortZipLookup() { paramLookup, err := ServiceParamLookupInitialize(suite.AppContextForTest(), suite.planner, mtoServiceItem, uuid.Must(uuid.NewV4()), mtoServiceItem.MoveTaskOrderID, nil) suite.FatalNoError(err) - _, err = paramLookup.ServiceParamValue(suite.AppContextForTest(), key) - suite.Error(err) + portZip, err := paramLookup.ServiceParamValue(suite.AppContextForTest(), key) + suite.NoError(err) + suite.Equal(portZip, "") }) } diff --git a/pkg/services/mocks/IntlDestinationAdditionalDaySITPricer.go b/pkg/services/mocks/IntlDestinationAdditionalDaySITPricer.go new file mode 100644 index 00000000000..7a11b759d7f --- /dev/null +++ b/pkg/services/mocks/IntlDestinationAdditionalDaySITPricer.go @@ -0,0 +1,109 @@ +// Code generated by mockery. DO NOT EDIT. + +package mocks + +import ( + mock "github.com/stretchr/testify/mock" + appcontext "github.com/transcom/mymove/pkg/appcontext" + + models "github.com/transcom/mymove/pkg/models" + + services "github.com/transcom/mymove/pkg/services" + + time "time" + + unit "github.com/transcom/mymove/pkg/unit" +) + +// IntlDestinationAdditionalDaySITPricer is an autogenerated mock type for the IntlDestinationAdditionalDaySITPricer type +type IntlDestinationAdditionalDaySITPricer struct { + mock.Mock +} + +// Price provides a mock function with given fields: appCtx, contractCode, requestedPickupDate, numberOfDaysInSIT, weight, perUnitCents +func (_m *IntlDestinationAdditionalDaySITPricer) Price(appCtx appcontext.AppContext, contractCode string, requestedPickupDate time.Time, numberOfDaysInSIT int, weight unit.Pound, perUnitCents int) (unit.Cents, services.PricingDisplayParams, error) { + ret := _m.Called(appCtx, contractCode, requestedPickupDate, numberOfDaysInSIT, weight, perUnitCents) + + if len(ret) == 0 { + panic("no return value specified for Price") + } + + var r0 unit.Cents + var r1 services.PricingDisplayParams + var r2 error + if rf, ok := ret.Get(0).(func(appcontext.AppContext, string, time.Time, int, unit.Pound, int) (unit.Cents, services.PricingDisplayParams, error)); ok { + return rf(appCtx, contractCode, requestedPickupDate, numberOfDaysInSIT, weight, perUnitCents) + } + if rf, ok := ret.Get(0).(func(appcontext.AppContext, string, time.Time, int, unit.Pound, int) unit.Cents); ok { + r0 = rf(appCtx, contractCode, requestedPickupDate, numberOfDaysInSIT, weight, perUnitCents) + } else { + r0 = ret.Get(0).(unit.Cents) + } + + if rf, ok := ret.Get(1).(func(appcontext.AppContext, string, time.Time, int, unit.Pound, int) services.PricingDisplayParams); ok { + r1 = rf(appCtx, contractCode, requestedPickupDate, numberOfDaysInSIT, weight, perUnitCents) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(services.PricingDisplayParams) + } + } + + if rf, ok := ret.Get(2).(func(appcontext.AppContext, string, time.Time, int, unit.Pound, int) error); ok { + r2 = rf(appCtx, contractCode, requestedPickupDate, numberOfDaysInSIT, weight, perUnitCents) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// PriceUsingParams provides a mock function with given fields: appCtx, params +func (_m *IntlDestinationAdditionalDaySITPricer) PriceUsingParams(appCtx appcontext.AppContext, params models.PaymentServiceItemParams) (unit.Cents, services.PricingDisplayParams, error) { + ret := _m.Called(appCtx, params) + + if len(ret) == 0 { + panic("no return value specified for PriceUsingParams") + } + + var r0 unit.Cents + var r1 services.PricingDisplayParams + var r2 error + if rf, ok := ret.Get(0).(func(appcontext.AppContext, models.PaymentServiceItemParams) (unit.Cents, services.PricingDisplayParams, error)); ok { + return rf(appCtx, params) + } + if rf, ok := ret.Get(0).(func(appcontext.AppContext, models.PaymentServiceItemParams) unit.Cents); ok { + r0 = rf(appCtx, params) + } else { + r0 = ret.Get(0).(unit.Cents) + } + + if rf, ok := ret.Get(1).(func(appcontext.AppContext, models.PaymentServiceItemParams) services.PricingDisplayParams); ok { + r1 = rf(appCtx, params) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(services.PricingDisplayParams) + } + } + + if rf, ok := ret.Get(2).(func(appcontext.AppContext, models.PaymentServiceItemParams) error); ok { + r2 = rf(appCtx, params) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// NewIntlDestinationAdditionalDaySITPricer creates a new instance of IntlDestinationAdditionalDaySITPricer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewIntlDestinationAdditionalDaySITPricer(t interface { + mock.TestingT + Cleanup(func()) +}) *IntlDestinationAdditionalDaySITPricer { + mock := &IntlDestinationAdditionalDaySITPricer{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/services/mocks/IntlDestinationFirstDaySITPricer.go b/pkg/services/mocks/IntlDestinationFirstDaySITPricer.go new file mode 100644 index 00000000000..99df36da131 --- /dev/null +++ b/pkg/services/mocks/IntlDestinationFirstDaySITPricer.go @@ -0,0 +1,109 @@ +// Code generated by mockery. DO NOT EDIT. + +package mocks + +import ( + mock "github.com/stretchr/testify/mock" + appcontext "github.com/transcom/mymove/pkg/appcontext" + + models "github.com/transcom/mymove/pkg/models" + + services "github.com/transcom/mymove/pkg/services" + + time "time" + + unit "github.com/transcom/mymove/pkg/unit" +) + +// IntlDestinationFirstDaySITPricer is an autogenerated mock type for the IntlDestinationFirstDaySITPricer type +type IntlDestinationFirstDaySITPricer struct { + mock.Mock +} + +// Price provides a mock function with given fields: appCtx, contractCode, requestedPickupDate, weight, perUnitCents +func (_m *IntlDestinationFirstDaySITPricer) Price(appCtx appcontext.AppContext, contractCode string, requestedPickupDate time.Time, weight unit.Pound, perUnitCents int) (unit.Cents, services.PricingDisplayParams, error) { + ret := _m.Called(appCtx, contractCode, requestedPickupDate, weight, perUnitCents) + + if len(ret) == 0 { + panic("no return value specified for Price") + } + + var r0 unit.Cents + var r1 services.PricingDisplayParams + var r2 error + if rf, ok := ret.Get(0).(func(appcontext.AppContext, string, time.Time, unit.Pound, int) (unit.Cents, services.PricingDisplayParams, error)); ok { + return rf(appCtx, contractCode, requestedPickupDate, weight, perUnitCents) + } + if rf, ok := ret.Get(0).(func(appcontext.AppContext, string, time.Time, unit.Pound, int) unit.Cents); ok { + r0 = rf(appCtx, contractCode, requestedPickupDate, weight, perUnitCents) + } else { + r0 = ret.Get(0).(unit.Cents) + } + + if rf, ok := ret.Get(1).(func(appcontext.AppContext, string, time.Time, unit.Pound, int) services.PricingDisplayParams); ok { + r1 = rf(appCtx, contractCode, requestedPickupDate, weight, perUnitCents) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(services.PricingDisplayParams) + } + } + + if rf, ok := ret.Get(2).(func(appcontext.AppContext, string, time.Time, unit.Pound, int) error); ok { + r2 = rf(appCtx, contractCode, requestedPickupDate, weight, perUnitCents) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// PriceUsingParams provides a mock function with given fields: appCtx, params +func (_m *IntlDestinationFirstDaySITPricer) PriceUsingParams(appCtx appcontext.AppContext, params models.PaymentServiceItemParams) (unit.Cents, services.PricingDisplayParams, error) { + ret := _m.Called(appCtx, params) + + if len(ret) == 0 { + panic("no return value specified for PriceUsingParams") + } + + var r0 unit.Cents + var r1 services.PricingDisplayParams + var r2 error + if rf, ok := ret.Get(0).(func(appcontext.AppContext, models.PaymentServiceItemParams) (unit.Cents, services.PricingDisplayParams, error)); ok { + return rf(appCtx, params) + } + if rf, ok := ret.Get(0).(func(appcontext.AppContext, models.PaymentServiceItemParams) unit.Cents); ok { + r0 = rf(appCtx, params) + } else { + r0 = ret.Get(0).(unit.Cents) + } + + if rf, ok := ret.Get(1).(func(appcontext.AppContext, models.PaymentServiceItemParams) services.PricingDisplayParams); ok { + r1 = rf(appCtx, params) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(services.PricingDisplayParams) + } + } + + if rf, ok := ret.Get(2).(func(appcontext.AppContext, models.PaymentServiceItemParams) error); ok { + r2 = rf(appCtx, params) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// NewIntlDestinationFirstDaySITPricer creates a new instance of IntlDestinationFirstDaySITPricer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewIntlDestinationFirstDaySITPricer(t interface { + mock.TestingT + Cleanup(func()) +}) *IntlDestinationFirstDaySITPricer { + mock := &IntlDestinationFirstDaySITPricer{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/services/mocks/IntlOriginAdditionalDaySITPricer.go b/pkg/services/mocks/IntlOriginAdditionalDaySITPricer.go new file mode 100644 index 00000000000..a931b2d3879 --- /dev/null +++ b/pkg/services/mocks/IntlOriginAdditionalDaySITPricer.go @@ -0,0 +1,109 @@ +// Code generated by mockery. DO NOT EDIT. + +package mocks + +import ( + mock "github.com/stretchr/testify/mock" + appcontext "github.com/transcom/mymove/pkg/appcontext" + + models "github.com/transcom/mymove/pkg/models" + + services "github.com/transcom/mymove/pkg/services" + + time "time" + + unit "github.com/transcom/mymove/pkg/unit" +) + +// IntlOriginAdditionalDaySITPricer is an autogenerated mock type for the IntlOriginAdditionalDaySITPricer type +type IntlOriginAdditionalDaySITPricer struct { + mock.Mock +} + +// Price provides a mock function with given fields: appCtx, contractCode, requestedPickupDate, numberOfDaysInSIT, weight, perUnitCents +func (_m *IntlOriginAdditionalDaySITPricer) Price(appCtx appcontext.AppContext, contractCode string, requestedPickupDate time.Time, numberOfDaysInSIT int, weight unit.Pound, perUnitCents int) (unit.Cents, services.PricingDisplayParams, error) { + ret := _m.Called(appCtx, contractCode, requestedPickupDate, numberOfDaysInSIT, weight, perUnitCents) + + if len(ret) == 0 { + panic("no return value specified for Price") + } + + var r0 unit.Cents + var r1 services.PricingDisplayParams + var r2 error + if rf, ok := ret.Get(0).(func(appcontext.AppContext, string, time.Time, int, unit.Pound, int) (unit.Cents, services.PricingDisplayParams, error)); ok { + return rf(appCtx, contractCode, requestedPickupDate, numberOfDaysInSIT, weight, perUnitCents) + } + if rf, ok := ret.Get(0).(func(appcontext.AppContext, string, time.Time, int, unit.Pound, int) unit.Cents); ok { + r0 = rf(appCtx, contractCode, requestedPickupDate, numberOfDaysInSIT, weight, perUnitCents) + } else { + r0 = ret.Get(0).(unit.Cents) + } + + if rf, ok := ret.Get(1).(func(appcontext.AppContext, string, time.Time, int, unit.Pound, int) services.PricingDisplayParams); ok { + r1 = rf(appCtx, contractCode, requestedPickupDate, numberOfDaysInSIT, weight, perUnitCents) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(services.PricingDisplayParams) + } + } + + if rf, ok := ret.Get(2).(func(appcontext.AppContext, string, time.Time, int, unit.Pound, int) error); ok { + r2 = rf(appCtx, contractCode, requestedPickupDate, numberOfDaysInSIT, weight, perUnitCents) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// PriceUsingParams provides a mock function with given fields: appCtx, params +func (_m *IntlOriginAdditionalDaySITPricer) PriceUsingParams(appCtx appcontext.AppContext, params models.PaymentServiceItemParams) (unit.Cents, services.PricingDisplayParams, error) { + ret := _m.Called(appCtx, params) + + if len(ret) == 0 { + panic("no return value specified for PriceUsingParams") + } + + var r0 unit.Cents + var r1 services.PricingDisplayParams + var r2 error + if rf, ok := ret.Get(0).(func(appcontext.AppContext, models.PaymentServiceItemParams) (unit.Cents, services.PricingDisplayParams, error)); ok { + return rf(appCtx, params) + } + if rf, ok := ret.Get(0).(func(appcontext.AppContext, models.PaymentServiceItemParams) unit.Cents); ok { + r0 = rf(appCtx, params) + } else { + r0 = ret.Get(0).(unit.Cents) + } + + if rf, ok := ret.Get(1).(func(appcontext.AppContext, models.PaymentServiceItemParams) services.PricingDisplayParams); ok { + r1 = rf(appCtx, params) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(services.PricingDisplayParams) + } + } + + if rf, ok := ret.Get(2).(func(appcontext.AppContext, models.PaymentServiceItemParams) error); ok { + r2 = rf(appCtx, params) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// NewIntlOriginAdditionalDaySITPricer creates a new instance of IntlOriginAdditionalDaySITPricer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewIntlOriginAdditionalDaySITPricer(t interface { + mock.TestingT + Cleanup(func()) +}) *IntlOriginAdditionalDaySITPricer { + mock := &IntlOriginAdditionalDaySITPricer{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/services/mocks/IntlOriginFirstDaySITPricer.go b/pkg/services/mocks/IntlOriginFirstDaySITPricer.go new file mode 100644 index 00000000000..d36cb1e92c9 --- /dev/null +++ b/pkg/services/mocks/IntlOriginFirstDaySITPricer.go @@ -0,0 +1,109 @@ +// Code generated by mockery. DO NOT EDIT. + +package mocks + +import ( + mock "github.com/stretchr/testify/mock" + appcontext "github.com/transcom/mymove/pkg/appcontext" + + models "github.com/transcom/mymove/pkg/models" + + services "github.com/transcom/mymove/pkg/services" + + time "time" + + unit "github.com/transcom/mymove/pkg/unit" +) + +// IntlOriginFirstDaySITPricer is an autogenerated mock type for the IntlOriginFirstDaySITPricer type +type IntlOriginFirstDaySITPricer struct { + mock.Mock +} + +// Price provides a mock function with given fields: appCtx, contractCode, requestedPickupDate, weight, perUnitCents +func (_m *IntlOriginFirstDaySITPricer) Price(appCtx appcontext.AppContext, contractCode string, requestedPickupDate time.Time, weight unit.Pound, perUnitCents int) (unit.Cents, services.PricingDisplayParams, error) { + ret := _m.Called(appCtx, contractCode, requestedPickupDate, weight, perUnitCents) + + if len(ret) == 0 { + panic("no return value specified for Price") + } + + var r0 unit.Cents + var r1 services.PricingDisplayParams + var r2 error + if rf, ok := ret.Get(0).(func(appcontext.AppContext, string, time.Time, unit.Pound, int) (unit.Cents, services.PricingDisplayParams, error)); ok { + return rf(appCtx, contractCode, requestedPickupDate, weight, perUnitCents) + } + if rf, ok := ret.Get(0).(func(appcontext.AppContext, string, time.Time, unit.Pound, int) unit.Cents); ok { + r0 = rf(appCtx, contractCode, requestedPickupDate, weight, perUnitCents) + } else { + r0 = ret.Get(0).(unit.Cents) + } + + if rf, ok := ret.Get(1).(func(appcontext.AppContext, string, time.Time, unit.Pound, int) services.PricingDisplayParams); ok { + r1 = rf(appCtx, contractCode, requestedPickupDate, weight, perUnitCents) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(services.PricingDisplayParams) + } + } + + if rf, ok := ret.Get(2).(func(appcontext.AppContext, string, time.Time, unit.Pound, int) error); ok { + r2 = rf(appCtx, contractCode, requestedPickupDate, weight, perUnitCents) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// PriceUsingParams provides a mock function with given fields: appCtx, params +func (_m *IntlOriginFirstDaySITPricer) PriceUsingParams(appCtx appcontext.AppContext, params models.PaymentServiceItemParams) (unit.Cents, services.PricingDisplayParams, error) { + ret := _m.Called(appCtx, params) + + if len(ret) == 0 { + panic("no return value specified for PriceUsingParams") + } + + var r0 unit.Cents + var r1 services.PricingDisplayParams + var r2 error + if rf, ok := ret.Get(0).(func(appcontext.AppContext, models.PaymentServiceItemParams) (unit.Cents, services.PricingDisplayParams, error)); ok { + return rf(appCtx, params) + } + if rf, ok := ret.Get(0).(func(appcontext.AppContext, models.PaymentServiceItemParams) unit.Cents); ok { + r0 = rf(appCtx, params) + } else { + r0 = ret.Get(0).(unit.Cents) + } + + if rf, ok := ret.Get(1).(func(appcontext.AppContext, models.PaymentServiceItemParams) services.PricingDisplayParams); ok { + r1 = rf(appCtx, params) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(services.PricingDisplayParams) + } + } + + if rf, ok := ret.Get(2).(func(appcontext.AppContext, models.PaymentServiceItemParams) error); ok { + r2 = rf(appCtx, params) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// NewIntlOriginFirstDaySITPricer creates a new instance of IntlOriginFirstDaySITPricer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewIntlOriginFirstDaySITPricer(t interface { + mock.TestingT + Cleanup(func()) +}) *IntlOriginFirstDaySITPricer { + mock := &IntlOriginFirstDaySITPricer{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} From 7fad2a1d0065308d1f833aa1c3b7a8faf4549461 Mon Sep 17 00:00:00 2001 From: Daniel Jordan Date: Tue, 21 Jan 2025 16:43:25 +0000 Subject: [PATCH 10/18] added tests for pricers and ghcrateengine --- ..._destination_additional_days_sit_pricer.go | 4 +- ...ination_additional_days_sit_pricer_test.go | 127 ++++++++++++++++++ ...l_destination_first_day_sit_pricer_test.go | 116 ++++++++++++++++ ..._origin_additional_days_sit_pricer_test.go | 127 ++++++++++++++++++ .../intl_origin_first_day_sit_pricer_test.go | 116 ++++++++++++++++ .../ghcrateengine/pricer_helpers_intl.go | 4 +- .../ghcrateengine/pricer_helpers_intl_test.go | 106 +++++++++++++++ 7 files changed, 596 insertions(+), 4 deletions(-) create mode 100644 pkg/services/ghcrateengine/intl_destination_additional_days_sit_pricer_test.go create mode 100644 pkg/services/ghcrateengine/intl_destination_first_day_sit_pricer_test.go create mode 100644 pkg/services/ghcrateengine/intl_origin_additional_days_sit_pricer_test.go create mode 100644 pkg/services/ghcrateengine/intl_origin_first_day_sit_pricer_test.go diff --git a/pkg/services/ghcrateengine/intl_destination_additional_days_sit_pricer.go b/pkg/services/ghcrateengine/intl_destination_additional_days_sit_pricer.go index 465099cf73f..ed57a28b57c 100644 --- a/pkg/services/ghcrateengine/intl_destination_additional_days_sit_pricer.go +++ b/pkg/services/ghcrateengine/intl_destination_additional_days_sit_pricer.go @@ -26,12 +26,12 @@ func (p intlDestinationAdditionalDaySITPricer) PriceUsingParams(appCtx appcontex return unit.Cents(0), nil, err } - numberOfDaysInSIT, err := getParamInt(params, models.ServiceItemParamNameNumberDaysSIT) + referenceDate, err := getParamTime(params, models.ServiceItemParamNameReferenceDate) if err != nil { return unit.Cents(0), nil, err } - referenceDate, err := getParamTime(params, models.ServiceItemParamNameReferenceDate) + numberOfDaysInSIT, err := getParamInt(params, models.ServiceItemParamNameNumberDaysSIT) if err != nil { return unit.Cents(0), nil, err } diff --git a/pkg/services/ghcrateengine/intl_destination_additional_days_sit_pricer_test.go b/pkg/services/ghcrateengine/intl_destination_additional_days_sit_pricer_test.go new file mode 100644 index 00000000000..76a40753891 --- /dev/null +++ b/pkg/services/ghcrateengine/intl_destination_additional_days_sit_pricer_test.go @@ -0,0 +1,127 @@ +package ghcrateengine + +import ( + "fmt" + "strconv" + "time" + + "github.com/transcom/mymove/pkg/factory" + "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/services" + "github.com/transcom/mymove/pkg/testdatagen" + "github.com/transcom/mymove/pkg/unit" +) + +const ( + idasitTestContractYearName = "Base Period Year 1" + idasitTestPerUnitCents = unit.Cents(15000) + idasitTestTotalCost = unit.Cents(1575000) + idasitTestIsPeakPeriod = true + idasitTestEscalationCompounded = 1.0000 + idasitTestWeight = unit.Pound(2100) + idasitTestPriceCents = unit.Cents(500) + idasitNumerDaysInSIT = 5 +) + +var idasitTestRequestedPickupDate = time.Date(testdatagen.TestYear, peakStart.month, peakStart.day, 0, 0, 0, 0, time.UTC) + +func (suite *GHCRateEngineServiceSuite) TestIntlDestinationAdditionalDaySITPricer() { + pricer := NewIntlDestinationAdditionalDaySITPricer() + + suite.Run("success using PaymentServiceItemParams", func() { + paymentServiceItem := suite.setupIntlDestinationAdditionalDayServiceItem() + + totalCost, displayParams, err := pricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.NoError(err) + suite.Equal(idasitTestTotalCost, totalCost) + + expectedParams := services.PricingDisplayParams{ + {Key: models.ServiceItemParamNameContractYearName, Value: idasitTestContractYearName}, + {Key: models.ServiceItemParamNamePriceRateOrFactor, Value: FormatCents(idasitTestPerUnitCents)}, + {Key: models.ServiceItemParamNameIsPeak, Value: FormatBool(idasitTestIsPeakPeriod)}, + {Key: models.ServiceItemParamNameEscalationCompounded, Value: FormatEscalation(idasitTestEscalationCompounded)}, + } + suite.validatePricerCreatedParams(expectedParams, displayParams) + }) + + suite.Run("invalid parameters to PriceUsingParams", func() { + paymentServiceItem := suite.setupIntlDestinationAdditionalDayServiceItem() + + // WeightBilled + paymentServiceItem.PaymentServiceItemParams[4].ServiceItemParamKey.Type = models.ServiceItemParamTypeBoolean + _, _, err := pricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.Error(err) + suite.Contains(err.Error(), fmt.Sprintf("trying to convert %s to an int", models.ServiceItemParamNameWeightBilled)) + + // PerUnitCents + paymentServiceItem.PaymentServiceItemParams[3].ServiceItemParamKey.Type = models.ServiceItemParamTypeBoolean + _, _, err = pricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.Error(err) + suite.Contains(err.Error(), fmt.Sprintf("trying to convert %s to an int", models.ServiceItemParamNamePerUnitCents)) + + // NumberDaysSIT + paymentServiceItem.PaymentServiceItemParams[2].ServiceItemParamKey.Type = models.ServiceItemParamTypeBoolean + _, _, err = pricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.Error(err) + suite.Contains(err.Error(), fmt.Sprintf("trying to convert %s to an int", models.ServiceItemParamNameNumberDaysSIT)) + + // ReferenceDate + paymentServiceItem.PaymentServiceItemParams[1].ServiceItemParamKey.Type = models.ServiceItemParamTypeBoolean + _, _, err = pricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.Error(err) + suite.Contains(err.Error(), fmt.Sprintf("trying to convert %s to a time", models.ServiceItemParamNameReferenceDate)) + + // ContractCode + paymentServiceItem.PaymentServiceItemParams[0].ServiceItemParamKey.Type = models.ServiceItemParamTypeBoolean + _, _, err = pricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.Error(err) + suite.Contains(err.Error(), fmt.Sprintf("trying to convert %s to a string", models.ServiceItemParamNameContractCode)) + }) +} + +func (suite *GHCRateEngineServiceSuite) setupIntlDestinationAdditionalDayServiceItem() models.PaymentServiceItem { + contract := testdatagen.FetchOrMakeReContract(suite.DB(), testdatagen.Assertions{}) + startDate := time.Date(2018, time.January, 1, 12, 0, 0, 0, time.UTC) + endDate := time.Date(2018, time.December, 31, 12, 0, 0, 0, time.UTC) + testdatagen.FetchOrMakeReContractYear(suite.DB(), testdatagen.Assertions{ + ReContractYear: models.ReContractYear{ + Contract: contract, + ContractID: contract.ID, + StartDate: startDate, + EndDate: endDate, + Escalation: 1.0, + EscalationCompounded: 1.0, + }, + }) + return factory.BuildPaymentServiceItemWithParams( + suite.DB(), + models.ReServiceCodeIDASIT, + []factory.CreatePaymentServiceItemParams{ + { + Key: models.ServiceItemParamNameContractCode, + KeyType: models.ServiceItemParamTypeString, + Value: contract.Code, + }, + { + Key: models.ServiceItemParamNameReferenceDate, + KeyType: models.ServiceItemParamTypeDate, + Value: idasitTestRequestedPickupDate.Format(DateParamFormat), + }, + { + Key: models.ServiceItemParamNameNumberDaysSIT, + KeyType: models.ServiceItemParamTypeInteger, + Value: fmt.Sprintf("%d", int(idasitNumerDaysInSIT)), + }, + { + Key: models.ServiceItemParamNamePerUnitCents, + KeyType: models.ServiceItemParamTypeInteger, + Value: fmt.Sprintf("%d", int(idasitTestPerUnitCents)), + }, + { + Key: models.ServiceItemParamNameWeightBilled, + KeyType: models.ServiceItemParamTypeInteger, + Value: strconv.Itoa(idasitTestWeight.Int()), + }, + }, nil, nil, + ) +} diff --git a/pkg/services/ghcrateengine/intl_destination_first_day_sit_pricer_test.go b/pkg/services/ghcrateengine/intl_destination_first_day_sit_pricer_test.go new file mode 100644 index 00000000000..fbaebec2df0 --- /dev/null +++ b/pkg/services/ghcrateengine/intl_destination_first_day_sit_pricer_test.go @@ -0,0 +1,116 @@ +package ghcrateengine + +import ( + "fmt" + "strconv" + "time" + + "github.com/transcom/mymove/pkg/factory" + "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/services" + "github.com/transcom/mymove/pkg/testdatagen" + "github.com/transcom/mymove/pkg/unit" +) + +const ( + idfsitTestContractYearName = "Base Period Year 1" + idfsitTestPerUnitCents = unit.Cents(15000) + idfsitTestTotalCost = unit.Cents(315000) + idfsitTestIsPeakPeriod = true + idfsitTestEscalationCompounded = 1.0000 + idfsitTestWeight = unit.Pound(2100) + idfsitTestPriceCents = unit.Cents(500) + idfsitNumerDaysInSIT = 5 +) + +var idfsitTestRequestedPickupDate = time.Date(testdatagen.TestYear, peakStart.month, peakStart.day, 0, 0, 0, 0, time.UTC) + +func (suite *GHCRateEngineServiceSuite) TestIntlDestinationFirstDaySITPricer() { + pricer := NewIntlDestinationFirstDaySITPricer() + + suite.Run("success using PaymentServiceItemParams", func() { + paymentServiceItem := suite.setupIntlDestinationFirstDayServiceItem() + + totalCost, displayParams, err := pricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.NoError(err) + suite.Equal(idfsitTestTotalCost, totalCost) + + expectedParams := services.PricingDisplayParams{ + {Key: models.ServiceItemParamNameContractYearName, Value: idfsitTestContractYearName}, + {Key: models.ServiceItemParamNamePriceRateOrFactor, Value: FormatCents(idfsitTestPerUnitCents)}, + {Key: models.ServiceItemParamNameIsPeak, Value: FormatBool(idfsitTestIsPeakPeriod)}, + {Key: models.ServiceItemParamNameEscalationCompounded, Value: FormatEscalation(idfsitTestEscalationCompounded)}, + } + suite.validatePricerCreatedParams(expectedParams, displayParams) + }) + + suite.Run("invalid parameters to PriceUsingParams", func() { + paymentServiceItem := suite.setupIntlDestinationFirstDayServiceItem() + + // WeightBilled + paymentServiceItem.PaymentServiceItemParams[3].ServiceItemParamKey.Type = models.ServiceItemParamTypeBoolean + _, _, err := pricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.Error(err) + suite.Contains(err.Error(), fmt.Sprintf("trying to convert %s to an int", models.ServiceItemParamNameWeightBilled)) + + // PerUnitCents + paymentServiceItem.PaymentServiceItemParams[2].ServiceItemParamKey.Type = models.ServiceItemParamTypeBoolean + _, _, err = pricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.Error(err) + suite.Contains(err.Error(), fmt.Sprintf("trying to convert %s to an int", models.ServiceItemParamNamePerUnitCents)) + + // ReferenceDate + paymentServiceItem.PaymentServiceItemParams[1].ServiceItemParamKey.Type = models.ServiceItemParamTypeBoolean + _, _, err = pricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.Error(err) + suite.Contains(err.Error(), fmt.Sprintf("trying to convert %s to a time", models.ServiceItemParamNameReferenceDate)) + + // ContractCode + paymentServiceItem.PaymentServiceItemParams[0].ServiceItemParamKey.Type = models.ServiceItemParamTypeBoolean + _, _, err = pricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.Error(err) + suite.Contains(err.Error(), fmt.Sprintf("trying to convert %s to a string", models.ServiceItemParamNameContractCode)) + }) +} + +func (suite *GHCRateEngineServiceSuite) setupIntlDestinationFirstDayServiceItem() models.PaymentServiceItem { + contract := testdatagen.FetchOrMakeReContract(suite.DB(), testdatagen.Assertions{}) + startDate := time.Date(2018, time.January, 1, 12, 0, 0, 0, time.UTC) + endDate := time.Date(2018, time.December, 31, 12, 0, 0, 0, time.UTC) + testdatagen.FetchOrMakeReContractYear(suite.DB(), testdatagen.Assertions{ + ReContractYear: models.ReContractYear{ + Contract: contract, + ContractID: contract.ID, + StartDate: startDate, + EndDate: endDate, + Escalation: 1.0, + EscalationCompounded: 1.0, + }, + }) + return factory.BuildPaymentServiceItemWithParams( + suite.DB(), + models.ReServiceCodeIDFSIT, + []factory.CreatePaymentServiceItemParams{ + { + Key: models.ServiceItemParamNameContractCode, + KeyType: models.ServiceItemParamTypeString, + Value: contract.Code, + }, + { + Key: models.ServiceItemParamNameReferenceDate, + KeyType: models.ServiceItemParamTypeDate, + Value: idfsitTestRequestedPickupDate.Format(DateParamFormat), + }, + { + Key: models.ServiceItemParamNamePerUnitCents, + KeyType: models.ServiceItemParamTypeInteger, + Value: fmt.Sprintf("%d", int(idfsitTestPerUnitCents)), + }, + { + Key: models.ServiceItemParamNameWeightBilled, + KeyType: models.ServiceItemParamTypeInteger, + Value: strconv.Itoa(idfsitTestWeight.Int()), + }, + }, nil, nil, + ) +} diff --git a/pkg/services/ghcrateengine/intl_origin_additional_days_sit_pricer_test.go b/pkg/services/ghcrateengine/intl_origin_additional_days_sit_pricer_test.go new file mode 100644 index 00000000000..87686d5110a --- /dev/null +++ b/pkg/services/ghcrateengine/intl_origin_additional_days_sit_pricer_test.go @@ -0,0 +1,127 @@ +package ghcrateengine + +import ( + "fmt" + "strconv" + "time" + + "github.com/transcom/mymove/pkg/factory" + "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/services" + "github.com/transcom/mymove/pkg/testdatagen" + "github.com/transcom/mymove/pkg/unit" +) + +const ( + ioasitTestContractYearName = "Base Period Year 1" + ioasitTestPerUnitCents = unit.Cents(15000) + ioasitTestTotalCost = unit.Cents(1575000) + ioasitTestIsPeakPeriod = true + ioasitTestEscalationCompounded = 1.0000 + ioasitTestWeight = unit.Pound(2100) + ioasitTestPriceCents = unit.Cents(500) + ioasitNumerDaysInSIT = 5 +) + +var ioasitTestRequestedPickupDate = time.Date(testdatagen.TestYear, peakStart.month, peakStart.day, 0, 0, 0, 0, time.UTC) + +func (suite *GHCRateEngineServiceSuite) TestIntlOriginAdditionalDaySITPricer() { + pricer := NewIntlOriginAdditionalDaySITPricer() + + suite.Run("success using PaymentServiceItemParams", func() { + paymentServiceItem := suite.setupIntlOriginAdditionalDayServiceItem() + + totalCost, displayParams, err := pricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.NoError(err) + suite.Equal(ioasitTestTotalCost, totalCost) + + expectedParams := services.PricingDisplayParams{ + {Key: models.ServiceItemParamNameContractYearName, Value: ioasitTestContractYearName}, + {Key: models.ServiceItemParamNamePriceRateOrFactor, Value: FormatCents(ioasitTestPerUnitCents)}, + {Key: models.ServiceItemParamNameIsPeak, Value: FormatBool(ioasitTestIsPeakPeriod)}, + {Key: models.ServiceItemParamNameEscalationCompounded, Value: FormatEscalation(ioasitTestEscalationCompounded)}, + } + suite.validatePricerCreatedParams(expectedParams, displayParams) + }) + + suite.Run("invalid parameters to PriceUsingParams", func() { + paymentServiceItem := suite.setupIntlOriginAdditionalDayServiceItem() + + // WeightBilled + paymentServiceItem.PaymentServiceItemParams[4].ServiceItemParamKey.Type = models.ServiceItemParamTypeBoolean + _, _, err := pricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.Error(err) + suite.Contains(err.Error(), fmt.Sprintf("trying to convert %s to an int", models.ServiceItemParamNameWeightBilled)) + + // PerUnitCents + paymentServiceItem.PaymentServiceItemParams[3].ServiceItemParamKey.Type = models.ServiceItemParamTypeBoolean + _, _, err = pricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.Error(err) + suite.Contains(err.Error(), fmt.Sprintf("trying to convert %s to an int", models.ServiceItemParamNamePerUnitCents)) + + // NumberDaysSIT + paymentServiceItem.PaymentServiceItemParams[2].ServiceItemParamKey.Type = models.ServiceItemParamTypeBoolean + _, _, err = pricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.Error(err) + suite.Contains(err.Error(), fmt.Sprintf("trying to convert %s to an int", models.ServiceItemParamNameNumberDaysSIT)) + + // ReferenceDate + paymentServiceItem.PaymentServiceItemParams[1].ServiceItemParamKey.Type = models.ServiceItemParamTypeBoolean + _, _, err = pricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.Error(err) + suite.Contains(err.Error(), fmt.Sprintf("trying to convert %s to a time", models.ServiceItemParamNameReferenceDate)) + + // ContractCode + paymentServiceItem.PaymentServiceItemParams[0].ServiceItemParamKey.Type = models.ServiceItemParamTypeBoolean + _, _, err = pricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.Error(err) + suite.Contains(err.Error(), fmt.Sprintf("trying to convert %s to a string", models.ServiceItemParamNameContractCode)) + }) +} + +func (suite *GHCRateEngineServiceSuite) setupIntlOriginAdditionalDayServiceItem() models.PaymentServiceItem { + contract := testdatagen.FetchOrMakeReContract(suite.DB(), testdatagen.Assertions{}) + startDate := time.Date(2018, time.January, 1, 12, 0, 0, 0, time.UTC) + endDate := time.Date(2018, time.December, 31, 12, 0, 0, 0, time.UTC) + testdatagen.FetchOrMakeReContractYear(suite.DB(), testdatagen.Assertions{ + ReContractYear: models.ReContractYear{ + Contract: contract, + ContractID: contract.ID, + StartDate: startDate, + EndDate: endDate, + Escalation: 1.0, + EscalationCompounded: 1.0, + }, + }) + return factory.BuildPaymentServiceItemWithParams( + suite.DB(), + models.ReServiceCodeIOASIT, + []factory.CreatePaymentServiceItemParams{ + { + Key: models.ServiceItemParamNameContractCode, + KeyType: models.ServiceItemParamTypeString, + Value: contract.Code, + }, + { + Key: models.ServiceItemParamNameReferenceDate, + KeyType: models.ServiceItemParamTypeDate, + Value: ioasitTestRequestedPickupDate.Format(DateParamFormat), + }, + { + Key: models.ServiceItemParamNameNumberDaysSIT, + KeyType: models.ServiceItemParamTypeInteger, + Value: fmt.Sprintf("%d", int(ioasitNumerDaysInSIT)), + }, + { + Key: models.ServiceItemParamNamePerUnitCents, + KeyType: models.ServiceItemParamTypeInteger, + Value: fmt.Sprintf("%d", int(ioasitTestPerUnitCents)), + }, + { + Key: models.ServiceItemParamNameWeightBilled, + KeyType: models.ServiceItemParamTypeInteger, + Value: strconv.Itoa(ioasitTestWeight.Int()), + }, + }, nil, nil, + ) +} diff --git a/pkg/services/ghcrateengine/intl_origin_first_day_sit_pricer_test.go b/pkg/services/ghcrateengine/intl_origin_first_day_sit_pricer_test.go new file mode 100644 index 00000000000..ae6d9069fc4 --- /dev/null +++ b/pkg/services/ghcrateengine/intl_origin_first_day_sit_pricer_test.go @@ -0,0 +1,116 @@ +package ghcrateengine + +import ( + "fmt" + "strconv" + "time" + + "github.com/transcom/mymove/pkg/factory" + "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/services" + "github.com/transcom/mymove/pkg/testdatagen" + "github.com/transcom/mymove/pkg/unit" +) + +const ( + iofsitTestContractYearName = "Base Period Year 1" + iofsitTestPerUnitCents = unit.Cents(15000) + iofsitTestTotalCost = unit.Cents(315000) + iofsitTestIsPeakPeriod = true + iofsitTestEscalationCompounded = 1.0000 + iofsitTestWeight = unit.Pound(2100) + iofsitTestPriceCents = unit.Cents(500) + iofsitNumerDaysInSIT = 5 +) + +var iofsitTestRequestedPickupDate = time.Date(testdatagen.TestYear, peakStart.month, peakStart.day, 0, 0, 0, 0, time.UTC) + +func (suite *GHCRateEngineServiceSuite) TestIntlOriginFirstDaySITPricer() { + pricer := NewIntlOriginFirstDaySITPricer() + + suite.Run("success using PaymentServiceItemParams", func() { + paymentServiceItem := suite.setupIntlOriginFirstDayServiceItem() + + totalCost, displayParams, err := pricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.NoError(err) + suite.Equal(iofsitTestTotalCost, totalCost) + + expectedParams := services.PricingDisplayParams{ + {Key: models.ServiceItemParamNameContractYearName, Value: iofsitTestContractYearName}, + {Key: models.ServiceItemParamNamePriceRateOrFactor, Value: FormatCents(iofsitTestPerUnitCents)}, + {Key: models.ServiceItemParamNameIsPeak, Value: FormatBool(iofsitTestIsPeakPeriod)}, + {Key: models.ServiceItemParamNameEscalationCompounded, Value: FormatEscalation(iofsitTestEscalationCompounded)}, + } + suite.validatePricerCreatedParams(expectedParams, displayParams) + }) + + suite.Run("invalid parameters to PriceUsingParams", func() { + paymentServiceItem := suite.setupIntlOriginFirstDayServiceItem() + + // WeightBilled + paymentServiceItem.PaymentServiceItemParams[3].ServiceItemParamKey.Type = models.ServiceItemParamTypeBoolean + _, _, err := pricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.Error(err) + suite.Contains(err.Error(), fmt.Sprintf("trying to convert %s to an int", models.ServiceItemParamNameWeightBilled)) + + // PerUnitCents + paymentServiceItem.PaymentServiceItemParams[2].ServiceItemParamKey.Type = models.ServiceItemParamTypeBoolean + _, _, err = pricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.Error(err) + suite.Contains(err.Error(), fmt.Sprintf("trying to convert %s to an int", models.ServiceItemParamNamePerUnitCents)) + + // ReferenceDate + paymentServiceItem.PaymentServiceItemParams[1].ServiceItemParamKey.Type = models.ServiceItemParamTypeBoolean + _, _, err = pricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.Error(err) + suite.Contains(err.Error(), fmt.Sprintf("trying to convert %s to a time", models.ServiceItemParamNameReferenceDate)) + + // ContractCode + paymentServiceItem.PaymentServiceItemParams[0].ServiceItemParamKey.Type = models.ServiceItemParamTypeBoolean + _, _, err = pricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.Error(err) + suite.Contains(err.Error(), fmt.Sprintf("trying to convert %s to a string", models.ServiceItemParamNameContractCode)) + }) +} + +func (suite *GHCRateEngineServiceSuite) setupIntlOriginFirstDayServiceItem() models.PaymentServiceItem { + contract := testdatagen.FetchOrMakeReContract(suite.DB(), testdatagen.Assertions{}) + startDate := time.Date(2018, time.January, 1, 12, 0, 0, 0, time.UTC) + endDate := time.Date(2018, time.December, 31, 12, 0, 0, 0, time.UTC) + testdatagen.FetchOrMakeReContractYear(suite.DB(), testdatagen.Assertions{ + ReContractYear: models.ReContractYear{ + Contract: contract, + ContractID: contract.ID, + StartDate: startDate, + EndDate: endDate, + Escalation: 1.0, + EscalationCompounded: 1.0, + }, + }) + return factory.BuildPaymentServiceItemWithParams( + suite.DB(), + models.ReServiceCodeIOFSIT, + []factory.CreatePaymentServiceItemParams{ + { + Key: models.ServiceItemParamNameContractCode, + KeyType: models.ServiceItemParamTypeString, + Value: contract.Code, + }, + { + Key: models.ServiceItemParamNameReferenceDate, + KeyType: models.ServiceItemParamTypeDate, + Value: iofsitTestRequestedPickupDate.Format(DateParamFormat), + }, + { + Key: models.ServiceItemParamNamePerUnitCents, + KeyType: models.ServiceItemParamTypeInteger, + Value: fmt.Sprintf("%d", int(iofsitTestPerUnitCents)), + }, + { + Key: models.ServiceItemParamNameWeightBilled, + KeyType: models.ServiceItemParamTypeInteger, + Value: strconv.Itoa(iofsitTestWeight.Int()), + }, + }, nil, nil, + ) +} diff --git a/pkg/services/ghcrateengine/pricer_helpers_intl.go b/pkg/services/ghcrateengine/pricer_helpers_intl.go index bb49759504f..a7c1aa9f81f 100644 --- a/pkg/services/ghcrateengine/pricer_helpers_intl.go +++ b/pkg/services/ghcrateengine/pricer_helpers_intl.go @@ -67,7 +67,7 @@ func priceIntlPackUnpack(appCtx appcontext.AppContext, packUnpackCode models.ReS func priceIntlFirstDaySIT(appCtx appcontext.AppContext, firstDaySITCode models.ReServiceCode, contractCode string, referenceDate time.Time, weight unit.Pound, perUnitCents int) (unit.Cents, services.PricingDisplayParams, error) { if firstDaySITCode != models.ReServiceCodeIOFSIT && firstDaySITCode != models.ReServiceCodeIDFSIT { - return 0, nil, fmt.Errorf("unsupported pack/unpack code of %s", firstDaySITCode) + return 0, nil, fmt.Errorf("unsupported first day SIT code of %s", firstDaySITCode) } if len(contractCode) == 0 { return 0, nil, errors.New("ContractCode is required") @@ -119,7 +119,7 @@ func priceIntlFirstDaySIT(appCtx appcontext.AppContext, firstDaySITCode models.R func priceIntlAdditionalDaySIT(appCtx appcontext.AppContext, additionalDaySITCode models.ReServiceCode, contractCode string, referenceDate time.Time, numberOfDaysInSIT int, weight unit.Pound, perUnitCents int) (unit.Cents, services.PricingDisplayParams, error) { if additionalDaySITCode != models.ReServiceCodeIOASIT && additionalDaySITCode != models.ReServiceCodeIDASIT { - return 0, nil, fmt.Errorf("unsupported additional day of SIT code of %s", additionalDaySITCode) + return 0, nil, fmt.Errorf("unsupported additional day SIT code of %s", additionalDaySITCode) } if len(contractCode) == 0 { return 0, nil, errors.New("ContractCode is required") diff --git a/pkg/services/ghcrateengine/pricer_helpers_intl_test.go b/pkg/services/ghcrateengine/pricer_helpers_intl_test.go index 19539e4c976..848a5ad5e6a 100644 --- a/pkg/services/ghcrateengine/pricer_helpers_intl_test.go +++ b/pkg/services/ghcrateengine/pricer_helpers_intl_test.go @@ -44,3 +44,109 @@ func (suite *GHCRateEngineServiceSuite) TestPriceIntlPackUnpack() { }) } + +func (suite *GHCRateEngineServiceSuite) TestPriceIntlFirstDaySIT() { + suite.Run("success with IDFSIT", func() { + suite.setupIntlDestinationFirstDayServiceItem() + totalCost, displayParams, err := priceIntlFirstDaySIT(suite.AppContextForTest(), models.ReServiceCodeIDFSIT, testdatagen.DefaultContractCode, idfsitTestRequestedPickupDate, idfsitTestWeight, idfsitTestPerUnitCents.Int()) + suite.NoError(err) + suite.Equal(idfsitTestTotalCost, totalCost) + + expectedParams := services.PricingDisplayParams{ + {Key: models.ServiceItemParamNameContractYearName, Value: idfsitTestContractYearName}, + {Key: models.ServiceItemParamNameEscalationCompounded, Value: FormatEscalation(idfsitTestEscalationCompounded)}, + {Key: models.ServiceItemParamNameIsPeak, Value: FormatBool(idfsitTestIsPeakPeriod)}, + {Key: models.ServiceItemParamNamePriceRateOrFactor, Value: FormatCents(idfsitTestPerUnitCents)}, + } + suite.validatePricerCreatedParams(expectedParams, displayParams) + }) + + suite.Run("success with IOFSIT", func() { + suite.setupIntlOriginFirstDayServiceItem() + totalCost, displayParams, err := priceIntlFirstDaySIT(suite.AppContextForTest(), models.ReServiceCodeIOFSIT, testdatagen.DefaultContractCode, iofsitTestRequestedPickupDate, iofsitTestWeight, iofsitTestPerUnitCents.Int()) + suite.NoError(err) + suite.Equal(iofsitTestTotalCost, totalCost) + + expectedParams := services.PricingDisplayParams{ + {Key: models.ServiceItemParamNameContractYearName, Value: iofsitTestContractYearName}, + {Key: models.ServiceItemParamNameEscalationCompounded, Value: FormatEscalation(iofsitTestEscalationCompounded)}, + {Key: models.ServiceItemParamNameIsPeak, Value: FormatBool(iofsitTestIsPeakPeriod)}, + {Key: models.ServiceItemParamNamePriceRateOrFactor, Value: FormatCents(iofsitTestPerUnitCents)}, + } + suite.validatePricerCreatedParams(expectedParams, displayParams) + }) + + suite.Run("Invalid parameters to Price", func() { + suite.setupIntlDestinationFirstDayServiceItem() + _, _, err := priceIntlFirstDaySIT(suite.AppContextForTest(), models.ReServiceCodeDLH, testdatagen.DefaultContractCode, idfsitTestRequestedPickupDate, idfsitTestWeight, idfsitTestPerUnitCents.Int()) + suite.Error(err) + suite.Contains(err.Error(), "unsupported first day SIT code") + + _, _, err = priceIntlFirstDaySIT(suite.AppContextForTest(), models.ReServiceCodeIDFSIT, "", idfsitTestRequestedPickupDate, idfsitTestWeight, idfsitTestPerUnitCents.Int()) + suite.Error(err) + suite.Contains(err.Error(), "ContractCode is required") + + _, _, err = priceIntlFirstDaySIT(suite.AppContextForTest(), models.ReServiceCodeIDFSIT, testdatagen.DefaultContractCode, time.Time{}, idfsitTestWeight, idfsitTestPerUnitCents.Int()) + suite.Error(err) + suite.Contains(err.Error(), "ReferenceDate is required") + + _, _, err = priceIntlFirstDaySIT(suite.AppContextForTest(), models.ReServiceCodeIDFSIT, testdatagen.DefaultContractCode, idfsitTestRequestedPickupDate, idfsitTestWeight, 0) + suite.Error(err) + suite.Contains(err.Error(), "PerUnitCents is required") + }) +} + +func (suite *GHCRateEngineServiceSuite) TestPriceIntlAdditionalDaySIT() { + suite.Run("success with IDASIT", func() { + suite.setupIntlDestinationAdditionalDayServiceItem() + totalCost, displayParams, err := priceIntlAdditionalDaySIT(suite.AppContextForTest(), models.ReServiceCodeIDASIT, testdatagen.DefaultContractCode, idasitTestRequestedPickupDate, idasitNumerDaysInSIT, idasitTestWeight, idasitTestPerUnitCents.Int()) + suite.NoError(err) + suite.Equal(idasitTestTotalCost, totalCost) + + expectedParams := services.PricingDisplayParams{ + {Key: models.ServiceItemParamNameContractYearName, Value: idasitTestContractYearName}, + {Key: models.ServiceItemParamNameEscalationCompounded, Value: FormatEscalation(idasitTestEscalationCompounded)}, + {Key: models.ServiceItemParamNameIsPeak, Value: FormatBool(idasitTestIsPeakPeriod)}, + {Key: models.ServiceItemParamNamePriceRateOrFactor, Value: FormatCents(idasitTestPerUnitCents)}, + } + suite.validatePricerCreatedParams(expectedParams, displayParams) + }) + + suite.Run("success with IOASIT", func() { + suite.setupIntlOriginAdditionalDayServiceItem() + totalCost, displayParams, err := priceIntlAdditionalDaySIT(suite.AppContextForTest(), models.ReServiceCodeIOASIT, testdatagen.DefaultContractCode, ioasitTestRequestedPickupDate, idasitNumerDaysInSIT, ioasitTestWeight, ioasitTestPerUnitCents.Int()) + suite.NoError(err) + suite.Equal(ioasitTestTotalCost, totalCost) + + expectedParams := services.PricingDisplayParams{ + {Key: models.ServiceItemParamNameContractYearName, Value: ioasitTestContractYearName}, + {Key: models.ServiceItemParamNameEscalationCompounded, Value: FormatEscalation(ioasitTestEscalationCompounded)}, + {Key: models.ServiceItemParamNameIsPeak, Value: FormatBool(ioasitTestIsPeakPeriod)}, + {Key: models.ServiceItemParamNamePriceRateOrFactor, Value: FormatCents(ioasitTestPerUnitCents)}, + } + suite.validatePricerCreatedParams(expectedParams, displayParams) + }) + + suite.Run("Invalid parameters to Price", func() { + suite.setupIntlDestinationAdditionalDayServiceItem() + _, _, err := priceIntlAdditionalDaySIT(suite.AppContextForTest(), models.ReServiceCodeDLH, testdatagen.DefaultContractCode, idasitTestRequestedPickupDate, idasitNumerDaysInSIT, idasitTestWeight, idasitTestPerUnitCents.Int()) + suite.Error(err) + suite.Contains(err.Error(), "unsupported additional day SIT code") + + _, _, err = priceIntlAdditionalDaySIT(suite.AppContextForTest(), models.ReServiceCodeIDASIT, "", idasitTestRequestedPickupDate, idasitNumerDaysInSIT, idasitTestWeight, idasitTestPerUnitCents.Int()) + suite.Error(err) + suite.Contains(err.Error(), "ContractCode is required") + + _, _, err = priceIntlAdditionalDaySIT(suite.AppContextForTest(), models.ReServiceCodeIDASIT, testdatagen.DefaultContractCode, time.Time{}, idasitNumerDaysInSIT, idasitTestWeight, idasitTestPerUnitCents.Int()) + suite.Error(err) + suite.Contains(err.Error(), "ReferenceDate is required") + + _, _, err = priceIntlAdditionalDaySIT(suite.AppContextForTest(), models.ReServiceCodeIDASIT, testdatagen.DefaultContractCode, idasitTestRequestedPickupDate, idasitNumerDaysInSIT, idasitTestWeight, 0) + suite.Error(err) + suite.Contains(err.Error(), "PerUnitCents is required") + + _, _, err = priceIntlAdditionalDaySIT(suite.AppContextForTest(), models.ReServiceCodeIDASIT, testdatagen.DefaultContractCode, idasitTestRequestedPickupDate, 0, idasitTestWeight, 0) + suite.Error(err) + suite.Contains(err.Error(), "NumberDaysSIT is required") + }) +} From ff8399f7266d2501bf5c5490df9606279502f592 Mon Sep 17 00:00:00 2001 From: Daniel Jordan Date: Tue, 21 Jan 2025 19:31:03 +0000 Subject: [PATCH 11/18] added tests for ppm estimator --- .../ppm_closeout/ppm_closeout_test.go | 2 +- pkg/services/ppmshipment/ppm_estimator.go | 2 +- .../ppmshipment/ppm_estimator_test.go | 547 +++++++++++++++++- 3 files changed, 523 insertions(+), 28 deletions(-) diff --git a/pkg/services/ppm_closeout/ppm_closeout_test.go b/pkg/services/ppm_closeout/ppm_closeout_test.go index c1479c140ea..084c086908c 100644 --- a/pkg/services/ppm_closeout/ppm_closeout_test.go +++ b/pkg/services/ppm_closeout/ppm_closeout_test.go @@ -24,7 +24,7 @@ const ( ppmBuildWaitingOnCustomer = "waitingOnCustomer" ) -func (suite *PPMCloseoutSuite) TestPPMShipmentCreator() { +func (suite *PPMCloseoutSuite) TestPPMShipmentCloseout() { // One-time test setup mockedPlanner := &mocks.Planner{} diff --git a/pkg/services/ppmshipment/ppm_estimator.go b/pkg/services/ppmshipment/ppm_estimator.go index 1d3d4916f43..c80661561b0 100644 --- a/pkg/services/ppmshipment/ppm_estimator.go +++ b/pkg/services/ppmshipment/ppm_estimator.go @@ -257,7 +257,7 @@ func (f *estimatePPM) estimateIncentive(appCtx appcontext.AppContext, oldPPMShip newPPMShipment.HasRequestedAdvance = nil newPPMShipment.AdvanceAmountRequested = nil - estimatedIncentive, err = f.CalculateOCONUSIncentive(appCtx, newPPMShipment.ID, *pickupAddress, *destinationAddress, contractDate, newPPMShipment.EstimatedWeight.Int(), false, false, true) + estimatedIncentive, err = f.CalculateOCONUSIncentive(appCtx, newPPMShipment.ID, *pickupAddress, *destinationAddress, contractDate, newPPMShipment.EstimatedWeight.Int(), true, false, false) if err != nil { return nil, nil, fmt.Errorf("failed to calculate estimated PPM incentive: %w", err) } diff --git a/pkg/services/ppmshipment/ppm_estimator_test.go b/pkg/services/ppmshipment/ppm_estimator_test.go index 71b0749a7ea..98fc639b472 100644 --- a/pkg/services/ppmshipment/ppm_estimator_test.go +++ b/pkg/services/ppmshipment/ppm_estimator_test.go @@ -696,6 +696,7 @@ func (suite *PPMShipmentSuite) TestPPMEstimator() { }, }, }, nil) + setupPricerData() newPPM := oldPPMShipment newPPM.HasProGear = models.BoolPointer(false) @@ -1559,6 +1560,7 @@ func (suite *PPMShipmentSuite) TestPPMEstimator() { }) suite.Run("Final Incentive - does not change when required fields are the same", func() { + setupPricerData() oldPPMShipment := factory.BuildPPMShipment(suite.DB(), []factory.Customization{ { Model: models.PPMShipment{ @@ -1607,6 +1609,7 @@ func (suite *PPMShipmentSuite) TestPPMEstimator() { }) suite.Run("Final Incentive - set to nil when missing info", func() { + setupPricerData() oldPPMShipment := factory.BuildPPMShipment(suite.DB(), []factory.Customization{ { Model: models.PPMShipment{ @@ -1698,7 +1701,7 @@ func (suite *PPMShipmentSuite) TestPPMEstimator() { }, }, { - Model: &models.Address{ + Model: models.Address{ StreetAddress1: "987 Other Avenue", StreetAddress2: models.StringPointer("P.O. Box 1234"), StreetAddress3: models.StringPointer("c/o Another Person"), @@ -1710,7 +1713,7 @@ func (suite *PPMShipmentSuite) TestPPMEstimator() { Type: &factory.Addresses.PickupAddress, }, { - Model: &models.Address{ + Model: models.Address{ StreetAddress1: "987 Other Avenue", StreetAddress2: models.StringPointer("P.O. Box 12345"), StreetAddress3: models.StringPointer("c/o Another Person"), @@ -1760,30 +1763,6 @@ func (suite *PPMShipmentSuite) TestPPMEstimator() { SITEstimatedDepartureDate: &entryDate, }, }, - { - Model: models.Address{ - StreetAddress1: "987 Other Avenue", - StreetAddress2: models.StringPointer("P.O. Box 1234"), - StreetAddress3: models.StringPointer("c/o Another Person"), - City: "Des Moines", - State: "IA", - PostalCode: "50309", - County: models.StringPointer("POLK"), - }, - Type: &factory.Addresses.PickupAddress, - }, - { - Model: models.Address{ - StreetAddress1: "987 Other Avenue", - StreetAddress2: models.StringPointer("P.O. Box 12345"), - StreetAddress3: models.StringPointer("c/o Another Person"), - City: "Fort Eisenhower", - State: "GA", - PostalCode: "50309", - County: models.StringPointer("COLUMBIA"), - }, - Type: &factory.Addresses.DeliveryAddress, - }, { Model: mtoShipment, LinkOnly: true, @@ -2049,3 +2028,519 @@ func (suite *PPMShipmentSuite) TestPPMEstimator() { }) }) } + +func (suite *PPMShipmentSuite) TestInternationalPPMEstimator() { + planner := &mocks.Planner{} + paymentRequestHelper := &prhelpermocks.Helper{} + ppmEstimator := NewEstimatePPM(planner, paymentRequestHelper) + + setupPricerData := func() { + contract := testdatagen.FetchOrMakeReContract(suite.DB(), testdatagen.Assertions{}) + startDate := time.Date(2020, time.January, 1, 12, 0, 0, 0, time.UTC) + endDate := time.Date(2020, time.December, 31, 12, 0, 0, 0, time.UTC) + testdatagen.FetchOrMakeReContractYear(suite.DB(), testdatagen.Assertions{ + ReContractYear: models.ReContractYear{ + Contract: contract, + ContractID: contract.ID, + StartDate: startDate, + EndDate: endDate, + Escalation: 1.0, + EscalationCompounded: 1.0, + }, + }) + } + + suite.Run("Estimated Incentive", func() { + suite.Run("Estimated Incentive - Success using estimated weight and not db authorized weight for CONUS -> OCONUS", func() { + ppm := factory.BuildPPMShipment(suite.DB(), []factory.Customization{ + { + Model: models.MTOShipment{ + MarketCode: models.MarketCodeInternational, + }, + }, + { + Model: models.Address{ + StreetAddress1: "Tester Address", + City: "Tulsa", + State: "OK", + PostalCode: "74133", + }, + Type: &factory.Addresses.PickupAddress, + }, + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "JBER", + State: "AK", + PostalCode: "99505", + IsOconus: models.BoolPointer(true), + }, + Type: &factory.Addresses.DeliveryAddress, + }, + }, nil) + + setupPricerData() + + estimatedWeight := unit.Pound(5000) + newPPM := ppm + newPPM.EstimatedWeight = &estimatedWeight + + planner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), + "74133", "98424", true, true).Return(3000, nil) + + ppmEstimate, _, err := ppmEstimator.EstimateIncentiveWithDefaultChecks(suite.AppContextForTest(), ppm, &newPPM) + suite.NilOrNoVerrs(err) + suite.NotNil(ppmEstimate) + + // it should've called from the pickup -> port and NOT pickup -> dest + planner.AssertCalled(suite.T(), "ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), + "74133", "98424", true, true) + suite.Equal(unit.Cents(459178), *ppmEstimate) + }) + + suite.Run("Estimated Incentive - Success using estimated weight and not db authorized weight for OCONUS -> CONUS", func() { + ppm := factory.BuildPPMShipment(suite.DB(), []factory.Customization{ + { + Model: models.MTOShipment{ + MarketCode: models.MarketCodeInternational, + }, + }, + { + Model: models.Address{ + StreetAddress1: "Tester Address", + City: "Tulsa", + State: "OK", + PostalCode: "74133", + }, + Type: &factory.Addresses.DeliveryAddress, + }, + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "JBER", + State: "AK", + PostalCode: "99505", + IsOconus: models.BoolPointer(true), + }, + Type: &factory.Addresses.PickupAddress, + }, + }, nil) + + setupPricerData() + + estimatedWeight := unit.Pound(5000) + newPPM := ppm + newPPM.EstimatedWeight = &estimatedWeight + + planner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), + "98424", "74133", true, true).Return(3000, nil) + + ppmEstimate, _, err := ppmEstimator.EstimateIncentiveWithDefaultChecks(suite.AppContextForTest(), ppm, &newPPM) + suite.NilOrNoVerrs(err) + suite.NotNil(ppmEstimate) + + // it should've called from the pickup -> port and NOT pickup -> dest + planner.AssertCalled(suite.T(), "ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), + "98424", "74133", true, true) + suite.Equal(unit.Cents(423178), *ppmEstimate) + }) + }) + + suite.Run("Max Incentive", func() { + suite.Run("Max Incentive - Success using db authorized weight and not estimated for CONUS -> OCONUS", func() { + oconusAddress := factory.BuildAddress(suite.DB(), []factory.Customization{ + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "JBER", + State: "AK", + PostalCode: "99505", + IsOconus: models.BoolPointer(true), + }, + }, + }, nil) + destDutyLocation := factory.BuildDutyLocation(suite.DB(), []factory.Customization{ + { + Model: models.DutyLocation{ + Name: "Test OCONUS Duty Location", + AddressID: oconusAddress.ID, + }, + }, + }, nil) + order := factory.BuildOrder(suite.DB(), []factory.Customization{ + { + Model: models.Order{ + NewDutyLocationID: destDutyLocation.ID, + }, + }, + }, nil) + // when the PPM shipment is in draft, we use the estimated weight and not the db authorized weight + ppm := factory.BuildPPMShipment(suite.DB(), []factory.Customization{ + { + Model: models.Move{ + OrdersID: order.ID, + }, + }, + { + Model: models.MTOShipment{ + MarketCode: models.MarketCodeInternational, + }, + }, + { + Model: models.Address{ + StreetAddress1: "Tester Address", + City: "Tulsa", + State: "OK", + PostalCode: "74133", + }, + Type: &factory.Addresses.PickupAddress, + }, + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "JBER", + State: "AK", + PostalCode: "99505", + IsOconus: models.BoolPointer(true), + }, + Type: &factory.Addresses.DeliveryAddress, + }, + }, nil) + + setupPricerData() + + estimatedWeight := unit.Pound(5000) + newPPM := ppm + newPPM.EstimatedWeight = &estimatedWeight + + // DTOD will be called to get the distance between the origin duty location & the Tacoma Port ZIP + planner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), + "50309", "98424", true, true).Return(3000, nil) + + ppmMaxIncentive, err := ppmEstimator.MaxIncentive(suite.AppContextForTest(), ppm, &newPPM) + suite.NilOrNoVerrs(err) + suite.NotNil(ppmMaxIncentive) + + // it should've called from the pickup -> port and NOT pickup -> dest + planner.AssertCalled(suite.T(), "ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), + "50309", "98424", true, true) + suite.Equal(unit.Cents(656532), *ppmMaxIncentive) + }) + + suite.Run("Max Incentive - Success using db authorized weight and not estimated for OCONUS -> CONUS", func() { + oconusAddress := factory.BuildAddress(suite.DB(), []factory.Customization{ + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "JBER", + State: "AK", + PostalCode: "99505", + IsOconus: models.BoolPointer(true), + }, + }, + }, nil) + pickupDutyLocation := factory.BuildDutyLocation(suite.DB(), []factory.Customization{ + { + Model: models.DutyLocation{ + Name: "Test OCONUS Duty Location", + AddressID: oconusAddress.ID, + }, + }, + }, nil) + order := factory.BuildOrder(suite.DB(), []factory.Customization{ + { + Model: models.Order{ + OriginDutyLocationID: &pickupDutyLocation.ID, + }, + }, + }, nil) + // when the PPM shipment is in draft, we use the estimated weight and not the db authorized weight + ppm := factory.BuildPPMShipment(suite.DB(), []factory.Customization{ + { + Model: models.Move{ + OrdersID: order.ID, + }, + }, + { + Model: models.MTOShipment{ + MarketCode: models.MarketCodeInternational, + }, + }, + { + Model: models.Address{ + StreetAddress1: "Tester Address", + City: "Tulsa", + State: "OK", + PostalCode: "74133", + }, + Type: &factory.Addresses.PickupAddress, + }, + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "JBER", + State: "AK", + PostalCode: "99505", + IsOconus: models.BoolPointer(true), + }, + Type: &factory.Addresses.DeliveryAddress, + }, + }, nil) + + setupPricerData() + + estimatedWeight := unit.Pound(5000) + newPPM := ppm + newPPM.EstimatedWeight = &estimatedWeight + + // DTOD will be called to get the distance between the origin duty location & the Tacoma Port ZIP + planner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), + "98424", "30813", true, true).Return(3000, nil) + + ppmMaxIncentive, err := ppmEstimator.MaxIncentive(suite.AppContextForTest(), ppm, &newPPM) + suite.NilOrNoVerrs(err) + suite.NotNil(ppmMaxIncentive) + + // it should've called from the pickup -> port and NOT pickup -> dest + planner.AssertCalled(suite.T(), "ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), + "98424", "30813", true, true) + suite.Equal(unit.Cents(676692), *ppmMaxIncentive) + }) + }) + + suite.Run("Final Incentive", func() { + suite.Run("Final Incentive - Success using estimated weight for CONUS -> OCONUS", func() { + updatedMoveDate := time.Date(2020, time.March, 15, 0, 0, 0, 0, time.UTC) + ppm := factory.BuildPPMShipment(suite.DB(), []factory.Customization{ + { + Model: models.PPMShipment{ + ActualMoveDate: models.TimePointer(updatedMoveDate), + Status: models.PPMShipmentStatusWaitingOnCustomer, + EstimatedWeight: models.PoundPointer(4000), + }, + }, + { + Model: models.MTOShipment{ + MarketCode: models.MarketCodeInternational, + }, + }, + { + Model: models.Address{ + StreetAddress1: "Tester Address", + City: "Tulsa", + State: "OK", + PostalCode: "74133", + }, + Type: &factory.Addresses.PickupAddress, + }, + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "JBER", + State: "AK", + PostalCode: "99505", + IsOconus: models.BoolPointer(true), + }, + Type: &factory.Addresses.DeliveryAddress, + }, + }, nil) + + newPPM := ppm + newFullWeight := unit.Pound(8000) + newEmptyWeight := unit.Pound(3000) + newPPM.WeightTickets = models.WeightTickets{ + factory.BuildWeightTicket(suite.DB(), []factory.Customization{ + { + Model: models.WeightTicket{ + FullWeight: &newFullWeight, + EmptyWeight: &newEmptyWeight, + }, + }, + }, nil), + } + + setupPricerData() + + planner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), + "74133", "98424", true, true).Return(3000, nil) + + ppmFinalIncentive, err := ppmEstimator.FinalIncentiveWithDefaultChecks(suite.AppContextForTest(), ppm, &newPPM) + suite.NilOrNoVerrs(err) + suite.NotNil(ppmFinalIncentive) + + // it should've called from the pickup -> port and NOT pickup -> dest + planner.AssertCalled(suite.T(), "ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), + "74133", "98424", true, true) + suite.Equal(unit.Cents(459178), *ppmFinalIncentive) + }) + + suite.Run("Final Incentive - Success using estimated weight for OCONUS -> CONUS", func() { + updatedMoveDate := time.Date(2020, time.March, 15, 0, 0, 0, 0, time.UTC) + ppm := factory.BuildPPMShipment(suite.DB(), []factory.Customization{ + { + Model: models.PPMShipment{ + ActualMoveDate: models.TimePointer(updatedMoveDate), + Status: models.PPMShipmentStatusWaitingOnCustomer, + EstimatedWeight: models.PoundPointer(4000), + }, + }, + { + Model: models.MTOShipment{ + MarketCode: models.MarketCodeInternational, + }, + }, + { + Model: models.Address{ + StreetAddress1: "Tester Address", + City: "Tulsa", + State: "OK", + PostalCode: "74133", + }, + Type: &factory.Addresses.DeliveryAddress, + }, + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "JBER", + State: "AK", + PostalCode: "99505", + IsOconus: models.BoolPointer(true), + }, + Type: &factory.Addresses.PickupAddress, + }, + }, nil) + + newPPM := ppm + newFullWeight := unit.Pound(8000) + newEmptyWeight := unit.Pound(3000) + newPPM.WeightTickets = models.WeightTickets{ + factory.BuildWeightTicket(suite.DB(), []factory.Customization{ + { + Model: models.WeightTicket{ + FullWeight: &newFullWeight, + EmptyWeight: &newEmptyWeight, + }, + }, + }, nil), + } + + setupPricerData() + + planner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), + "98424", "74133", true, true).Return(3000, nil) + + ppmFinalIncentive, err := ppmEstimator.FinalIncentiveWithDefaultChecks(suite.AppContextForTest(), ppm, &newPPM) + suite.NilOrNoVerrs(err) + suite.NotNil(ppmFinalIncentive) + + // it should've called from the pickup -> port and NOT pickup -> dest + planner.AssertCalled(suite.T(), "ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), + "98424", "74133", true, true) + suite.Equal(unit.Cents(423178), *ppmFinalIncentive) + }) + }) + + suite.Run("SIT Costs for OCONUS PPMs", func() { + suite.Run("CalculateSITCost - Success using estimated weight for CONUS -> OCONUS", func() { + originLocation := models.SITLocationTypeOrigin + entryDate := time.Date(2020, time.March, 15, 0, 0, 0, 0, time.UTC) + ppm := factory.BuildPPMShipment(suite.DB(), []factory.Customization{ + { + Model: models.PPMShipment{ + EstimatedWeight: models.PoundPointer(4000), + SITExpected: models.BoolPointer(true), + SITLocation: &originLocation, + SITEstimatedWeight: models.PoundPointer(unit.Pound(2000)), + SITEstimatedEntryDate: &entryDate, + SITEstimatedDepartureDate: models.TimePointer(entryDate.Add(time.Hour * 24 * 30)), + }, + }, + { + Model: models.MTOShipment{ + MarketCode: models.MarketCodeInternational, + }, + }, + { + Model: models.Address{ + StreetAddress1: "Tester Address", + City: "Tulsa", + State: "OK", + PostalCode: "74133", + }, + Type: &factory.Addresses.PickupAddress, + }, + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "JBER", + State: "AK", + PostalCode: "99505", + IsOconus: models.BoolPointer(true), + }, + Type: &factory.Addresses.DeliveryAddress, + }, + }, nil) + + newPPM := ppm + newEstimatedWeight := models.PoundPointer(5500) + newPPM.SITEstimatedWeight = newEstimatedWeight + setupPricerData() + + _, estimatedSITCost, err := ppmEstimator.EstimateIncentiveWithDefaultChecks(suite.AppContextForTest(), ppm, &newPPM) + suite.NilOrNoVerrs(err) + suite.NotNil(estimatedSITCost) + suite.Equal(unit.Cents(24360), *estimatedSITCost) + }) + + suite.Run("CalculateSITCost - Success using estimated weight for CONUS -> OCONUS", func() { + originLocation := models.SITLocationTypeDestination + entryDate := time.Date(2020, time.March, 15, 0, 0, 0, 0, time.UTC) + ppm := factory.BuildPPMShipment(suite.DB(), []factory.Customization{ + { + Model: models.PPMShipment{ + EstimatedWeight: models.PoundPointer(4000), + SITExpected: models.BoolPointer(true), + SITLocation: &originLocation, + SITEstimatedWeight: models.PoundPointer(unit.Pound(2000)), + SITEstimatedEntryDate: &entryDate, + SITEstimatedDepartureDate: models.TimePointer(entryDate.Add(time.Hour * 24 * 30)), + }, + }, + { + Model: models.MTOShipment{ + MarketCode: models.MarketCodeInternational, + }, + }, + { + Model: models.Address{ + StreetAddress1: "Tester Address", + City: "Tulsa", + State: "OK", + PostalCode: "74133", + }, + Type: &factory.Addresses.PickupAddress, + }, + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "JBER", + State: "AK", + PostalCode: "99505", + IsOconus: models.BoolPointer(true), + }, + Type: &factory.Addresses.DeliveryAddress, + }, + }, nil) + + newPPM := ppm + newEstimatedWeight := models.PoundPointer(5500) + newPPM.SITEstimatedWeight = newEstimatedWeight + setupPricerData() + + _, estimatedSITCost, err := ppmEstimator.EstimateIncentiveWithDefaultChecks(suite.AppContextForTest(), ppm, &newPPM) + suite.NilOrNoVerrs(err) + suite.NotNil(estimatedSITCost) + suite.Equal(unit.Cents(41080), *estimatedSITCost) + }) + }) +} From 2d9f515fa8211672b540d65de4bc1d034e6bd913 Mon Sep 17 00:00:00 2001 From: Daniel Jordan Date: Tue, 21 Jan 2025 20:08:42 +0000 Subject: [PATCH 12/18] fixing UI tests --- .../PPM/PPMHeaderSummary/HeaderSection.jsx | 106 ++++++++---------- .../PPMHeaderSummary/HeaderSection.test.jsx | 20 +++- .../PPM/PPMHeaderSummary/PPMHeaderSummary.jsx | 2 - .../PPMHeaderSummary.test.jsx | 2 +- src/shared/constants.js | 5 - 5 files changed, 67 insertions(+), 68 deletions(-) diff --git a/src/components/Office/PPM/PPMHeaderSummary/HeaderSection.jsx b/src/components/Office/PPM/PPMHeaderSummary/HeaderSection.jsx index 2344a110ab2..a9201b3ec3f 100644 --- a/src/components/Office/PPM/PPMHeaderSummary/HeaderSection.jsx +++ b/src/components/Office/PPM/PPMHeaderSummary/HeaderSection.jsx @@ -271,7 +271,7 @@ const getSectionMarkup = (sectionInfo, handleEditOnClick, isFetchingItems, updat case sectionTypes.incentiveFactors: return (
- {sectionInfo.haulPrice > 0 ?? ( + {sectionInfo.haulPrice > 0 && (
@@ -296,54 +296,48 @@ 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') ? ( + + ) : ( + `$${formatCents(sectionInfo.unpackPrice)}` + )} + +
+ +
+ + + {isFetchingItems && isRecalulatedItem('dop') ? ( + + ) : ( + `$${formatCents(sectionInfo.dop)}` + )} + +
+ +
+ + + {isFetchingItems && isRecalulatedItem('ddp') ? ( + + ) : ( + `$${formatCents(sectionInfo.ddp)}` + )} + +
@@ -374,14 +368,12 @@ const getSectionMarkup = (sectionInfo, handleEditOnClick, isFetchingItems, updat )}
- {sectionInfo.sitReimbursement > 0 ?? ( -
- - - ${formatCents(sectionInfo.sitReimbursement)} - -
- )} +
+ + + ${formatCents(sectionInfo.sitReimbursement)} + +
); diff --git a/src/components/Office/PPM/PPMHeaderSummary/HeaderSection.test.jsx b/src/components/Office/PPM/PPMHeaderSummary/HeaderSection.test.jsx index 7fb05d4da7d..4b173db4bdb 100644 --- a/src/components/Office/PPM/PPMHeaderSummary/HeaderSection.test.jsx +++ b/src/components/Office/PPM/PPMHeaderSummary/HeaderSection.test.jsx @@ -219,16 +219,24 @@ const incentivesAdvanceReceivedZeroProps = { setIsSubmitting: jest.fn(), }; +const HAUL_TYPES = { + SHORTHAUL: 'Shorthaul', + LINEHAUL: 'Linehaul', +}; + const incentiveFactorsProps = { sectionInfo: { type: 'incentiveFactors', - haulType: 'Linehaul', + haulType: HAUL_TYPES.LINEHAUL, haulPrice: 6892668, - haulFSC: -143, + haulFSC: 143, packPrice: 20000, unpackPrice: 10000, dop: 15640, ddp: 34640, + intlPackPrice: 1234, + intlUnpackPrice: 12345, + intlLinehaulPrice: 123456, sitReimbursement: 30000, }, }; @@ -469,7 +477,7 @@ describe('PPMHeaderSummary component', () => { expect(screen.getByText('Linehaul Price')).toBeInTheDocument(); expect(screen.getByTestId('haulPrice')).toHaveTextContent('$68,926.68'); expect(screen.getByText('Linehaul Fuel Rate Adjustment')).toBeInTheDocument(); - expect(screen.getByTestId('haulFSC')).toHaveTextContent('-$1.43'); + expect(screen.getByTestId('haulFSC')).toHaveTextContent('$1.43'); expect(screen.getByText('Packing Charge')).toBeInTheDocument(); expect(screen.getByTestId('packPrice')).toHaveTextContent('$200.00'); expect(screen.getByText('Unpacking Charge')).toBeInTheDocument(); @@ -478,6 +486,12 @@ describe('PPMHeaderSummary component', () => { expect(screen.getByTestId('originPrice')).toHaveTextContent('$156.40'); expect(screen.getByText('Destination Price')).toBeInTheDocument(); expect(screen.getByTestId('destinationPrice')).toHaveTextContent('$346.40'); + expect(screen.getByText('International Packing Charge')).toBeInTheDocument(); + expect(screen.getByTestId('intlPackPrice')).toHaveTextContent('$12.34'); + expect(screen.getByText('International Unpacking Charge')).toBeInTheDocument(); + expect(screen.getByTestId('intlUnpackPrice')).toHaveTextContent('$123.45'); + expect(screen.getByText('International Shipping & Linehaul Charge')).toBeInTheDocument(); + expect(screen.getByTestId('intlLinehaulPrice')).toHaveTextContent('$1,234.56'); expect(screen.getByTestId('sitReimbursement')).toHaveTextContent('$300.00'); }); diff --git a/src/components/Office/PPM/PPMHeaderSummary/PPMHeaderSummary.jsx b/src/components/Office/PPM/PPMHeaderSummary/PPMHeaderSummary.jsx index 03a81392151..23d84460bf7 100644 --- a/src/components/Office/PPM/PPMHeaderSummary/PPMHeaderSummary.jsx +++ b/src/components/Office/PPM/PPMHeaderSummary/PPMHeaderSummary.jsx @@ -10,7 +10,6 @@ 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); @@ -79,7 +78,6 @@ 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/components/Office/PPM/PPMHeaderSummary/PPMHeaderSummary.test.jsx b/src/components/Office/PPM/PPMHeaderSummary/PPMHeaderSummary.test.jsx index 89c94406ccc..6d938da22b2 100644 --- a/src/components/Office/PPM/PPMHeaderSummary/PPMHeaderSummary.test.jsx +++ b/src/components/Office/PPM/PPMHeaderSummary/PPMHeaderSummary.test.jsx @@ -167,7 +167,7 @@ const defaultProps = { describe('PPMHeaderSummary component', () => { describe('displays form', () => { - it('renders blank form on load with defaults', async () => { + it('renders default values', async () => { usePPMShipmentDocsQueries.mockReturnValue(useEditShipmentQueriesReturnValue); useEditShipmentQueries.mockReturnValue(useEditShipmentQueriesReturnValue); renderWithProviders(, mockRoutingConfig); diff --git a/src/shared/constants.js b/src/shared/constants.js index 947dd877ba4..56b7601c585 100644 --- a/src/shared/constants.js +++ b/src/shared/constants.js @@ -236,9 +236,4 @@ 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]; From 382187c9fbd214a2b87d38b0803fd0c0b52d8121 Mon Sep 17 00:00:00 2001 From: Daniel Jordan Date: Tue, 21 Jan 2025 20:16:55 +0000 Subject: [PATCH 13/18] removing check from test since added logic to reduce overquerying incentives --- pkg/handlers/ghcapi/mto_shipment_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/pkg/handlers/ghcapi/mto_shipment_test.go b/pkg/handlers/ghcapi/mto_shipment_test.go index 858c571ce0e..ee40b31e2b7 100644 --- a/pkg/handlers/ghcapi/mto_shipment_test.go +++ b/pkg/handlers/ghcapi/mto_shipment_test.go @@ -4323,9 +4323,7 @@ func (suite *HandlerSuite) TestUpdateShipmentHandler() { suite.Equal(handlers.FmtPoundPtr(&sitEstimatedWeight), updatedShipment.PpmShipment.SitEstimatedWeight) suite.Equal(handlers.FmtDate(sitEstimatedEntryDate), updatedShipment.PpmShipment.SitEstimatedEntryDate) suite.Equal(handlers.FmtDate(sitEstimatedDepartureDate), updatedShipment.PpmShipment.SitEstimatedDepartureDate) - suite.Equal(int64(sitEstimatedCost), *updatedShipment.PpmShipment.SitEstimatedCost) suite.Equal(handlers.FmtPoundPtr(&estimatedWeight), updatedShipment.PpmShipment.EstimatedWeight) - suite.Equal(int64(estimatedIncentive), *updatedShipment.PpmShipment.EstimatedIncentive) suite.Equal(handlers.FmtBool(hasProGear), updatedShipment.PpmShipment.HasProGear) suite.Equal(handlers.FmtPoundPtr(&proGearWeight), updatedShipment.PpmShipment.ProGearWeight) suite.Equal(handlers.FmtPoundPtr(&spouseProGearWeight), updatedShipment.PpmShipment.SpouseProGearWeight) From e0b9e2148c158b656974dbaf2c8078088f643785 Mon Sep 17 00:00:00 2001 From: Daniel Jordan Date: Tue, 21 Jan 2025 20:29:57 +0000 Subject: [PATCH 14/18] fixes --- ...64752_add_ppm_estimated_incentive_proc.up.sql | 16 ++++++---------- pkg/services/ppmshipment/ppm_estimator.go | 2 +- 2 files changed, 7 insertions(+), 11 deletions(-) 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 4aecb8a3656..896af50b01e 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 @@ -47,7 +47,7 @@ END; $$ LANGUAGE plpgsql; --- db proc that will calculate a PPM's incentive +-- db func that will calculate a PPM's incentives -- this is used for estimated/final/max incentives CREATE OR REPLACE FUNCTION calculate_ppm_incentive( ppm_id UUID, @@ -164,7 +164,7 @@ END; $$ LANGUAGE plpgsql; --- db proc that will calculate a PPM's SIT cost +-- db func 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, @@ -185,30 +185,27 @@ DECLARE sit_rate_area_id UUID; service_id UUID; BEGIN - -- Validate SIT days + -- make sure we validate parameters 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 + -- 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( @@ -221,7 +218,7 @@ BEGIN ) * (weight / 100)::NUMERIC * 100 )::INT; - -- Calculate additional day SIT cost + -- 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( @@ -234,10 +231,9 @@ BEGIN ) * (weight / 100)::NUMERIC * 100 * sit_days )::INT; - -- Calculate total SIT cost + -- add em up 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/services/ppmshipment/ppm_estimator.go b/pkg/services/ppmshipment/ppm_estimator.go index c80661561b0..b925cf6bfcf 100644 --- a/pkg/services/ppmshipment/ppm_estimator.go +++ b/pkg/services/ppmshipment/ppm_estimator.go @@ -1360,7 +1360,7 @@ func StorageServiceItems(ppmShipment models.PPMShipment, locationType models.SIT } } return []models.MTOServiceItem{ - {ReService: models.ReService{Code: models.ReServiceCodeDDFSIT}, MTOShipmentID: &mtoShipmentID}} + {ReService: models.ReService{Code: models.ReServiceCodeIDFSIT}, MTOShipmentID: &mtoShipmentID}} } return nil From d55577309c6496f35a3c3f6a51720280637bf46f Mon Sep 17 00:00:00 2001 From: Daniel Jordan Date: Tue, 21 Jan 2025 21:42:27 +0000 Subject: [PATCH 15/18] adding some more tests for SIT costs and breakdowns --- .../ppmshipment/ppm_estimator_test.go | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/pkg/services/ppmshipment/ppm_estimator_test.go b/pkg/services/ppmshipment/ppm_estimator_test.go index 98fc639b472..1e0cf6cf6c2 100644 --- a/pkg/services/ppmshipment/ppm_estimator_test.go +++ b/pkg/services/ppmshipment/ppm_estimator_test.go @@ -2542,5 +2542,109 @@ func (suite *PPMShipmentSuite) TestInternationalPPMEstimator() { suite.NotNil(estimatedSITCost) suite.Equal(unit.Cents(41080), *estimatedSITCost) }) + + suite.Run("CalculatePPMSITEstimatedCost - Success for OCONUS PPM", func() { + originLocation := models.SITLocationTypeDestination + entryDate := time.Date(2020, time.March, 15, 0, 0, 0, 0, time.UTC) + ppm := factory.BuildPPMShipment(suite.DB(), []factory.Customization{ + { + Model: models.PPMShipment{ + EstimatedWeight: models.PoundPointer(4000), + SITExpected: models.BoolPointer(true), + SITLocation: &originLocation, + SITEstimatedWeight: models.PoundPointer(unit.Pound(2000)), + SITEstimatedEntryDate: &entryDate, + SITEstimatedDepartureDate: models.TimePointer(entryDate.Add(time.Hour * 24 * 30)), + }, + }, + { + Model: models.MTOShipment{ + MarketCode: models.MarketCodeInternational, + }, + }, + { + Model: models.Address{ + StreetAddress1: "Tester Address", + City: "Tulsa", + State: "OK", + PostalCode: "74133", + }, + Type: &factory.Addresses.PickupAddress, + }, + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "JBER", + State: "AK", + PostalCode: "99505", + IsOconus: models.BoolPointer(true), + }, + Type: &factory.Addresses.DeliveryAddress, + }, + }, nil) + + newPPM := ppm + newEstimatedWeight := models.PoundPointer(5500) + newPPM.SITEstimatedWeight = newEstimatedWeight + setupPricerData() + + estimatedSITCost, err := ppmEstimator.CalculatePPMSITEstimatedCost(suite.AppContextForTest(), &ppm) + suite.NilOrNoVerrs(err) + suite.NotNil(estimatedSITCost) + suite.Equal(unit.Cents(20540), *estimatedSITCost) + }) + + suite.Run("CalculatePPMSITEstimatedCostBreakdown - Success for OCONUS PPM", func() { + originLocation := models.SITLocationTypeDestination + entryDate := time.Date(2020, time.March, 15, 0, 0, 0, 0, time.UTC) + ppm := factory.BuildPPMShipment(suite.DB(), []factory.Customization{ + { + Model: models.PPMShipment{ + EstimatedWeight: models.PoundPointer(4000), + SITExpected: models.BoolPointer(true), + SITLocation: &originLocation, + SITEstimatedWeight: models.PoundPointer(unit.Pound(2000)), + SITEstimatedEntryDate: &entryDate, + SITEstimatedDepartureDate: models.TimePointer(entryDate.Add(time.Hour * 24 * 30)), + }, + }, + { + Model: models.MTOShipment{ + MarketCode: models.MarketCodeInternational, + }, + }, + { + Model: models.Address{ + StreetAddress1: "Tester Address", + City: "Tulsa", + State: "OK", + PostalCode: "74133", + }, + Type: &factory.Addresses.PickupAddress, + }, + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "JBER", + State: "AK", + PostalCode: "99505", + IsOconus: models.BoolPointer(true), + }, + Type: &factory.Addresses.DeliveryAddress, + }, + }, nil) + + newPPM := ppm + newEstimatedWeight := models.PoundPointer(5500) + newPPM.SITEstimatedWeight = newEstimatedWeight + setupPricerData() + + sitCosts, err := ppmEstimator.CalculatePPMSITEstimatedCostBreakdown(suite.AppContextForTest(), &ppm) + suite.NilOrNoVerrs(err) + suite.NotNil(sitCosts) + suite.Equal(unit.Cents(20540), *sitCosts.EstimatedSITCost) + suite.Equal(unit.Cents(12140), *sitCosts.PriceFirstDaySIT) + suite.Equal(unit.Cents(8400), *sitCosts.PriceAdditionalDaySIT) + }) }) } From 4a84e7eb9920827c1387f0bc889c57c503915761 Mon Sep 17 00:00:00 2001 From: Daniel Jordan Date: Thu, 23 Jan 2025 18:15:20 +0000 Subject: [PATCH 16/18] added PPM port to migration, updated tests --- ...52_add_ppm_estimated_incentive_proc.up.sql | 9 +++++++ .../distance_zip_lookup.go | 4 ++-- .../distance_zip_lookup_test.go | 4 ++-- .../port_zip_lookup.go | 4 ++-- .../port_zip_lookup_test.go | 4 ++-- pkg/services/ppmshipment/ppm_estimator.go | 2 +- .../ppmshipment/ppm_estimator_test.go | 24 +++++++++---------- 7 files changed, 30 insertions(+), 21 deletions(-) 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 896af50b01e..c0d7b3c9407 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 @@ -33,6 +33,15 @@ WHERE service_item_param_key_id = '4736f489-dfda-4df1-a303-8c434a120d5d'; DELETE FROM service_item_param_keys WHERE key = 'PriceAreaIntlDest'; +-- adding port info that PPMs will consume +INSERT INTO public.ports +(id, port_code, port_type, port_name, created_at, updated_at) +VALUES('d8776c6b-bc5e-45d8-ac50-ab60c34c022d'::uuid, '4E1', 'S','TACOMA, PUGET SOUND', now(), now()); + +INSERT INTO public.port_locations +(id, port_id, cities_id, us_post_region_cities_id, country_id, is_active, created_at, updated_at) +VALUES('ee3a97dc-112e-4805-8518-f56f2d9c6cc6'::uuid, 'd8776c6b-bc5e-45d8-ac50-ab60c34c022d'::uuid, 'baaf6ab1-6142-4fb7-b753-d0a142c75baf'::uuid, '86fef297-d61f-44ea-afec-4f679ce686b7'::uuid, '791899e6-cd77-46f2-981b-176ecb8d7098'::uuid, true, now(), now()); + -- 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 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 cd1f99edda3..968cfe718a9 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 @@ -79,9 +79,9 @@ func (r DistanceZipLookup) lookup(appCtx appcontext.AppContext, keyData *Service } } else { // 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 + portLocation, err := models.FetchPortLocationByCode(appCtx.DB(), "4E1") // Tacoma port code if err != nil { - return "", fmt.Errorf("unable to find port zip with code %s", "3002") + return "", fmt.Errorf("unable to find port zip with code %s", "4E1") } 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 diff --git a/pkg/payment_request/service_param_value_lookups/distance_zip_lookup_test.go b/pkg/payment_request/service_param_value_lookups/distance_zip_lookup_test.go index 918442f2ca5..0ac37eb213b 100644 --- a/pkg/payment_request/service_param_value_lookups/distance_zip_lookup_test.go +++ b/pkg/payment_request/service_param_value_lookups/distance_zip_lookup_test.go @@ -171,8 +171,8 @@ func (suite *ServiceParamValueLookupsSuite) TestDistanceLookup() { suite.NotNil(distance) planner := suite.planner.(*mocks.Planner) - // should be called with the 98424 ZIP of the Tacoma port and NOT 99505 - planner.AssertCalled(suite.T(), "ZipTransitDistance", appContext, ppmShipment.PickupAddress.PostalCode, "98424", false, true) + // should be called with the 98421 ZIP of the Tacoma port and NOT 99505 + planner.AssertCalled(suite.T(), "ZipTransitDistance", appContext, ppmShipment.PickupAddress.PostalCode, "98421", false, true) }) suite.Run("Calculate transit zip distance with an approved Destination SIT service item", func() { 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 bade82c04b9..9621c56cd8a 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 @@ -28,9 +28,9 @@ func (p PortZipLookup) lookup(appCtx appcontext.AppContext, keyData *ServiceItem 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") + portLocation, err := models.FetchPortLocationByCode(appCtx.DB(), "4E1") if err != nil { - return "", fmt.Errorf("unable to find port zip with code %s", "3002") + return "", fmt.Errorf("unable to find port zip with code %s", "4E1") } return portLocation.UsPostRegionCity.UsprZipID, nil } else { diff --git a/pkg/payment_request/service_param_value_lookups/port_zip_lookup_test.go b/pkg/payment_request/service_param_value_lookups/port_zip_lookup_test.go index 3f8776e0ecf..b06533a4e1f 100644 --- a/pkg/payment_request/service_param_value_lookups/port_zip_lookup_test.go +++ b/pkg/payment_request/service_param_value_lookups/port_zip_lookup_test.go @@ -85,11 +85,11 @@ func (suite *ServiceParamValueLookupsSuite) TestPortZipLookup() { suite.Equal(portZip, port.UsPostRegionCity.UsprZipID) }) - suite.Run("success - returns PortZip value for Port Code 3002 for PPMs", func() { + suite.Run("success - returns PortZip value for Port Code 4E1 for PPMs", func() { port := factory.FetchPortLocation(suite.DB(), []factory.Customization{ { Model: models.Port{ - PortCode: "3002", + PortCode: "4E1", }, }, }, nil) diff --git a/pkg/services/ppmshipment/ppm_estimator.go b/pkg/services/ppmshipment/ppm_estimator.go index b925cf6bfcf..d54f2f516f0 100644 --- a/pkg/services/ppmshipment/ppm_estimator.go +++ b/pkg/services/ppmshipment/ppm_estimator.go @@ -769,7 +769,7 @@ func (f estimatePPM) priceBreakdown(appCtx appcontext.AppContext, ppmShipment *m // this simulates the reimbursement for an iHHG move with ISLH, IHPK, IHUPK, and CONUS portion of FSC func (f *estimatePPM) CalculateOCONUSIncentive(appCtx appcontext.AppContext, ppmShipmentID uuid.UUID, pickupAddress models.Address, destinationAddress models.Address, moveDate time.Time, weight int, isEstimated bool, isActual bool, isMax bool) (*unit.Cents, error) { var mileage int - ppmPort, err := models.FetchPortLocationByCode(appCtx.DB(), "3002") // Tacoma, WA port + ppmPort, err := models.FetchPortLocationByCode(appCtx.DB(), "4E1") // Tacoma, WA port if err != nil { return nil, fmt.Errorf("failed to fetch port location: %w", err) } diff --git a/pkg/services/ppmshipment/ppm_estimator_test.go b/pkg/services/ppmshipment/ppm_estimator_test.go index 1e0cf6cf6c2..6f647387bcc 100644 --- a/pkg/services/ppmshipment/ppm_estimator_test.go +++ b/pkg/services/ppmshipment/ppm_estimator_test.go @@ -2086,7 +2086,7 @@ func (suite *PPMShipmentSuite) TestInternationalPPMEstimator() { newPPM.EstimatedWeight = &estimatedWeight planner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "74133", "98424", true, true).Return(3000, nil) + "74133", "98421", true, true).Return(3000, nil) ppmEstimate, _, err := ppmEstimator.EstimateIncentiveWithDefaultChecks(suite.AppContextForTest(), ppm, &newPPM) suite.NilOrNoVerrs(err) @@ -2094,7 +2094,7 @@ func (suite *PPMShipmentSuite) TestInternationalPPMEstimator() { // it should've called from the pickup -> port and NOT pickup -> dest planner.AssertCalled(suite.T(), "ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "74133", "98424", true, true) + "74133", "98421", true, true) suite.Equal(unit.Cents(459178), *ppmEstimate) }) @@ -2133,7 +2133,7 @@ func (suite *PPMShipmentSuite) TestInternationalPPMEstimator() { newPPM.EstimatedWeight = &estimatedWeight planner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "98424", "74133", true, true).Return(3000, nil) + "98421", "74133", true, true).Return(3000, nil) ppmEstimate, _, err := ppmEstimator.EstimateIncentiveWithDefaultChecks(suite.AppContextForTest(), ppm, &newPPM) suite.NilOrNoVerrs(err) @@ -2141,7 +2141,7 @@ func (suite *PPMShipmentSuite) TestInternationalPPMEstimator() { // it should've called from the pickup -> port and NOT pickup -> dest planner.AssertCalled(suite.T(), "ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "98424", "74133", true, true) + "98421", "74133", true, true) suite.Equal(unit.Cents(423178), *ppmEstimate) }) }) @@ -2215,7 +2215,7 @@ func (suite *PPMShipmentSuite) TestInternationalPPMEstimator() { // DTOD will be called to get the distance between the origin duty location & the Tacoma Port ZIP planner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "50309", "98424", true, true).Return(3000, nil) + "50309", "98421", true, true).Return(3000, nil) ppmMaxIncentive, err := ppmEstimator.MaxIncentive(suite.AppContextForTest(), ppm, &newPPM) suite.NilOrNoVerrs(err) @@ -2223,7 +2223,7 @@ func (suite *PPMShipmentSuite) TestInternationalPPMEstimator() { // it should've called from the pickup -> port and NOT pickup -> dest planner.AssertCalled(suite.T(), "ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "50309", "98424", true, true) + "50309", "98421", true, true) suite.Equal(unit.Cents(656532), *ppmMaxIncentive) }) @@ -2295,7 +2295,7 @@ func (suite *PPMShipmentSuite) TestInternationalPPMEstimator() { // DTOD will be called to get the distance between the origin duty location & the Tacoma Port ZIP planner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "98424", "30813", true, true).Return(3000, nil) + "98421", "30813", true, true).Return(3000, nil) ppmMaxIncentive, err := ppmEstimator.MaxIncentive(suite.AppContextForTest(), ppm, &newPPM) suite.NilOrNoVerrs(err) @@ -2303,7 +2303,7 @@ func (suite *PPMShipmentSuite) TestInternationalPPMEstimator() { // it should've called from the pickup -> port and NOT pickup -> dest planner.AssertCalled(suite.T(), "ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "98424", "30813", true, true) + "98421", "30813", true, true) suite.Equal(unit.Cents(676692), *ppmMaxIncentive) }) }) @@ -2362,7 +2362,7 @@ func (suite *PPMShipmentSuite) TestInternationalPPMEstimator() { setupPricerData() planner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "74133", "98424", true, true).Return(3000, nil) + "74133", "98421", true, true).Return(3000, nil) ppmFinalIncentive, err := ppmEstimator.FinalIncentiveWithDefaultChecks(suite.AppContextForTest(), ppm, &newPPM) suite.NilOrNoVerrs(err) @@ -2370,7 +2370,7 @@ func (suite *PPMShipmentSuite) TestInternationalPPMEstimator() { // it should've called from the pickup -> port and NOT pickup -> dest planner.AssertCalled(suite.T(), "ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "74133", "98424", true, true) + "74133", "98421", true, true) suite.Equal(unit.Cents(459178), *ppmFinalIncentive) }) @@ -2427,7 +2427,7 @@ func (suite *PPMShipmentSuite) TestInternationalPPMEstimator() { setupPricerData() planner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "98424", "74133", true, true).Return(3000, nil) + "98421", "74133", true, true).Return(3000, nil) ppmFinalIncentive, err := ppmEstimator.FinalIncentiveWithDefaultChecks(suite.AppContextForTest(), ppm, &newPPM) suite.NilOrNoVerrs(err) @@ -2435,7 +2435,7 @@ func (suite *PPMShipmentSuite) TestInternationalPPMEstimator() { // it should've called from the pickup -> port and NOT pickup -> dest planner.AssertCalled(suite.T(), "ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "98424", "74133", true, true) + "98421", "74133", true, true) suite.Equal(unit.Cents(423178), *ppmFinalIncentive) }) }) From f5f4b7fe73e7d93865213de25e3e5673b7821992 Mon Sep 17 00:00:00 2001 From: Daniel Jordan Date: Tue, 28 Jan 2025 13:53:08 +0000 Subject: [PATCH 17/18] updating mock test params --- .../distance_zip_lookup_test.go | 2 +- .../ppmshipment/ppm_estimator_test.go | 24 +++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/pkg/payment_request/service_param_value_lookups/distance_zip_lookup_test.go b/pkg/payment_request/service_param_value_lookups/distance_zip_lookup_test.go index a9785fe0b9f..8b8d866b313 100644 --- a/pkg/payment_request/service_param_value_lookups/distance_zip_lookup_test.go +++ b/pkg/payment_request/service_param_value_lookups/distance_zip_lookup_test.go @@ -172,7 +172,7 @@ func (suite *ServiceParamValueLookupsSuite) TestDistanceLookup() { planner := suite.planner.(*mocks.Planner) // should be called with the 98421 ZIP of the Tacoma port and NOT 99505 - planner.AssertCalled(suite.T(), "ZipTransitDistance", appContext, ppmShipment.PickupAddress.PostalCode, "98421", false, true) + planner.AssertCalled(suite.T(), "ZipTransitDistance", appContext, ppmShipment.PickupAddress.PostalCode, "98421", true) }) suite.Run("Calculate transit zip distance with an approved Destination SIT service item", func() { diff --git a/pkg/services/ppmshipment/ppm_estimator_test.go b/pkg/services/ppmshipment/ppm_estimator_test.go index eb3de6845dd..f384223e3ba 100644 --- a/pkg/services/ppmshipment/ppm_estimator_test.go +++ b/pkg/services/ppmshipment/ppm_estimator_test.go @@ -2086,7 +2086,7 @@ func (suite *PPMShipmentSuite) TestInternationalPPMEstimator() { newPPM.EstimatedWeight = &estimatedWeight planner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "74133", "98421", true, true).Return(3000, nil) + "74133", "98421", true).Return(3000, nil) ppmEstimate, _, err := ppmEstimator.EstimateIncentiveWithDefaultChecks(suite.AppContextForTest(), ppm, &newPPM) suite.NilOrNoVerrs(err) @@ -2094,7 +2094,7 @@ func (suite *PPMShipmentSuite) TestInternationalPPMEstimator() { // it should've called from the pickup -> port and NOT pickup -> dest planner.AssertCalled(suite.T(), "ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "74133", "98421", true, true) + "74133", "98421", true) suite.Equal(unit.Cents(459178), *ppmEstimate) }) @@ -2133,7 +2133,7 @@ func (suite *PPMShipmentSuite) TestInternationalPPMEstimator() { newPPM.EstimatedWeight = &estimatedWeight planner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "98421", "74133", true, true).Return(3000, nil) + "98421", "74133", true).Return(3000, nil) ppmEstimate, _, err := ppmEstimator.EstimateIncentiveWithDefaultChecks(suite.AppContextForTest(), ppm, &newPPM) suite.NilOrNoVerrs(err) @@ -2141,7 +2141,7 @@ func (suite *PPMShipmentSuite) TestInternationalPPMEstimator() { // it should've called from the pickup -> port and NOT pickup -> dest planner.AssertCalled(suite.T(), "ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "98421", "74133", true, true) + "98421", "74133", true) suite.Equal(unit.Cents(423178), *ppmEstimate) }) }) @@ -2215,7 +2215,7 @@ func (suite *PPMShipmentSuite) TestInternationalPPMEstimator() { // DTOD will be called to get the distance between the origin duty location & the Tacoma Port ZIP planner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "50309", "98421", true, true).Return(3000, nil) + "50309", "98421", true).Return(3000, nil) ppmMaxIncentive, err := ppmEstimator.MaxIncentive(suite.AppContextForTest(), ppm, &newPPM) suite.NilOrNoVerrs(err) @@ -2223,7 +2223,7 @@ func (suite *PPMShipmentSuite) TestInternationalPPMEstimator() { // it should've called from the pickup -> port and NOT pickup -> dest planner.AssertCalled(suite.T(), "ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "50309", "98421", true, true) + "50309", "98421", true) suite.Equal(unit.Cents(656532), *ppmMaxIncentive) }) @@ -2295,7 +2295,7 @@ func (suite *PPMShipmentSuite) TestInternationalPPMEstimator() { // DTOD will be called to get the distance between the origin duty location & the Tacoma Port ZIP planner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "98421", "30813", true, true).Return(3000, nil) + "98421", "30813", true).Return(3000, nil) ppmMaxIncentive, err := ppmEstimator.MaxIncentive(suite.AppContextForTest(), ppm, &newPPM) suite.NilOrNoVerrs(err) @@ -2303,7 +2303,7 @@ func (suite *PPMShipmentSuite) TestInternationalPPMEstimator() { // it should've called from the pickup -> port and NOT pickup -> dest planner.AssertCalled(suite.T(), "ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "98421", "30813", true, true) + "98421", "30813", true) suite.Equal(unit.Cents(676692), *ppmMaxIncentive) }) }) @@ -2362,7 +2362,7 @@ func (suite *PPMShipmentSuite) TestInternationalPPMEstimator() { setupPricerData() planner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "74133", "98421", true, true).Return(3000, nil) + "74133", "98421", true).Return(3000, nil) ppmFinalIncentive, err := ppmEstimator.FinalIncentiveWithDefaultChecks(suite.AppContextForTest(), ppm, &newPPM) suite.NilOrNoVerrs(err) @@ -2370,7 +2370,7 @@ func (suite *PPMShipmentSuite) TestInternationalPPMEstimator() { // it should've called from the pickup -> port and NOT pickup -> dest planner.AssertCalled(suite.T(), "ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "74133", "98421", true, true) + "74133", "98421", true) suite.Equal(unit.Cents(459178), *ppmFinalIncentive) }) @@ -2427,7 +2427,7 @@ func (suite *PPMShipmentSuite) TestInternationalPPMEstimator() { setupPricerData() planner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "98421", "74133", true, true).Return(3000, nil) + "98421", "74133", true).Return(3000, nil) ppmFinalIncentive, err := ppmEstimator.FinalIncentiveWithDefaultChecks(suite.AppContextForTest(), ppm, &newPPM) suite.NilOrNoVerrs(err) @@ -2435,7 +2435,7 @@ func (suite *PPMShipmentSuite) TestInternationalPPMEstimator() { // it should've called from the pickup -> port and NOT pickup -> dest planner.AssertCalled(suite.T(), "ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), - "98421", "74133", true, true) + "98421", "74133", true) suite.Equal(unit.Cents(423178), *ppmFinalIncentive) }) }) From 95292a76789a8380016994139dc6f1f21a4080bd Mon Sep 17 00:00:00 2001 From: Daniel Jordan Date: Tue, 4 Feb 2025 19:36:35 +0000 Subject: [PATCH 18/18] if isInternationalShipment to copy from INT --- .../service_param_value_lookups/distance_zip_lookup.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 f7c2e531e5b..acc87eb2715 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 @@ -62,7 +62,7 @@ func (r DistanceZipLookup) lookup(appCtx appcontext.AppContext, keyData *Service isInternationalShipment := mtoShipment.MarketCode == models.MarketCodeInternational // 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 { + if isInternationalShipment { if mtoShipment.ShipmentType != models.MTOShipmentTypePPM { portZip, portType, err := models.GetPortLocationInfoForShipment(appCtx.DB(), *mtoShipmentID) if err != nil {