From cfe9b4f426ee9e6131d18049a1f0bf41a188aee1 Mon Sep 17 00:00:00 2001 From: Daniel Jordan Date: Sat, 22 Feb 2025 19:03:06 +0000 Subject: [PATCH] initial commit, updated shorthaul pricer to consider PPMs and improved same ZIP logic --- pkg/services/ghc_rate_engine.go | 2 +- .../domestic_shorthaul_pricer.go | 15 ++++++-- .../domestic_shorthaul_pricer_test.go | 36 ++++++++++++++++--- pkg/services/ghcrateengine/pricer_helpers.go | 3 +- pkg/services/mocks/DomesticShorthaulPricer.go | 22 ++++++------ .../mto_service_item_creator.go | 2 +- pkg/services/ppmshipment/ppm_estimator.go | 20 +++++++++-- 7 files changed, 75 insertions(+), 25 deletions(-) diff --git a/pkg/services/ghc_rate_engine.go b/pkg/services/ghc_rate_engine.go index 8944ad1b1bc..f10ccdd8bbc 100644 --- a/pkg/services/ghc_rate_engine.go +++ b/pkg/services/ghc_rate_engine.go @@ -57,7 +57,7 @@ type DomesticLinehaulPricer interface { // //go:generate mockery --name DomesticShorthaulPricer type DomesticShorthaulPricer interface { - Price(appCtx appcontext.AppContext, contractCode string, requestedPickupDate time.Time, distance unit.Miles, weight unit.Pound, serviceArea string) (unit.Cents, PricingDisplayParams, error) + Price(appCtx appcontext.AppContext, contractCode string, requestedPickupDate time.Time, distance unit.Miles, weight unit.Pound, serviceArea string, isPPM bool) (unit.Cents, PricingDisplayParams, error) ParamsPricer } diff --git a/pkg/services/ghcrateengine/domestic_shorthaul_pricer.go b/pkg/services/ghcrateengine/domestic_shorthaul_pricer.go index 44aa7d161b6..08600956811 100644 --- a/pkg/services/ghcrateengine/domestic_shorthaul_pricer.go +++ b/pkg/services/ghcrateengine/domestic_shorthaul_pricer.go @@ -28,7 +28,8 @@ func (p domesticShorthaulPricer) Price(appCtx appcontext.AppContext, contractCod referenceDate time.Time, distance unit.Miles, weight unit.Pound, - serviceArea string) (totalCost unit.Cents, params services.PricingDisplayParams, err error) { + serviceArea string, + isPPM bool) (totalCost unit.Cents, params services.PricingDisplayParams, err error) { // Validate parameters if len(contractCode) == 0 { return 0, nil, errors.New("ContractCode is required") @@ -36,7 +37,7 @@ func (p domesticShorthaulPricer) Price(appCtx appcontext.AppContext, contractCod if referenceDate.IsZero() { return 0, nil, errors.New("ReferenceDate is required") } - if weight < minDomesticWeight { + if !isPPM && weight < minDomesticWeight { return 0, nil, fmt.Errorf("Weight must be a minimum of %d", minDomesticWeight) } if distance <= 0 { @@ -110,5 +111,13 @@ func (p domesticShorthaulPricer) PriceUsingParams(appCtx appcontext.AppContext, return unit.Cents(0), nil, err } - return p.Price(appCtx, contractCode, referenceDate, unit.Miles(distanceZip), unit.Pound(weightBilled), serviceAreaOrigin) + var isPPM = false + if params[0].PaymentServiceItem.MTOServiceItem.MTOShipment.ShipmentType == models.MTOShipmentTypePPM { + // PPMs do not require minimums for a shipment's weight or distance + // this flag is passed into the Price function to ensure the weight and distance mins + // are not enforced for PPMs + isPPM = true + } + + return p.Price(appCtx, contractCode, referenceDate, unit.Miles(distanceZip), unit.Pound(weightBilled), serviceAreaOrigin, isPPM) } diff --git a/pkg/services/ghcrateengine/domestic_shorthaul_pricer_test.go b/pkg/services/ghcrateengine/domestic_shorthaul_pricer_test.go index 7f43328515a..d8f157f2682 100644 --- a/pkg/services/ghcrateengine/domestic_shorthaul_pricer_test.go +++ b/pkg/services/ghcrateengine/domestic_shorthaul_pricer_test.go @@ -17,6 +17,8 @@ const ( dshTestMileage = 1200 ) +var dshRequestedPickupDate = time.Date(testdatagen.TestYear, time.June, 5, 7, 33, 11, 456, time.UTC) + func (suite *GHCRateEngineServiceSuite) TestPriceDomesticShorthaulWithServiceItemParamsBadData() { requestedPickup := time.Date(testdatagen.TestYear, peakStart.month, peakStart.day, 0, 0, 0, 0, time.UTC).Format(DateParamFormat) pricer := NewDomesticShorthaulPricer() @@ -120,6 +122,7 @@ func (suite *GHCRateEngineServiceSuite) TestPriceDomesticShorthaulWithServiceIte } func (suite *GHCRateEngineServiceSuite) TestPriceDomesticShorthaul() { + isPPM := false suite.Run("success shorthaul cost within peak period", func() { requestedPickup := time.Date(testdatagen.TestYear, peakStart.month, peakStart.day, 0, 0, 0, 0, time.UTC).Format(DateParamFormat) suite.setUpDomesticShorthaulData() @@ -135,6 +138,7 @@ func (suite *GHCRateEngineServiceSuite) TestPriceDomesticShorthaul() { dshTestMileage, dshTestWeight, dshTestServiceArea, + isPPM, ) expectedCost := unit.Cents(6566400) suite.NoError(err) @@ -158,6 +162,7 @@ func (suite *GHCRateEngineServiceSuite) TestPriceDomesticShorthaul() { dshTestMileage, dshTestWeight, dshTestServiceArea, + isPPM, ) expectedCost := unit.Cents(5702400) suite.NoError(err) @@ -176,6 +181,7 @@ func (suite *GHCRateEngineServiceSuite) TestPriceDomesticShorthaul() { dshTestMileage, dshTestWeight, dshTestServiceArea, + isPPM, ) suite.Error(err) @@ -194,6 +200,7 @@ func (suite *GHCRateEngineServiceSuite) TestPriceDomesticShorthaul() { dshTestMileage, dshTestWeight, dshTestServiceArea, + isPPM, ) suite.Error(err) @@ -212,6 +219,7 @@ func (suite *GHCRateEngineServiceSuite) TestPriceDomesticShorthaul() { dshTestMileage, unit.Pound(499), dshTestServiceArea, + isPPM, ) suite.Equal(unit.Cents(0), cost) suite.Error(err) @@ -219,6 +227,24 @@ func (suite *GHCRateEngineServiceSuite) TestPriceDomesticShorthaul() { suite.Nil(rateEngineParams) }) + suite.Run("successfully finds shorthaul price for ppm with weight < 500 lbs with Price method", func() { + suite.setUpDomesticShorthaulData() + pricer := NewDomesticShorthaulPricer() + isPPM = true + // the PPM price for weights < 500 should be prorated from a base of 500 + basePriceCents, _, err := pricer.Price(suite.AppContextForTest(), testdatagen.DefaultContractCode, dshRequestedPickupDate, dshTestMileage, unit.Pound(500), dshTestServiceArea, isPPM) + suite.NoError(err) + + halfPriceCents, _, err := pricer.Price(suite.AppContextForTest(), testdatagen.DefaultContractCode, dshRequestedPickupDate, dshTestMileage, unit.Pound(250), dshTestServiceArea, isPPM) + suite.NoError(err) + suite.Equal(basePriceCents/2, halfPriceCents) + + fifthPriceCents, _, err := pricer.Price(suite.AppContextForTest(), testdatagen.DefaultContractCode, dshRequestedPickupDate, dshTestMileage, unit.Pound(100), dshTestServiceArea, isPPM) + suite.NoError(err) + suite.Equal(basePriceCents/5, fifthPriceCents) + isPPM = false + }) + suite.Run("validation errors", func() { suite.setUpDomesticShorthaulData() pricer := NewDomesticShorthaulPricer() @@ -226,31 +252,31 @@ func (suite *GHCRateEngineServiceSuite) TestPriceDomesticShorthaul() { requestedPickupDate := time.Date(testdatagen.TestYear, time.July, 4, 0, 0, 0, 0, time.UTC) // No contract code - _, rateEngineParams, err := pricer.Price(suite.AppContextForTest(), "", requestedPickupDate, dshTestMileage, dshTestWeight, dshTestServiceArea) + _, rateEngineParams, err := pricer.Price(suite.AppContextForTest(), "", requestedPickupDate, dshTestMileage, dshTestWeight, dshTestServiceArea, isPPM) suite.Error(err) suite.Equal("ContractCode is required", err.Error()) suite.Nil(rateEngineParams) // No reference date - _, rateEngineParams, err = pricer.Price(suite.AppContextForTest(), testdatagen.DefaultContractCode, time.Time{}, dshTestMileage, dshTestWeight, dshTestServiceArea) + _, rateEngineParams, err = pricer.Price(suite.AppContextForTest(), testdatagen.DefaultContractCode, time.Time{}, dshTestMileage, dshTestWeight, dshTestServiceArea, isPPM) suite.Error(err) suite.Equal("ReferenceDate is required", err.Error()) suite.Nil(rateEngineParams) // No distance - _, rateEngineParams, err = pricer.Price(suite.AppContextForTest(), testdatagen.DefaultContractCode, requestedPickupDate, 0, dshTestWeight, dshTestServiceArea) + _, rateEngineParams, err = pricer.Price(suite.AppContextForTest(), testdatagen.DefaultContractCode, requestedPickupDate, 0, dshTestWeight, dshTestServiceArea, isPPM) suite.Error(err) suite.Equal("Distance must be greater than 0", err.Error()) suite.Nil(rateEngineParams) // No weight - _, rateEngineParams, err = pricer.Price(suite.AppContextForTest(), testdatagen.DefaultContractCode, requestedPickupDate, dshTestMileage, 0, dshTestServiceArea) + _, rateEngineParams, err = pricer.Price(suite.AppContextForTest(), testdatagen.DefaultContractCode, requestedPickupDate, dshTestMileage, 0, dshTestServiceArea, isPPM) suite.Error(err) suite.Equal("Weight must be a minimum of 500", err.Error()) suite.Nil(rateEngineParams) // No service area - _, rateEngineParams, err = pricer.Price(suite.AppContextForTest(), testdatagen.DefaultContractCode, requestedPickupDate, dshTestMileage, dshTestWeight, "") + _, rateEngineParams, err = pricer.Price(suite.AppContextForTest(), testdatagen.DefaultContractCode, requestedPickupDate, dshTestMileage, dshTestWeight, "", isPPM) suite.Error(err) suite.Equal("ServiceArea is required", err.Error()) suite.Nil(rateEngineParams) diff --git a/pkg/services/ghcrateengine/pricer_helpers.go b/pkg/services/ghcrateengine/pricer_helpers.go index cb804b0da49..9f88767d652 100644 --- a/pkg/services/ghcrateengine/pricer_helpers.go +++ b/pkg/services/ghcrateengine/pricer_helpers.go @@ -267,7 +267,8 @@ func priceDomesticPickupDeliverySIT(appCtx appcontext.AppContext, pickupDelivery if zip3Original == zip3Actual { // Do a normal shorthaul calculation shorthaulPricer := NewDomesticShorthaulPricer() - totalPriceCents, displayParams, err := shorthaulPricer.Price(appCtx, contractCode, referenceDate, distance, weight, serviceArea) + isPPM := false + totalPriceCents, displayParams, err := shorthaulPricer.Price(appCtx, contractCode, referenceDate, distance, weight, serviceArea, isPPM) if err != nil { return unit.Cents(0), nil, fmt.Errorf("could not price shorthaul: %w", err) } diff --git a/pkg/services/mocks/DomesticShorthaulPricer.go b/pkg/services/mocks/DomesticShorthaulPricer.go index 029872ef7e4..5ffc3ad9536 100644 --- a/pkg/services/mocks/DomesticShorthaulPricer.go +++ b/pkg/services/mocks/DomesticShorthaulPricer.go @@ -20,9 +20,9 @@ type DomesticShorthaulPricer struct { mock.Mock } -// Price provides a mock function with given fields: appCtx, contractCode, requestedPickupDate, distance, weight, serviceArea -func (_m *DomesticShorthaulPricer) Price(appCtx appcontext.AppContext, contractCode string, requestedPickupDate time.Time, distance unit.Miles, weight unit.Pound, serviceArea string) (unit.Cents, services.PricingDisplayParams, error) { - ret := _m.Called(appCtx, contractCode, requestedPickupDate, distance, weight, serviceArea) +// Price provides a mock function with given fields: appCtx, contractCode, requestedPickupDate, distance, weight, serviceArea, isPPM +func (_m *DomesticShorthaulPricer) Price(appCtx appcontext.AppContext, contractCode string, requestedPickupDate time.Time, distance unit.Miles, weight unit.Pound, serviceArea string, isPPM bool) (unit.Cents, services.PricingDisplayParams, error) { + ret := _m.Called(appCtx, contractCode, requestedPickupDate, distance, weight, serviceArea, isPPM) if len(ret) == 0 { panic("no return value specified for Price") @@ -31,25 +31,25 @@ func (_m *DomesticShorthaulPricer) Price(appCtx appcontext.AppContext, contractC var r0 unit.Cents var r1 services.PricingDisplayParams var r2 error - if rf, ok := ret.Get(0).(func(appcontext.AppContext, string, time.Time, unit.Miles, unit.Pound, string) (unit.Cents, services.PricingDisplayParams, error)); ok { - return rf(appCtx, contractCode, requestedPickupDate, distance, weight, serviceArea) + if rf, ok := ret.Get(0).(func(appcontext.AppContext, string, time.Time, unit.Miles, unit.Pound, string, bool) (unit.Cents, services.PricingDisplayParams, error)); ok { + return rf(appCtx, contractCode, requestedPickupDate, distance, weight, serviceArea, isPPM) } - if rf, ok := ret.Get(0).(func(appcontext.AppContext, string, time.Time, unit.Miles, unit.Pound, string) unit.Cents); ok { - r0 = rf(appCtx, contractCode, requestedPickupDate, distance, weight, serviceArea) + if rf, ok := ret.Get(0).(func(appcontext.AppContext, string, time.Time, unit.Miles, unit.Pound, string, bool) unit.Cents); ok { + r0 = rf(appCtx, contractCode, requestedPickupDate, distance, weight, serviceArea, isPPM) } else { r0 = ret.Get(0).(unit.Cents) } - if rf, ok := ret.Get(1).(func(appcontext.AppContext, string, time.Time, unit.Miles, unit.Pound, string) services.PricingDisplayParams); ok { - r1 = rf(appCtx, contractCode, requestedPickupDate, distance, weight, serviceArea) + if rf, ok := ret.Get(1).(func(appcontext.AppContext, string, time.Time, unit.Miles, unit.Pound, string, bool) services.PricingDisplayParams); ok { + r1 = rf(appCtx, contractCode, requestedPickupDate, distance, weight, serviceArea, isPPM) } 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.Miles, unit.Pound, string) error); ok { - r2 = rf(appCtx, contractCode, requestedPickupDate, distance, weight, serviceArea) + if rf, ok := ret.Get(2).(func(appcontext.AppContext, string, time.Time, unit.Miles, unit.Pound, string, bool) error); ok { + r2 = rf(appCtx, contractCode, requestedPickupDate, distance, weight, serviceArea, isPPM) } else { r2 = ret.Error(2) } diff --git a/pkg/services/mto_service_item/mto_service_item_creator.go b/pkg/services/mto_service_item/mto_service_item_creator.go index 3b46e33ea3a..aaa7b4c3a36 100644 --- a/pkg/services/mto_service_item/mto_service_item_creator.go +++ b/pkg/services/mto_service_item/mto_service_item_creator.go @@ -151,7 +151,7 @@ func (o *mtoServiceItemCreator) FindEstimatedPrice(appCtx appcontext.AppContext, return 0, err } } - price, _, err = o.shorthaulPricer.Price(appCtx, contractCode, requestedPickupDate, unit.Miles(distance), *adjustedWeight, domesticServiceArea.ServiceArea) + price, _, err = o.shorthaulPricer.Price(appCtx, contractCode, requestedPickupDate, unit.Miles(distance), *adjustedWeight, domesticServiceArea.ServiceArea, isPPM) if err != nil { return 0, err } diff --git a/pkg/services/ppmshipment/ppm_estimator.go b/pkg/services/ppmshipment/ppm_estimator.go index cf77b16fb04..d162aa3a722 100644 --- a/pkg/services/ppmshipment/ppm_estimator.go +++ b/pkg/services/ppmshipment/ppm_estimator.go @@ -489,8 +489,16 @@ func (f estimatePPM) calculatePrice(appCtx appcontext.AppContext, ppmShipment *m } } - if pickupPostal[0:3] == destPostal[0:3] { - serviceItemsToPrice[0] = models.MTOServiceItem{ReService: models.ReService{Code: models.ReServiceCodeDSH}, MTOShipmentID: &ppmShipment.ShipmentID} + // if the ZIPs are the same, we need to replace the DLH service item with DSH + if len(pickupPostal) >= 3 && len(destPostal) >= 3 && pickupPostal[:3] == destPostal[:3] { + if pickupPostal[0:3] == destPostal[0:3] { + for i, serviceItem := range serviceItemsToPrice { + if serviceItem.ReService.Code == models.ReServiceCodeDLH { + serviceItemsToPrice[i] = models.MTOServiceItem{ReService: models.ReService{Code: models.ReServiceCodeDSH}, MTOShipmentID: &ppmShipment.ShipmentID} + break + } + } + } } // Get a list of all the pricing params needed to calculate the price for each service item @@ -630,9 +638,15 @@ func (f estimatePPM) priceBreakdown(appCtx appcontext.AppContext, ppmShipment *m destPostal = ppmShipment.DestinationAddress.PostalCode } + // if the ZIPs are the same, we need to replace the DLH service item with DSH if len(pickupPostal) >= 3 && len(destPostal) >= 3 && pickupPostal[:3] == destPostal[:3] { if pickupPostal[0:3] == destPostal[0:3] { - serviceItemsToPrice[0] = models.MTOServiceItem{ReService: models.ReService{Code: models.ReServiceCodeDSH}, MTOShipmentID: &ppmShipment.ShipmentID} + for i, serviceItem := range serviceItemsToPrice { + if serviceItem.ReService.Code == models.ReServiceCodeDLH { + serviceItemsToPrice[i] = models.MTOServiceItem{ReService: models.ReService{Code: models.ReServiceCodeDSH}, MTOShipmentID: &ppmShipment.ShipmentID} + break + } + } } }