Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

B-21473 Add extra days for Alaska shipments #14655

Merged
merged 9 commits into from
Feb 25, 2025
1 change: 1 addition & 0 deletions migrations/app/migrations_manifest.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1091,6 +1091,7 @@
20250121153007_update_pricing_proc_to_handle_international_shuttle.up.sql
20250121184450_upd_duty_loc_B-22242.up.sql
20250123173216_add_destination_queue_db_func_and_gbloc_view.up.sql
20250123210535_update_re_intl_transit_times_for_ak_hhg.up.sql
20250204162411_updating_create_accessorial_service_item_proc_for_crating.up.sql
20250206173204_add_hawaii_data.up.sql
20250207153450_add_fetch_documents_func.up.sql
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
UPDATE re_intl_transit_times
SET hhg_transit_time = 10
WHERE origin_rate_area_id IN ('b80a00d4-f829-4051-961a-b8945c62c37d','5a27e806-21d4-4672-aa5e-29518f10c0aa')
OR destination_rate_area_id IN ('b80a00d4-f829-4051-961a-b8945c62c37d','5a27e806-21d4-4672-aa5e-29518f10c0aa');

update re_intl_transit_times
SET hhg_transit_time = 20
WHERE origin_rate_area_id IN ('9bb87311-1b29-4f29-8561-8a4c795654d4','635e4b79-342c-4cfc-8069-39c408a2decd')
OR destination_rate_area_id IN ('9bb87311-1b29-4f29-8561-8a4c795654d4','635e4b79-342c-4cfc-8069-39c408a2decd');
72 changes: 72 additions & 0 deletions pkg/factory/address_factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,3 +201,75 @@ func GetTraitAddress4() []Customization {
},
}
}

// GetTraitAddressAKZone1 is an address in Zone 1 of AK
func GetTraitAddressAKZone1() []Customization {

return []Customization{
{
Model: models.Address{
StreetAddress1: "82 Joe Gibbs Rd",
StreetAddress2: models.StringPointer("P.O. Box 1234"),
StreetAddress3: models.StringPointer("c/o Another Person"),
City: "ANCHORAGE",
State: "AK",
PostalCode: "99695",
IsOconus: models.BoolPointer(true),
},
},
}
}

// GetTraitAddressAKZone2 is an address in Zone 2 of Alaska
func GetTraitAddressAKZone2() []Customization {

return []Customization{
{
Model: models.Address{
StreetAddress1: "44 John Riggins Rd",
StreetAddress2: models.StringPointer("P.O. Box 1234"),
StreetAddress3: models.StringPointer("c/o Another Person"),
City: "FAIRBANKS",
State: "AK",
PostalCode: "99703",
IsOconus: models.BoolPointer(true),
},
},
}
}

// GetTraitAddressAKZone3 is an address in Zone 3 of Alaska
func GetTraitAddressAKZone3() []Customization {

return []Customization{
{
Model: models.Address{
StreetAddress1: "26 Clinton Portis Rd",
StreetAddress2: models.StringPointer("P.O. Box 1234"),
StreetAddress3: models.StringPointer("c/o Another Person"),
City: "KODIAK",
State: "AK",
PostalCode: "99697",
IsOconus: models.BoolPointer(true),
},
},
}
}

// GetTraitAddressAKZone4 is an address in Zone 4 of Alaska
func GetTraitAddressAKZone4() []Customization {

return []Customization{
{
Model: models.Address{
StreetAddress1: "8 Alex Ovechkin Rd",
StreetAddress2: models.StringPointer("P.O. Box 1234"),
StreetAddress3: models.StringPointer("c/o Another Person"),
City: "JUNEAU",
State: "AK",
PostalCode: "99801",
IsOconus: models.BoolPointer(true),
},
},
}
}
8 changes: 8 additions & 0 deletions pkg/models/address.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/gobuffalo/validate/v3"
"github.com/gobuffalo/validate/v3/validators"
"github.com/gofrs/uuid"
"github.com/pkg/errors"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"

Expand Down Expand Up @@ -146,6 +147,13 @@ func (a *Address) LineDisplayFormat() string {
return fmt.Sprintf("%s%s%s, %s, %s %s", a.StreetAddress1, optionalStreetAddress2, optionalStreetAddress3, a.City, a.State, a.PostalCode)
}

func (a *Address) IsAddressAlaska() (bool, error) {
if a == nil {
return false, errors.New("address is nil")
}
return a.State == "AK", nil
}

// NotImplementedCountryCode is the default for unimplemented country code lookup
type NotImplementedCountryCode struct {
message string
Expand Down
31 changes: 31 additions & 0 deletions pkg/models/address_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -385,3 +385,34 @@ func (suite *ModelSuite) Test_FetchDutyLocationGblocForAK() {
suite.Equal(string(*gbloc), "MAPK")
})
}

func (suite *ModelSuite) TestIsAddressAlaska() {
var address *m.Address
bool1, err := address.IsAddressAlaska()
suite.Error(err)
suite.Equal("address is nil", err.Error())
suite.Equal(false, bool1)

address = &m.Address{
StreetAddress1: "street 1",
StreetAddress2: m.StringPointer("street 2"),
StreetAddress3: m.StringPointer("street 3"),
City: "city",
PostalCode: "90210",
County: m.StringPointer("County"),
}

bool2, err := address.IsAddressAlaska()
suite.NoError(err)
suite.Equal(m.BoolPointer(false), &bool2)

address.State = "MT"
bool3, err := address.IsAddressAlaska()
suite.NoError(err)
suite.Equal(m.BoolPointer(false), &bool3)

address.State = "AK"
bool4, err := address.IsAddressAlaska()
suite.NoError(err)
suite.Equal(m.BoolPointer(true), &bool4)
}
75 changes: 53 additions & 22 deletions pkg/services/mto_shipment/mto_shipment_updater.go
Original file line number Diff line number Diff line change
Expand Up @@ -1075,7 +1075,7 @@ func (o *mtoShipmentStatusUpdater) setRequiredDeliveryDate(appCtx appcontext.App
pickupLocation = shipment.PickupAddress
deliveryLocation = shipment.DestinationAddress
}
requiredDeliveryDate, calcErr := CalculateRequiredDeliveryDate(appCtx, o.planner, *pickupLocation, *deliveryLocation, *shipment.ScheduledPickupDate, weight.Int(), shipment.MarketCode)
requiredDeliveryDate, calcErr := CalculateRequiredDeliveryDate(appCtx, o.planner, *pickupLocation, *deliveryLocation, *shipment.ScheduledPickupDate, weight.Int(), shipment.MarketCode, shipment.MoveTaskOrderID)
if calcErr != nil {
return calcErr
}
Expand Down Expand Up @@ -1192,18 +1192,7 @@ func reServiceCodesForShipment(shipment models.MTOShipment) []models.ReServiceCo
// CalculateRequiredDeliveryDate function is used to get a distance calculation using the pickup and destination addresses. It then uses
// the value returned to make a fetch on the ghc_domestic_transit_times table and returns a required delivery date
// based on the max_days_transit_time.
func CalculateRequiredDeliveryDate(appCtx appcontext.AppContext, planner route.Planner, pickupAddress models.Address, destinationAddress models.Address, pickupDate time.Time, weight int, marketCode models.MarketCode) (*time.Time, error) {
// Okay, so this is something to get us able to take care of the 20 day condition over in the gdoc linked in this
// story: https://dp3.atlassian.net/browse/MB-1141
// We unfortunately didn't get a lot of guidance regarding vicinity. So for now we're taking zip codes that are the
// explicitly mentioned 20 day cities and those in the same county (that I've manually compiled together here).
// If a move is in that group it adds 20 days, if it's not in that group, but is in Alaska it adds 10 days.
// Else it will not do either of those things.
// The cities for 20 days are: Adak, Kodiak, Juneau, Ketchikan, and Sitka. As well as others in their 'vicinity.'
twentyDayAKZips := [28]string{"99546", "99547", "99591", "99638", "99660", "99685", "99692", "99550", "99608",
"99615", "99619", "99624", "99643", "99644", "99697", "99650", "99801", "99802", "99803", "99811", "99812",
"99950", "99824", "99850", "99901", "99928", "99950", "99835"}

func CalculateRequiredDeliveryDate(appCtx appcontext.AppContext, planner route.Planner, pickupAddress models.Address, destinationAddress models.Address, pickupDate time.Time, weight int, marketCode models.MarketCode, moveID uuid.UUID) (*time.Time, error) {
internationalShipment := marketCode == models.MarketCodeInternational

distance, err := planner.ZipTransitDistance(appCtx, pickupAddress.PostalCode, destinationAddress.PostalCode, internationalShipment)
Expand All @@ -1225,17 +1214,59 @@ func CalculateRequiredDeliveryDate(appCtx appcontext.AppContext, planner route.P
// Add the max transit time to the pickup date to get the new required delivery date
requiredDeliveryDate := pickupDate.AddDate(0, 0, ghcDomesticTransitTime.MaxDaysTransitTime)

// Let's add some days if we're dealing with an alaska shipment.
if destinationAddress.State == "AK" {
for _, zip := range twentyDayAKZips {
if destinationAddress.PostalCode == zip {
// Add an extra 10 days here, so that after we add the 10 for being in AK we wind up with a total of 20
requiredDeliveryDate = requiredDeliveryDate.AddDate(0, 0, 10)
break
destinationIsAlaska, err := destinationAddress.IsAddressAlaska()
if err != nil {
return nil, fmt.Errorf("destination address is nil for move ID: %s", moveID)
}
pickupIsAlaska, err := pickupAddress.IsAddressAlaska()
if err != nil {
return nil, fmt.Errorf("pickup address is nil for move ID: %s", moveID)
}
// Let's add some days if we're dealing with a shipment between CONUS/Alaska
if (destinationIsAlaska || pickupIsAlaska) && !(destinationIsAlaska && pickupIsAlaska) {
var rateAreaID uuid.UUID
var intlTransTime models.InternationalTransitTime

contract, err := models.FetchContractForMove(appCtx, moveID)
if err != nil {
return nil, fmt.Errorf("error fetching contract for move ID: %s", moveID)
}

if destinationIsAlaska {
rateAreaID, err = models.FetchRateAreaID(appCtx.DB(), destinationAddress.ID, &uuid.Nil, contract.ID)
if err != nil {
return nil, fmt.Errorf("error fetching destination rate area id for address ID: %s", destinationAddress.ID)
}
err = appCtx.DB().Where("destination_rate_area_id = $1", rateAreaID).First(&intlTransTime)
if err != nil {
switch err {
case sql.ErrNoRows:
return nil, fmt.Errorf("no international transit time found for destination rate area ID: %s", rateAreaID)
default:
return nil, err
}
}
}

if pickupIsAlaska {
rateAreaID, err = models.FetchRateAreaID(appCtx.DB(), pickupAddress.ID, &uuid.Nil, contract.ID)
if err != nil {
return nil, fmt.Errorf("error fetching pickup rate area id for address ID: %s", pickupAddress.ID)
}
err = appCtx.DB().Where("origin_rate_area_id = $1", rateAreaID).First(&intlTransTime)
if err != nil {
switch err {
case sql.ErrNoRows:
return nil, fmt.Errorf("no international transit time found for pickup rate area ID: %s", rateAreaID)
default:
return nil, err
}
}
}
// Add an extra 10 days for being in AK
requiredDeliveryDate = requiredDeliveryDate.AddDate(0, 0, 10)

if intlTransTime.HhgTransitTime != nil {
requiredDeliveryDate = requiredDeliveryDate.AddDate(0, 0, *intlTransTime.HhgTransitTime)
}
}

// return the value
Expand Down
131 changes: 131 additions & 0 deletions pkg/services/mto_shipment/mto_shipment_updater_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2462,6 +2462,137 @@ func (suite *MTOShipmentServiceSuite) TestUpdateMTOShipmentStatus() {
}
})

suite.Run("Test that we are properly adding days to Alaska shipments", func() {
reContract := testdatagen.FetchOrMakeReContract(suite.DB(), testdatagen.Assertions{})
testdatagen.FetchOrMakeReContractYear(suite.DB(), testdatagen.Assertions{
ReContractYear: models.ReContractYear{
Contract: reContract,
ContractID: reContract.ID,
StartDate: time.Now(),
EndDate: time.Now().Add(time.Hour * 12),
Escalation: 1.0,
EscalationCompounded: 1.0,
},
})
move := factory.BuildAvailableToPrimeMove(suite.DB(), nil, nil)
appCtx := suite.AppContextForTest()

ghcDomesticTransitTime0LbsUpper := models.GHCDomesticTransitTime{
MaxDaysTransitTime: 12,
WeightLbsLower: 10001,
WeightLbsUpper: 0,
DistanceMilesLower: 0,
DistanceMilesUpper: 10000,
}
verrs, err := suite.DB().ValidateAndCreate(&ghcDomesticTransitTime0LbsUpper)
suite.Assert().False(verrs.HasAny())
suite.NoError(err)

conusAddress := factory.BuildAddress(suite.DB(), nil, []factory.Trait{factory.GetTraitAddress2})
zone1Address := factory.BuildAddress(suite.DB(), nil, []factory.Trait{factory.GetTraitAddressAKZone1})
zone2Address := factory.BuildAddress(suite.DB(), nil, []factory.Trait{factory.GetTraitAddressAKZone2})
zone3Address := factory.BuildAddress(suite.DB(), nil, []factory.Trait{factory.GetTraitAddressAKZone3})
zone4Address := factory.BuildAddress(suite.DB(), nil, []factory.Trait{factory.GetTraitAddressAKZone4})

estimatedWeight := unit.Pound(11000)

testCases10Days := []struct {
pickupLocation models.Address
destinationLocation models.Address
}{
{conusAddress, zone1Address},
{conusAddress, zone2Address},
{zone1Address, conusAddress},
{zone2Address, conusAddress},
}
// adding 22 days; ghcDomesticTransitTime0LbsUpper.MaxDaysTransitTime is 12, plus 10 for Zones 1 and 2
rdd10DaysDate := testdatagen.DateInsidePeakRateCycle.AddDate(0, 0, 22)
for _, testCase := range testCases10Days {
shipment := factory.BuildMTOShipmentMinimal(suite.DB(), []factory.Customization{
{
Model: move,
LinkOnly: true,
},
{
Model: models.MTOShipment{
ShipmentType: models.MTOShipmentTypeHHG,
ScheduledPickupDate: &testdatagen.DateInsidePeakRateCycle,
PrimeEstimatedWeight: &estimatedWeight,
Status: models.MTOShipmentStatusSubmitted,
},
},
{
Model: testCase.pickupLocation,
Type: &factory.Addresses.PickupAddress,
LinkOnly: true,
},
{
Model: testCase.destinationLocation,
Type: &factory.Addresses.DeliveryAddress,
LinkOnly: true,
},
}, nil)
shipmentEtag := etag.GenerateEtag(shipment.UpdatedAt)
_, err = updater.UpdateMTOShipmentStatus(appCtx, shipment.ID, status, nil, nil, shipmentEtag)
suite.NoError(err)

fetchedShipment := models.MTOShipment{}
err = suite.DB().Find(&fetchedShipment, shipment.ID)
suite.NoError(err)
suite.NotNil(fetchedShipment.RequiredDeliveryDate)
suite.Equal(rdd10DaysDate.Format(time.RFC3339), fetchedShipment.RequiredDeliveryDate.Format(time.RFC3339))
}

testCases20Days := []struct {
pickupLocation models.Address
destinationLocation models.Address
}{
{conusAddress, zone3Address},
{conusAddress, zone4Address},
{zone3Address, conusAddress},
{zone4Address, conusAddress},
}
// adding 32 days; ghcDomesticTransitTime0LbsUpper.MaxDaysTransitTime is 12, plus 20 for Zones 3 and 4
rdd20DaysDate := testdatagen.DateInsidePeakRateCycle.AddDate(0, 0, 32)
for _, testCase := range testCases20Days {
shipment := factory.BuildMTOShipmentMinimal(suite.DB(), []factory.Customization{
{
Model: move,
LinkOnly: true,
},
{
Model: models.MTOShipment{
ShipmentType: models.MTOShipmentTypeHHG,
ScheduledPickupDate: &testdatagen.DateInsidePeakRateCycle,
PrimeEstimatedWeight: &estimatedWeight,
Status: models.MTOShipmentStatusSubmitted,
},
},
{
Model: testCase.pickupLocation,
Type: &factory.Addresses.PickupAddress,
LinkOnly: true,
},
{
Model: testCase.destinationLocation,
Type: &factory.Addresses.DeliveryAddress,
LinkOnly: true,
},
}, nil)
shipmentEtag := etag.GenerateEtag(shipment.UpdatedAt)
_, err = updater.UpdateMTOShipmentStatus(appCtx, shipment.ID, status, nil, nil, shipmentEtag)
suite.NoError(err)

fetchedShipment := models.MTOShipment{}
err = suite.DB().Find(&fetchedShipment, shipment.ID)
suite.NoError(err)
suite.NotNil(fetchedShipment.RequiredDeliveryDate)
fmt.Println("fetchedShipment.RequiredDeliveryDate")
fmt.Println(fetchedShipment.RequiredDeliveryDate)
suite.Equal(rdd20DaysDate.Format(time.RFC3339), fetchedShipment.RequiredDeliveryDate.Format(time.RFC3339))
}
})

suite.Run("Cannot set SUBMITTED status on shipment via UpdateMTOShipmentStatus", func() {
setupTestData()

Expand Down
2 changes: 1 addition & 1 deletion pkg/services/mto_shipment/rules.go
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ func checkPrimeValidationsOnModel(planner route.Planner) validator {
weight = older.NTSRecordedWeight
}
requiredDeliveryDate, err := CalculateRequiredDeliveryDate(appCtx, planner, *latestPickupAddress,
*latestDestinationAddress, *latestSchedPickupDate, weight.Int(), older.MarketCode)
*latestDestinationAddress, *latestSchedPickupDate, weight.Int(), older.MarketCode, older.MoveTaskOrderID)
if err != nil {
verrs.Add("requiredDeliveryDate", err.Error())
}
Expand Down
Loading
Loading