From 249ffc53b5ca4645dfb1fbeebce264bc5555192c Mon Sep 17 00:00:00 2001 From: Steven Gleason <180579696+stevengleason-caci@users.noreply.github.com> Date: Mon, 16 Dec 2024 19:17:05 +0000 Subject: [PATCH 01/14] B-21569 add UB in mto_shipment_updater, only include Fuel Surcharge FSC if either origin or destination is CONUS. --- .../mto_shipment/mto_shipment_updater.go | 17 +++++ .../mto_shipment/mto_shipment_updater_test.go | 69 +++++++++++++++++++ 2 files changed, 86 insertions(+) diff --git a/pkg/services/mto_shipment/mto_shipment_updater.go b/pkg/services/mto_shipment/mto_shipment_updater.go index 5b3bd8cb659..733a0f5a0b2 100644 --- a/pkg/services/mto_shipment/mto_shipment_updater.go +++ b/pkg/services/mto_shipment/mto_shipment_updater.go @@ -1134,6 +1134,23 @@ func reServiceCodesForShipment(shipment models.MTOShipment) []models.ReServiceCo models.ReServiceCodeDDP, models.ReServiceCodeDBTF, } + case models.MTOShipmentTypeUnaccompaniedBaggage: + pickupIsOconus, _ := models.IsAddressOconus(nil, *shipment.PickupAddress) + destinationIsOconus, _ := models.IsAddressOconus(nil, *shipment.DestinationAddress) + if pickupIsOconus && destinationIsOconus { + return []models.ReServiceCode{ + models.ReServiceCodeUBP, + models.ReServiceCodeIUBPK, + models.ReServiceCodeIUBUPK, + } + } else { + return []models.ReServiceCode{ + models.ReServiceCodeUBP, + models.ReServiceCodeFSC, + models.ReServiceCodeIUBPK, + models.ReServiceCodeIUBUPK, + } + } } return []models.ReServiceCode{} diff --git a/pkg/services/mto_shipment/mto_shipment_updater_test.go b/pkg/services/mto_shipment/mto_shipment_updater_test.go index dfabece3112..1a591542a1c 100644 --- a/pkg/services/mto_shipment/mto_shipment_updater_test.go +++ b/pkg/services/mto_shipment/mto_shipment_updater_test.go @@ -3030,4 +3030,73 @@ func (suite *MTOShipmentServiceSuite) TestUpdateStatusServiceItems() { suite.Equal(models.ReServiceCodeDSH, serviceItems[0].ReService.Code) }) + + suite.Run("Service Codes for UB CONUS CONUS", func() { + usCountry := models.Country{ + Country: "US", + } + conusAddress := models.Address{ + Country: &usCountry, + State: "MO", + } + mtoShipment := models.MTOShipment{ + DestinationAddress: &conusAddress, + PickupAddress: &conusAddress, + ShipmentType: "UNACCOMPANIED_BAGGAGE", + } + + serviceCodes := reServiceCodesForShipment(mtoShipment) + + suite.Equal(models.ReServiceCodeUBP, serviceCodes[0]) + suite.Equal(models.ReServiceCodeFSC, serviceCodes[1]) + suite.Equal(models.ReServiceCodeIUBPK, serviceCodes[2]) + suite.Equal(models.ReServiceCodeIUBUPK, serviceCodes[3]) + }) + + suite.Run("Service Codes for UB CONUS OCONUS", func() { + usCountry := models.Country{ + Country: "US", + } + conusAddress := models.Address{ + Country: &usCountry, + State: "MO", + } + oconusAddress := models.Address{ + Country: &usCountry, + State: "AK", + } + mtoShipment := models.MTOShipment{ + DestinationAddress: &oconusAddress, + PickupAddress: &conusAddress, + ShipmentType: "UNACCOMPANIED_BAGGAGE", + } + + serviceCodes := reServiceCodesForShipment(mtoShipment) + + suite.Equal(models.ReServiceCodeUBP, serviceCodes[0]) + suite.Equal(models.ReServiceCodeFSC, serviceCodes[1]) + suite.Equal(models.ReServiceCodeIUBPK, serviceCodes[2]) + suite.Equal(models.ReServiceCodeIUBUPK, serviceCodes[3]) + }) + + suite.Run("Service Codes for UB OCONUS OCONUS", func() { + usCountry := models.Country{ + Country: "US", + } + oconusAddress := models.Address{ + Country: &usCountry, + State: "AK", + } + mtoShipment := models.MTOShipment{ + DestinationAddress: &oconusAddress, + PickupAddress: &oconusAddress, + ShipmentType: "UNACCOMPANIED_BAGGAGE", + } + + serviceCodes := reServiceCodesForShipment(mtoShipment) + + suite.Equal(models.ReServiceCodeUBP, serviceCodes[0]) + suite.Equal(models.ReServiceCodeIUBPK, serviceCodes[1]) + suite.Equal(models.ReServiceCodeIUBUPK, serviceCodes[2]) + }) } From c7cf1d827388b02e01355b6770d1b5b3c8e89147 Mon Sep 17 00:00:00 2001 From: Steven Gleason <180579696+stevengleason-caci@users.noreply.github.com> Date: Fri, 27 Dec 2024 22:16:45 +0000 Subject: [PATCH 02/14] B-21569 fix DB error for address OCONUS check. --- .../mto_shipment/mto_shipment_updater.go | 18 ++++++++++++++---- .../mto_shipment/mto_shipment_updater_test.go | 6 +++--- pkg/services/mto_shipment/shipment_approver.go | 2 +- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/pkg/services/mto_shipment/mto_shipment_updater.go b/pkg/services/mto_shipment/mto_shipment_updater.go index 92932d72f3b..ed9cb780346 100644 --- a/pkg/services/mto_shipment/mto_shipment_updater.go +++ b/pkg/services/mto_shipment/mto_shipment_updater.go @@ -971,7 +971,7 @@ func (o *mtoShipmentStatusUpdater) UpdateMTOShipmentStatus(appCtx appcontext.App // createShipmentServiceItems creates shipment level service items func (o *mtoShipmentStatusUpdater) createShipmentServiceItems(appCtx appcontext.AppContext, shipment *models.MTOShipment) error { - reServiceCodes := reServiceCodesForShipment(*shipment) + reServiceCodes := reServiceCodesForShipment(appCtx, *shipment) serviceItemsToCreate := constructMTOServiceItemModels(shipment.ID, shipment.MoveTaskOrderID, reServiceCodes) for _, serviceItem := range serviceItemsToCreate { copyOfServiceItem := serviceItem // Make copy to avoid implicit memory aliasing of items from a range statement. @@ -1058,7 +1058,7 @@ func fetchShipment(appCtx appcontext.AppContext, shipmentID uuid.UUID, builder U return &shipment, nil } -func reServiceCodesForShipment(shipment models.MTOShipment) []models.ReServiceCode { +func reServiceCodesForShipment(appCtx appcontext.AppContext, shipment models.MTOShipment) []models.ReServiceCode { // We will detect the type of shipment we're working with and then call a helper with the correct // default service items that we want created as a side effect. // More info in MB-1140: https://dp3.atlassian.net/browse/MB-1140 @@ -1137,8 +1137,18 @@ func reServiceCodesForShipment(shipment models.MTOShipment) []models.ReServiceCo models.ReServiceCodeDBTF, } case models.MTOShipmentTypeUnaccompaniedBaggage: - pickupIsOconus, _ := models.IsAddressOconus(nil, *shipment.PickupAddress) - destinationIsOconus, _ := models.IsAddressOconus(nil, *shipment.DestinationAddress) + pickupIsOconus, err := models.IsAddressOconus(appCtx.DB(), *shipment.PickupAddress) + if err != nil { + appCtx.Logger().Error(err.Error()) + return nil + } + + destinationIsOconus, err := models.IsAddressOconus(appCtx.DB(), *shipment.DestinationAddress) + if err != nil { + appCtx.Logger().Error(err.Error()) + return nil + } + if pickupIsOconus && destinationIsOconus { return []models.ReServiceCode{ models.ReServiceCodeUBP, diff --git a/pkg/services/mto_shipment/mto_shipment_updater_test.go b/pkg/services/mto_shipment/mto_shipment_updater_test.go index 56f55b5ae3b..2ba7bfc2fc8 100644 --- a/pkg/services/mto_shipment/mto_shipment_updater_test.go +++ b/pkg/services/mto_shipment/mto_shipment_updater_test.go @@ -3045,7 +3045,7 @@ func (suite *MTOShipmentServiceSuite) TestUpdateStatusServiceItems() { ShipmentType: "UNACCOMPANIED_BAGGAGE", } - serviceCodes := reServiceCodesForShipment(mtoShipment) + serviceCodes := reServiceCodesForShipment(suite.AppContextForTest(), mtoShipment) suite.Equal(models.ReServiceCodeUBP, serviceCodes[0]) suite.Equal(models.ReServiceCodeFSC, serviceCodes[1]) @@ -3071,7 +3071,7 @@ func (suite *MTOShipmentServiceSuite) TestUpdateStatusServiceItems() { ShipmentType: "UNACCOMPANIED_BAGGAGE", } - serviceCodes := reServiceCodesForShipment(mtoShipment) + serviceCodes := reServiceCodesForShipment(suite.AppContextForTest(), mtoShipment) suite.Equal(models.ReServiceCodeUBP, serviceCodes[0]) suite.Equal(models.ReServiceCodeFSC, serviceCodes[1]) @@ -3093,7 +3093,7 @@ func (suite *MTOShipmentServiceSuite) TestUpdateStatusServiceItems() { ShipmentType: "UNACCOMPANIED_BAGGAGE", } - serviceCodes := reServiceCodesForShipment(mtoShipment) + serviceCodes := reServiceCodesForShipment(suite.AppContextForTest(), mtoShipment) suite.Equal(models.ReServiceCodeUBP, serviceCodes[0]) suite.Equal(models.ReServiceCodeIUBPK, serviceCodes[1]) diff --git a/pkg/services/mto_shipment/shipment_approver.go b/pkg/services/mto_shipment/shipment_approver.go index 52e849e469b..a6ff3ab05f3 100644 --- a/pkg/services/mto_shipment/shipment_approver.go +++ b/pkg/services/mto_shipment/shipment_approver.go @@ -181,7 +181,7 @@ func (f *shipmentApprover) setRequiredDeliveryDate(appCtx appcontext.AppContext, } func (f *shipmentApprover) createShipmentServiceItems(appCtx appcontext.AppContext, shipment *models.MTOShipment) error { - reServiceCodes := reServiceCodesForShipment(*shipment) + reServiceCodes := reServiceCodesForShipment(appCtx, *shipment) serviceItemsToCreate := constructMTOServiceItemModels(shipment.ID, shipment.MoveTaskOrderID, reServiceCodes) for _, serviceItem := range serviceItemsToCreate { copyOfServiceItem := serviceItem // Make copy to avoid implicit memory aliasing of items from a range statement. From a331b8b7dbcab10adde61f76d550eba20c61597f Mon Sep 17 00:00:00 2001 From: Steven Gleason <180579696+stevengleason-caci@users.noreply.github.com> Date: Fri, 3 Jan 2025 18:20:38 +0000 Subject: [PATCH 03/14] B-21569 use the DB in shipment_approver instead of codded values in mto_shipment_updater, moved the tests over and updated for POEFSC and PODFSC instead of just FSC. --- .../mto_shipment/mto_shipment_updater.go | 31 +-- .../mto_shipment/mto_shipment_updater_test.go | 69 ------- .../mto_shipment/shipment_approver.go | 4 +- .../mto_shipment/shipment_approver_test.go | 180 ++++++++++++++++++ 4 files changed, 184 insertions(+), 100 deletions(-) diff --git a/pkg/services/mto_shipment/mto_shipment_updater.go b/pkg/services/mto_shipment/mto_shipment_updater.go index ed9cb780346..92ca8ba58c8 100644 --- a/pkg/services/mto_shipment/mto_shipment_updater.go +++ b/pkg/services/mto_shipment/mto_shipment_updater.go @@ -971,7 +971,7 @@ func (o *mtoShipmentStatusUpdater) UpdateMTOShipmentStatus(appCtx appcontext.App // createShipmentServiceItems creates shipment level service items func (o *mtoShipmentStatusUpdater) createShipmentServiceItems(appCtx appcontext.AppContext, shipment *models.MTOShipment) error { - reServiceCodes := reServiceCodesForShipment(appCtx, *shipment) + reServiceCodes := reServiceCodesForShipment(*shipment) serviceItemsToCreate := constructMTOServiceItemModels(shipment.ID, shipment.MoveTaskOrderID, reServiceCodes) for _, serviceItem := range serviceItemsToCreate { copyOfServiceItem := serviceItem // Make copy to avoid implicit memory aliasing of items from a range statement. @@ -1058,7 +1058,7 @@ func fetchShipment(appCtx appcontext.AppContext, shipmentID uuid.UUID, builder U return &shipment, nil } -func reServiceCodesForShipment(appCtx appcontext.AppContext, shipment models.MTOShipment) []models.ReServiceCode { +func reServiceCodesForShipment(shipment models.MTOShipment) []models.ReServiceCode { // We will detect the type of shipment we're working with and then call a helper with the correct // default service items that we want created as a side effect. // More info in MB-1140: https://dp3.atlassian.net/browse/MB-1140 @@ -1136,33 +1136,6 @@ func reServiceCodesForShipment(appCtx appcontext.AppContext, shipment models.MTO models.ReServiceCodeDDP, models.ReServiceCodeDBTF, } - case models.MTOShipmentTypeUnaccompaniedBaggage: - pickupIsOconus, err := models.IsAddressOconus(appCtx.DB(), *shipment.PickupAddress) - if err != nil { - appCtx.Logger().Error(err.Error()) - return nil - } - - destinationIsOconus, err := models.IsAddressOconus(appCtx.DB(), *shipment.DestinationAddress) - if err != nil { - appCtx.Logger().Error(err.Error()) - return nil - } - - if pickupIsOconus && destinationIsOconus { - return []models.ReServiceCode{ - models.ReServiceCodeUBP, - models.ReServiceCodeIUBPK, - models.ReServiceCodeIUBUPK, - } - } else { - return []models.ReServiceCode{ - models.ReServiceCodeUBP, - models.ReServiceCodeFSC, - models.ReServiceCodeIUBPK, - models.ReServiceCodeIUBUPK, - } - } } return []models.ReServiceCode{} diff --git a/pkg/services/mto_shipment/mto_shipment_updater_test.go b/pkg/services/mto_shipment/mto_shipment_updater_test.go index 2ba7bfc2fc8..aabab4f1a16 100644 --- a/pkg/services/mto_shipment/mto_shipment_updater_test.go +++ b/pkg/services/mto_shipment/mto_shipment_updater_test.go @@ -3030,73 +3030,4 @@ func (suite *MTOShipmentServiceSuite) TestUpdateStatusServiceItems() { suite.Equal(models.ReServiceCodeDSH, serviceItems[0].ReService.Code) }) - - suite.Run("Service Codes for UB CONUS CONUS", func() { - usCountry := models.Country{ - Country: "US", - } - conusAddress := models.Address{ - Country: &usCountry, - State: "MO", - } - mtoShipment := models.MTOShipment{ - DestinationAddress: &conusAddress, - PickupAddress: &conusAddress, - ShipmentType: "UNACCOMPANIED_BAGGAGE", - } - - serviceCodes := reServiceCodesForShipment(suite.AppContextForTest(), mtoShipment) - - suite.Equal(models.ReServiceCodeUBP, serviceCodes[0]) - suite.Equal(models.ReServiceCodeFSC, serviceCodes[1]) - suite.Equal(models.ReServiceCodeIUBPK, serviceCodes[2]) - suite.Equal(models.ReServiceCodeIUBUPK, serviceCodes[3]) - }) - - suite.Run("Service Codes for UB CONUS OCONUS", func() { - usCountry := models.Country{ - Country: "US", - } - conusAddress := models.Address{ - Country: &usCountry, - State: "MO", - } - oconusAddress := models.Address{ - Country: &usCountry, - State: "AK", - } - mtoShipment := models.MTOShipment{ - DestinationAddress: &oconusAddress, - PickupAddress: &conusAddress, - ShipmentType: "UNACCOMPANIED_BAGGAGE", - } - - serviceCodes := reServiceCodesForShipment(suite.AppContextForTest(), mtoShipment) - - suite.Equal(models.ReServiceCodeUBP, serviceCodes[0]) - suite.Equal(models.ReServiceCodeFSC, serviceCodes[1]) - suite.Equal(models.ReServiceCodeIUBPK, serviceCodes[2]) - suite.Equal(models.ReServiceCodeIUBUPK, serviceCodes[3]) - }) - - suite.Run("Service Codes for UB OCONUS OCONUS", func() { - usCountry := models.Country{ - Country: "US", - } - oconusAddress := models.Address{ - Country: &usCountry, - State: "AK", - } - mtoShipment := models.MTOShipment{ - DestinationAddress: &oconusAddress, - PickupAddress: &oconusAddress, - ShipmentType: "UNACCOMPANIED_BAGGAGE", - } - - serviceCodes := reServiceCodesForShipment(suite.AppContextForTest(), mtoShipment) - - suite.Equal(models.ReServiceCodeUBP, serviceCodes[0]) - suite.Equal(models.ReServiceCodeIUBPK, serviceCodes[1]) - suite.Equal(models.ReServiceCodeIUBUPK, serviceCodes[2]) - }) } diff --git a/pkg/services/mto_shipment/shipment_approver.go b/pkg/services/mto_shipment/shipment_approver.go index a6ff3ab05f3..8305b77ebed 100644 --- a/pkg/services/mto_shipment/shipment_approver.go +++ b/pkg/services/mto_shipment/shipment_approver.go @@ -78,7 +78,7 @@ func (f *shipmentApprover) ApproveShipment(appCtx appcontext.AppContext, shipmen } // create international shipment service items - if shipment.ShipmentType == models.MTOShipmentTypeHHG && shipment.MarketCode == models.MarketCodeInternational { + if (shipment.ShipmentType == models.MTOShipmentTypeHHG || shipment.ShipmentType == models.MTOShipmentTypeUnaccompaniedBaggage) && shipment.MarketCode == models.MarketCodeInternational { err := models.CreateApprovedServiceItemsForShipment(appCtx.DB(), shipment) if err != nil { return shipment, err @@ -181,7 +181,7 @@ func (f *shipmentApprover) setRequiredDeliveryDate(appCtx appcontext.AppContext, } func (f *shipmentApprover) createShipmentServiceItems(appCtx appcontext.AppContext, shipment *models.MTOShipment) error { - reServiceCodes := reServiceCodesForShipment(appCtx, *shipment) + reServiceCodes := reServiceCodesForShipment(*shipment) serviceItemsToCreate := constructMTOServiceItemModels(shipment.ID, shipment.MoveTaskOrderID, reServiceCodes) for _, serviceItem := range serviceItemsToCreate { copyOfServiceItem := serviceItem // Make copy to avoid implicit memory aliasing of items from a range statement. diff --git a/pkg/services/mto_shipment/shipment_approver_test.go b/pkg/services/mto_shipment/shipment_approver_test.go index 943cd4edb72..2284124206c 100644 --- a/pkg/services/mto_shipment/shipment_approver_test.go +++ b/pkg/services/mto_shipment/shipment_approver_test.go @@ -787,4 +787,184 @@ func (suite *MTOShipmentServiceSuite) TestApproveShipment() { suite.NotNil(shipment.MoveTaskOrder.ExcessWeightQualifiedAt) }) + + suite.Run("If the CONUS to OCONUS UB mtoShipment is approved successfully it should create pre approved mtoServiceItems", func() { + internationalShipment := factory.BuildMTOShipment(suite.AppContextForTest().DB(), []factory.Customization{ + { + Model: models.Move{ + Status: models.MoveStatusAPPROVED, + }, + }, + { + Model: models.Address{ + StreetAddress1: "Tester Address", + City: "Des Moines", + State: "IA", + PostalCode: "50314", + IsOconus: models.BoolPointer(false), + }, + Type: &factory.Addresses.PickupAddress, + }, + { + Model: models.MTOShipment{ + MarketCode: "i", + Status: models.MTOShipmentStatusSubmitted, + ShipmentType: "UNACCOMPANIED_BAGGAGE", + }, + }, + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "Anchorage", + State: "AK", + PostalCode: "99505", + IsOconus: models.BoolPointer(true), + }, + Type: &factory.Addresses.DeliveryAddress, + }, + }, nil) + + internationalShipmentEtag := etag.GenerateEtag(internationalShipment.UpdatedAt) + shipmentApprover := suite.createApproveShipmentSubtestData().shipmentApprover + _, err := shipmentApprover.ApproveShipment(suite.AppContextForTest(), internationalShipment.ID, internationalShipmentEtag) + suite.NoError(err) + + // Get created pre approved service items + var serviceItems []models.MTOServiceItem + err2 := suite.AppContextForTest().DB().EagerPreload("ReService").Where("mto_shipment_id = ?", internationalShipment.ID).Order("created_at asc").All(&serviceItems) + suite.NoError(err2) + + expectedReserviceCodes := []models.ReServiceCode{ + models.ReServiceCodeUBP, + models.ReServiceCodeIUBPK, + models.ReServiceCodeIUBUPK, + models.ReServiceCodePOEFSC, + } + + suite.Equal(4, len(serviceItems)) + for i := 0; i < len(serviceItems); i++ { + actualReServiceCode := serviceItems[i].ReService.Code + suite.True(slices.Contains(expectedReserviceCodes, actualReServiceCode), "Contains unexpected: "+actualReServiceCode.String()) + } + }) + + suite.Run("If the OCONUS to CONUS UB mtoShipment is approved successfully it should create pre approved mtoServiceItems", func() { + internationalShipment := factory.BuildMTOShipment(suite.AppContextForTest().DB(), []factory.Customization{ + { + Model: models.Move{ + Status: models.MoveStatusAPPROVED, + }, + }, + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "Anchorage", + State: "AK", + PostalCode: "99505", + IsOconus: models.BoolPointer(true), + }, + Type: &factory.Addresses.PickupAddress, + }, + { + Model: models.MTOShipment{ + MarketCode: "i", + Status: models.MTOShipmentStatusSubmitted, + ShipmentType: "UNACCOMPANIED_BAGGAGE", + }, + }, + { + Model: models.Address{ + StreetAddress1: "Tester Address", + City: "Des Moines", + State: "IA", + PostalCode: "50314", + IsOconus: models.BoolPointer(false), + }, + Type: &factory.Addresses.DeliveryAddress, + }, + }, nil) + + internationalShipmentEtag := etag.GenerateEtag(internationalShipment.UpdatedAt) + shipmentApprover := suite.createApproveShipmentSubtestData().shipmentApprover + _, err := shipmentApprover.ApproveShipment(suite.AppContextForTest(), internationalShipment.ID, internationalShipmentEtag) + suite.NoError(err) + + // Get created pre approved service items + var serviceItems []models.MTOServiceItem + err2 := suite.AppContextForTest().DB().EagerPreload("ReService").Where("mto_shipment_id = ?", internationalShipment.ID).Order("created_at asc").All(&serviceItems) + suite.NoError(err2) + + expectedReserviceCodes := []models.ReServiceCode{ + models.ReServiceCodeUBP, + models.ReServiceCodeIUBPK, + models.ReServiceCodeIUBUPK, + models.ReServiceCodePODFSC, + } + + suite.Equal(4, len(serviceItems)) + for i := 0; i < len(serviceItems); i++ { + actualReServiceCode := serviceItems[i].ReService.Code + suite.True(slices.Contains(expectedReserviceCodes, actualReServiceCode), "Contains unexpected: "+actualReServiceCode.String()) + } + }) + + suite.Run("If the OCONUS to OCONUS UB mtoShipment is approved successfully it should create pre approved mtoServiceItems", func() { + internationalShipment := factory.BuildMTOShipment(suite.AppContextForTest().DB(), []factory.Customization{ + { + Model: models.Move{ + Status: models.MoveStatusAPPROVED, + }, + }, + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "Anchorage", + State: "AK", + PostalCode: "99505", + IsOconus: models.BoolPointer(true), + }, + Type: &factory.Addresses.PickupAddress, + }, + { + Model: models.MTOShipment{ + MarketCode: "i", + Status: models.MTOShipmentStatusSubmitted, + ShipmentType: "UNACCOMPANIED_BAGGAGE", + }, + }, + { + Model: models.Address{ + StreetAddress1: "Tester Address", + City: "Fairbanks", + State: "AK", + PostalCode: "99701", + IsOconus: models.BoolPointer(true), + }, + Type: &factory.Addresses.DeliveryAddress, + }, + }, nil) + + internationalShipmentEtag := etag.GenerateEtag(internationalShipment.UpdatedAt) + shipmentApprover := suite.createApproveShipmentSubtestData().shipmentApprover + _, err := shipmentApprover.ApproveShipment(suite.AppContextForTest(), internationalShipment.ID, internationalShipmentEtag) + suite.NoError(err) + + // Get created pre approved service items + var serviceItems []models.MTOServiceItem + err2 := suite.AppContextForTest().DB().EagerPreload("ReService").Where("mto_shipment_id = ?", internationalShipment.ID).Order("created_at asc").All(&serviceItems) + suite.NoError(err2) + + expectedReserviceCodes := []models.ReServiceCode{ + models.ReServiceCodeUBP, + models.ReServiceCodeIUBPK, + models.ReServiceCodeIUBUPK, + } + + suite.Equal(3, len(serviceItems)) + for i := 0; i < len(serviceItems); i++ { + actualReServiceCode := serviceItems[i].ReService.Code + suite.True(slices.Contains(expectedReserviceCodes, actualReServiceCode), "Contains unexpected: "+actualReServiceCode.String()) + } + }) + } From f326b958af82e2ac2b10cadda0b3dbee4b4d54d3 Mon Sep 17 00:00:00 2001 From: Steven Gleason <180579696+stevengleason-caci@users.noreply.github.com> Date: Tue, 7 Jan 2025 15:50:58 +0000 Subject: [PATCH 04/14] B-21569 update the preview to exclude FSC for OCONUS to OCONUS. --- migrations/app/migrations_manifest.txt | 2 +- .../ShipmentServiceItemsTable.jsx | 4 ++-- .../ShipmentServiceItemsTable.test.jsx | 20 +++++++++++++++++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/migrations/app/migrations_manifest.txt b/migrations/app/migrations_manifest.txt index a31d11f3f55..a2fde11ae69 100644 --- a/migrations/app/migrations_manifest.txt +++ b/migrations/app/migrations_manifest.txt @@ -1053,9 +1053,9 @@ 20241204155919_update_ordering_proc.up.sql 20241204210208_retroactive_update_of_ppm_max_and_estimated_incentives_prd.up.sql 20241210143143_redefine_mto_shipment_audit_table.up.sql -20241218201833_add_PPPO_BASE_ELIZABETH.up.sql 20241217163231_update_duty_locations_bad_zips.up.sql 20241217180136_add_AK_zips_to_zip3_distances.up.sql +20241218201833_add_PPPO_BASE_ELIZABETH.up.sql 20241220171035_add_additional_AK_zips_to_zip3_distances.up.sql 20241227153723_remove_empty_string_emplid_values.up.sql 20241230190638_remove_AK_zips_from_zip3.up.sql diff --git a/src/components/Office/ShipmentServiceItemsTable/ShipmentServiceItemsTable.jsx b/src/components/Office/ShipmentServiceItemsTable/ShipmentServiceItemsTable.jsx index 272d341a662..f6d2e5e6ac8 100644 --- a/src/components/Office/ShipmentServiceItemsTable/ShipmentServiceItemsTable.jsx +++ b/src/components/Office/ShipmentServiceItemsTable/ShipmentServiceItemsTable.jsx @@ -59,12 +59,12 @@ function filterPortFuelSurcharge(shipment, autoApprovedItems) { const { destinationAddress, pickupAddress } = shipment; let filteredPortFuelSurchargeList = autoApprovedItems; if (pickupAddress.isOconus) { - filteredPortFuelSurchargeList = autoApprovedItems.filter((serviceItem) => { + filteredPortFuelSurchargeList = filteredPortFuelSurchargeList.filter((serviceItem) => { return serviceItem.serviceCode !== SERVICE_ITEM_CODES.POEFSC; }); } if (destinationAddress.isOconus) { - filteredPortFuelSurchargeList = autoApprovedItems.filter((serviceItem) => { + filteredPortFuelSurchargeList = filteredPortFuelSurchargeList.filter((serviceItem) => { return serviceItem.serviceCode !== SERVICE_ITEM_CODES.PODFSC; }); } diff --git a/src/components/Office/ShipmentServiceItemsTable/ShipmentServiceItemsTable.test.jsx b/src/components/Office/ShipmentServiceItemsTable/ShipmentServiceItemsTable.test.jsx index 498bab07788..9b18298293b 100644 --- a/src/components/Office/ShipmentServiceItemsTable/ShipmentServiceItemsTable.test.jsx +++ b/src/components/Office/ShipmentServiceItemsTable/ShipmentServiceItemsTable.test.jsx @@ -294,6 +294,13 @@ const intlUbOconusToConusShipment = { destinationAddress, }; +const intlUbOconusToOconusShipment = { + shipmentType: SHIPMENT_OPTIONS.UNACCOMPANIED_BAGGAGE, + marketCode: MARKET_CODES.INTERNATIONAL, + pickupAddress: oconusPickupAddress, + destinationAddress: oconusDestinationAddress, +}; + describe('Shipment Service Items Table', () => { describe('renders the hhg longhaul shipment type with service items', () => { it.each([ @@ -390,4 +397,17 @@ describe('Shipment Service Items Table', () => { expect(screen.getByText(serviceItem)).toBeInTheDocument(); }); }); + + describe('renders the intl UB shipment type (OCONUS -> OCONUS) with service items', () => { + it.each([['International UB'], ['International UB pack'], ['International UB unpack']])( + 'expects %s to be in the document', + async (serviceItem) => { + render(); + expect( + await screen.findByRole('heading', { name: 'Service items for this shipment 3 items', level: 4 }), + ).toBeInTheDocument(); + expect(screen.getByText(serviceItem)).toBeInTheDocument(); + }, + ); + }); }); From 580a4e6954191ae300a66e9ba6d3e02237021ccd Mon Sep 17 00:00:00 2001 From: Steven Gleason <180579696+stevengleason-caci@users.noreply.github.com> Date: Tue, 7 Jan 2025 20:09:05 +0000 Subject: [PATCH 05/14] B-21569 cleanup tests, use models variables. --- pkg/services/mto_shipment/shipment_approver_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/services/mto_shipment/shipment_approver_test.go b/pkg/services/mto_shipment/shipment_approver_test.go index 2284124206c..886aea6f3f9 100644 --- a/pkg/services/mto_shipment/shipment_approver_test.go +++ b/pkg/services/mto_shipment/shipment_approver_test.go @@ -807,9 +807,9 @@ func (suite *MTOShipmentServiceSuite) TestApproveShipment() { }, { Model: models.MTOShipment{ - MarketCode: "i", + MarketCode: models.MarketCodeInternational, Status: models.MTOShipmentStatusSubmitted, - ShipmentType: "UNACCOMPANIED_BAGGAGE", + ShipmentType: models.MTOShipmentTypeUnaccompaniedBaggage, }, }, { @@ -867,9 +867,9 @@ func (suite *MTOShipmentServiceSuite) TestApproveShipment() { }, { Model: models.MTOShipment{ - MarketCode: "i", + MarketCode: models.MarketCodeInternational, Status: models.MTOShipmentStatusSubmitted, - ShipmentType: "UNACCOMPANIED_BAGGAGE", + ShipmentType: models.MTOShipmentTypeUnaccompaniedBaggage, }, }, { @@ -927,9 +927,9 @@ func (suite *MTOShipmentServiceSuite) TestApproveShipment() { }, { Model: models.MTOShipment{ - MarketCode: "i", + MarketCode: models.MarketCodeInternational, Status: models.MTOShipmentStatusSubmitted, - ShipmentType: "UNACCOMPANIED_BAGGAGE", + ShipmentType: models.MTOShipmentTypeUnaccompaniedBaggage, }, }, { From e1c92549fd541a0444f45273f8e4610a73614759 Mon Sep 17 00:00:00 2001 From: Steven Gleason <180579696+stevengleason-caci@users.noreply.github.com> Date: Sat, 11 Jan 2025 00:12:58 +0000 Subject: [PATCH 06/14] B-21569 add the service items in their Sort order. Instead of inserting them as we encounter them in the proc, collect them into a temp table, sort them, and then insert. --- migrations/app/migrations_manifest.txt | 1 + ...60244_update_ordering_service_items.up.sql | 203 ++++++++++++++++++ .../mto_shipment/shipment_approver_test.go | 13 +- 3 files changed, 209 insertions(+), 8 deletions(-) create mode 100644 migrations/app/schema/20250110160244_update_ordering_service_items.up.sql diff --git a/migrations/app/migrations_manifest.txt b/migrations/app/migrations_manifest.txt index a2fde11ae69..221f6b2cffd 100644 --- a/migrations/app/migrations_manifest.txt +++ b/migrations/app/migrations_manifest.txt @@ -1060,3 +1060,4 @@ 20241227153723_remove_empty_string_emplid_values.up.sql 20241230190638_remove_AK_zips_from_zip3.up.sql 20241230190647_add_missing_AK_zips_to_zip3_distances.up.sql +20250110160244_update_ordering_service_items.up.sql diff --git a/migrations/app/schema/20250110160244_update_ordering_service_items.up.sql b/migrations/app/schema/20250110160244_update_ordering_service_items.up.sql new file mode 100644 index 00000000000..7b5196b9272 --- /dev/null +++ b/migrations/app/schema/20250110160244_update_ordering_service_items.up.sql @@ -0,0 +1,203 @@ +CREATE OR REPLACE PROCEDURE create_approved_service_items_for_shipment( + IN shipment_id UUID +) +AS ' +DECLARE + s_status mto_shipment_status; + s_type mto_shipment_type; + m_code market_code_enum; + move_id UUID; + pickup_address_id UUID; + destination_address_id UUID; + is_pickup_oconus BOOLEAN; + is_destination_oconus BOOLEAN; + service_item RECORD; +BEGIN + -- get shipment type, market code, move_id, and address IDs based on shipment_id + SELECT ms.shipment_type, ms.market_code, ms.move_id, ms.pickup_address_id, ms.destination_address_id, ms.status + INTO s_type, m_code, move_id, pickup_address_id, destination_address_id, s_status + FROM mto_shipments ms + WHERE ms.id = shipment_id; + + IF s_type IS NULL OR m_code IS NULL THEN + RAISE EXCEPTION ''Shipment with ID % not found or missing required details.'', shipment_id; + END IF; + + IF s_status IN (''APPROVED'') THEN + RAISE EXCEPTION ''Shipment with ID % is already in APPROVED status'', shipment_id; + END IF; + + -- get the is_oconus values for both pickup and destination addresses - this determines POD/POE creation + is_pickup_oconus := get_is_oconus(pickup_address_id); + is_destination_oconus := get_is_oconus(destination_address_id); + + -- determine which service item to create based on shipment direction + -- collect the service items into a temporary table for sorting + CREATE TEMPORARY TABLE temp_mto_service_items ( + mto_shipment_id uuid, + move_id uuid, + re_service_id uuid, + service_location service_location_enum, + status service_item_status, + sort text + ) ON COMMIT DROP; + + -- first create the direction-specific service item (POEFSC or PODFSC) + IF is_pickup_oconus AND NOT is_destination_oconus THEN + -- Shipment is OCONUS to CONUS, create PODFSC item + FOR service_item IN + SELECT rsi.id, + rs.id AS re_service_id, + rs.service_location, + rsi.is_auto_approved, + rsi.sort + FROM re_service_items rsi + JOIN re_services rs ON rsi.service_id = rs.id + WHERE rsi.shipment_type = s_type + AND rsi.market_code = m_code + AND rs.code = ''PODFSC'' + AND rsi.is_auto_approved = true + LOOP + BEGIN + IF NOT does_service_item_exist(service_item.re_service_id, shipment_id) THEN + INSERT INTO temp_mto_service_items ( + mto_shipment_id, + move_id, + re_service_id, + service_location, + status, + sort + ) + VALUES ( + shipment_id, + move_id, + service_item.re_service_id, + service_item.service_location, + ''APPROVED''::service_item_status, + service_item.sort + ); + END IF; + EXCEPTION + WHEN OTHERS THEN + RAISE EXCEPTION ''Error creating PODFSC service item for shipment %: %'', shipment_id, SQLERRM; + END; + END LOOP; + ELSIF NOT is_pickup_oconus AND is_destination_oconus THEN + -- Shipment is CONUS to OCONUS, create POEFSC item + FOR service_item IN + SELECT rsi.id, + rs.id AS re_service_id, + rs.service_location, + rsi.is_auto_approved, + rsi.sort + FROM re_service_items rsi + JOIN re_services rs ON rsi.service_id = rs.id + WHERE rsi.shipment_type = s_type + AND rsi.market_code = m_code + AND rs.code = ''POEFSC'' + AND rsi.is_auto_approved = true + LOOP + BEGIN + IF NOT does_service_item_exist(service_item.re_service_id, shipment_id) THEN + INSERT INTO temp_mto_service_items ( + mto_shipment_id, + move_id, + re_service_id, + service_location, + status, + sort + ) + VALUES ( + shipment_id, + move_id, + service_item.re_service_id, + service_item.service_location, + ''APPROVED''::service_item_status, + service_item.sort + ); + END IF; + EXCEPTION + WHEN OTHERS THEN + RAISE EXCEPTION ''Error creating POEFSC service item for shipment %: %'', shipment_id, SQLERRM; + END; + END LOOP; + END IF; + + -- create all other auto-approved service items, filtering out the POEFSC or PODFSC service items + FOR service_item IN + SELECT rsi.id, + rs.id AS re_service_id, + rs.service_location, + rsi.is_auto_approved, + rsi.sort + FROM re_service_items rsi + JOIN re_services rs ON rsi.service_id = rs.id + WHERE rsi.shipment_type = s_type + AND rsi.market_code = m_code + AND rsi.is_auto_approved = true + AND rs.code NOT IN (''POEFSC'', ''PODFSC'') + LOOP + BEGIN + IF NOT does_service_item_exist(service_item.re_service_id, shipment_id) THEN + INSERT INTO temp_mto_service_items ( + mto_shipment_id, + move_id, + re_service_id, + service_location, + status, + sort + ) + VALUES ( + shipment_id, + move_id, + service_item.re_service_id, + service_item.service_location, + ''APPROVED''::service_item_status, + service_item.sort + ); + End IF; + EXCEPTION + WHEN OTHERS THEN + RAISE EXCEPTION ''Error creating other service item for shipment %: %'', shipment_id, SQLERRM; + END; + END LOOP; + + -- Insert the mto_service_items in order + FOR service_item IN + SELECT tmsi.mto_shipment_id, + tmsi.move_id, + tmsi.re_service_id, + tmsi.service_location, + tmsi.status + FROM temp_mto_service_items tmsi + ORDER BY sort + LOOP + BEGIN + INSERT INTO mto_service_items ( + mto_shipment_id, + move_id, + re_service_id, + service_location, + status, + created_at, + updated_at, + approved_at + ) + VALUES ( + service_item.mto_shipment_id, + service_item.move_id, + service_item.re_service_id, + service_item.service_location, + service_item.status, + NOW(), + NOW(), + NOW() + ); + EXCEPTION + WHEN OTHERS THEN + RAISE EXCEPTION ''Error creating service items from temp table for shipment %: %'', shipment_id, SQLERRM; + END; + END LOOP; +END; +' +LANGUAGE plpgsql; diff --git a/pkg/services/mto_shipment/shipment_approver_test.go b/pkg/services/mto_shipment/shipment_approver_test.go index 886aea6f3f9..cca893524d9 100644 --- a/pkg/services/mto_shipment/shipment_approver_test.go +++ b/pkg/services/mto_shipment/shipment_approver_test.go @@ -836,15 +836,14 @@ func (suite *MTOShipmentServiceSuite) TestApproveShipment() { expectedReserviceCodes := []models.ReServiceCode{ models.ReServiceCodeUBP, + models.ReServiceCodePOEFSC, models.ReServiceCodeIUBPK, models.ReServiceCodeIUBUPK, - models.ReServiceCodePOEFSC, } suite.Equal(4, len(serviceItems)) for i := 0; i < len(serviceItems); i++ { - actualReServiceCode := serviceItems[i].ReService.Code - suite.True(slices.Contains(expectedReserviceCodes, actualReServiceCode), "Contains unexpected: "+actualReServiceCode.String()) + suite.Equal(expectedReserviceCodes[i], serviceItems[i].ReService.Code) } }) @@ -896,15 +895,14 @@ func (suite *MTOShipmentServiceSuite) TestApproveShipment() { expectedReserviceCodes := []models.ReServiceCode{ models.ReServiceCodeUBP, + models.ReServiceCodePODFSC, models.ReServiceCodeIUBPK, models.ReServiceCodeIUBUPK, - models.ReServiceCodePODFSC, } suite.Equal(4, len(serviceItems)) for i := 0; i < len(serviceItems); i++ { - actualReServiceCode := serviceItems[i].ReService.Code - suite.True(slices.Contains(expectedReserviceCodes, actualReServiceCode), "Contains unexpected: "+actualReServiceCode.String()) + suite.Equal(expectedReserviceCodes[i], serviceItems[i].ReService.Code) } }) @@ -962,8 +960,7 @@ func (suite *MTOShipmentServiceSuite) TestApproveShipment() { suite.Equal(3, len(serviceItems)) for i := 0; i < len(serviceItems); i++ { - actualReServiceCode := serviceItems[i].ReService.Code - suite.True(slices.Contains(expectedReserviceCodes, actualReServiceCode), "Contains unexpected: "+actualReServiceCode.String()) + suite.Equal(expectedReserviceCodes[i], serviceItems[i].ReService.Code) } }) From 1c962448ecb08d39cda3a2b643acc31bec467b8b Mon Sep 17 00:00:00 2001 From: Steven Gleason <180579696+stevengleason-caci@users.noreply.github.com> Date: Mon, 13 Jan 2025 19:47:01 +0000 Subject: [PATCH 07/14] B-21569 rename UBP to "International UB price", from just "International UB". --- migrations/app/migrations_manifest.txt | 1 + .../schema/20250113152050_rename_ubp.up.sql | 1 + pkg/models/re_service.go | 2 +- .../mto_shipment/shipment_approver_test.go | 32 +++++++++++++++---- .../ShipmentServiceItemsTable.test.jsx | 8 ++--- 5 files changed, 33 insertions(+), 11 deletions(-) create mode 100644 migrations/app/schema/20250113152050_rename_ubp.up.sql diff --git a/migrations/app/migrations_manifest.txt b/migrations/app/migrations_manifest.txt index 221f6b2cffd..0d5a220354b 100644 --- a/migrations/app/migrations_manifest.txt +++ b/migrations/app/migrations_manifest.txt @@ -1061,3 +1061,4 @@ 20241230190638_remove_AK_zips_from_zip3.up.sql 20241230190647_add_missing_AK_zips_to_zip3_distances.up.sql 20250110160244_update_ordering_service_items.up.sql +20250113152050_rename_ubp.up.sql diff --git a/migrations/app/schema/20250113152050_rename_ubp.up.sql b/migrations/app/schema/20250113152050_rename_ubp.up.sql new file mode 100644 index 00000000000..41a5f532193 --- /dev/null +++ b/migrations/app/schema/20250113152050_rename_ubp.up.sql @@ -0,0 +1 @@ +update re_services set name = 'International UB price' where code = 'UBP'; \ No newline at end of file diff --git a/pkg/models/re_service.go b/pkg/models/re_service.go index 5fc9d9b3e75..75d62317f1c 100644 --- a/pkg/models/re_service.go +++ b/pkg/models/re_service.go @@ -119,7 +119,7 @@ const ( ReServiceCodeNSTH ReServiceCode = "NSTH" // ReServiceCodeNSTUB Nonstandard UB ReServiceCodeNSTUB ReServiceCode = "NSTUB" - // ReServiceCodeUBP International UB + // ReServiceCodeUBP International UB price ReServiceCodeUBP ReServiceCode = "UBP" // ReServiceCodeISLH Shipping & Linehaul ReServiceCodeISLH ReServiceCode = "ISLH" diff --git a/pkg/services/mto_shipment/shipment_approver_test.go b/pkg/services/mto_shipment/shipment_approver_test.go index cca893524d9..aef6158ddc1 100644 --- a/pkg/services/mto_shipment/shipment_approver_test.go +++ b/pkg/services/mto_shipment/shipment_approver_test.go @@ -834,16 +834,23 @@ func (suite *MTOShipmentServiceSuite) TestApproveShipment() { err2 := suite.AppContextForTest().DB().EagerPreload("ReService").Where("mto_shipment_id = ?", internationalShipment.ID).Order("created_at asc").All(&serviceItems) suite.NoError(err2) - expectedReserviceCodes := []models.ReServiceCode{ + expectedReServiceCodes := []models.ReServiceCode{ models.ReServiceCodeUBP, models.ReServiceCodePOEFSC, models.ReServiceCodeIUBPK, models.ReServiceCodeIUBUPK, } + expectedReServiceNames := []string{ + "International UB price", + "International POE Fuel Surcharge", + "International UB pack", + "International UB unpack", + } suite.Equal(4, len(serviceItems)) for i := 0; i < len(serviceItems); i++ { - suite.Equal(expectedReserviceCodes[i], serviceItems[i].ReService.Code) + suite.Equal(expectedReServiceCodes[i], serviceItems[i].ReService.Code) + suite.Equal(expectedReServiceNames[i], serviceItems[i].ReService.Name) } }) @@ -893,16 +900,23 @@ func (suite *MTOShipmentServiceSuite) TestApproveShipment() { err2 := suite.AppContextForTest().DB().EagerPreload("ReService").Where("mto_shipment_id = ?", internationalShipment.ID).Order("created_at asc").All(&serviceItems) suite.NoError(err2) - expectedReserviceCodes := []models.ReServiceCode{ + expectedReServiceCodes := []models.ReServiceCode{ models.ReServiceCodeUBP, models.ReServiceCodePODFSC, models.ReServiceCodeIUBPK, models.ReServiceCodeIUBUPK, } + expectedReServiceNames := []string{ + "International UB price", + "International POD Fuel Surcharge", + "International UB pack", + "International UB unpack", + } suite.Equal(4, len(serviceItems)) for i := 0; i < len(serviceItems); i++ { - suite.Equal(expectedReserviceCodes[i], serviceItems[i].ReService.Code) + suite.Equal(expectedReServiceCodes[i], serviceItems[i].ReService.Code) + suite.Equal(expectedReServiceNames[i], serviceItems[i].ReService.Name) } }) @@ -952,15 +966,21 @@ func (suite *MTOShipmentServiceSuite) TestApproveShipment() { err2 := suite.AppContextForTest().DB().EagerPreload("ReService").Where("mto_shipment_id = ?", internationalShipment.ID).Order("created_at asc").All(&serviceItems) suite.NoError(err2) - expectedReserviceCodes := []models.ReServiceCode{ + expectedReServiceCodes := []models.ReServiceCode{ models.ReServiceCodeUBP, models.ReServiceCodeIUBPK, models.ReServiceCodeIUBUPK, } + expectedReServiceNames := []string{ + "International UB price", + "International UB pack", + "International UB unpack", + } suite.Equal(3, len(serviceItems)) for i := 0; i < len(serviceItems); i++ { - suite.Equal(expectedReserviceCodes[i], serviceItems[i].ReService.Code) + suite.Equal(expectedReServiceCodes[i], serviceItems[i].ReService.Code) + suite.Equal(expectedReServiceNames[i], serviceItems[i].ReService.Name) } }) diff --git a/src/components/Office/ShipmentServiceItemsTable/ShipmentServiceItemsTable.test.jsx b/src/components/Office/ShipmentServiceItemsTable/ShipmentServiceItemsTable.test.jsx index 9b18298293b..592ae52467a 100644 --- a/src/components/Office/ShipmentServiceItemsTable/ShipmentServiceItemsTable.test.jsx +++ b/src/components/Office/ShipmentServiceItemsTable/ShipmentServiceItemsTable.test.jsx @@ -25,7 +25,7 @@ const reServiceItemResponse = [ isAutoApproved: true, marketCode: 'i', serviceCode: 'UBP', - serviceName: 'International UB', + serviceName: 'International UB price', shipmentType: 'UNACCOMPANIED_BAGGAGE', }, { @@ -370,7 +370,7 @@ describe('Shipment Service Items Table', () => { describe('renders the intl UB shipment type (CONUS -> OCONUS) with service items', () => { it.each([ - ['International UB'], + ['International UB price'], ['International POE Fuel Surcharge'], ['International UB pack'], ['International UB unpack'], @@ -385,7 +385,7 @@ describe('Shipment Service Items Table', () => { describe('renders the intl UB shipment type (OCONUS -> CONUS) with service items', () => { it.each([ - ['International UB'], + ['International UB price'], ['International POD Fuel Surcharge'], ['International UB pack'], ['International UB unpack'], @@ -399,7 +399,7 @@ describe('Shipment Service Items Table', () => { }); describe('renders the intl UB shipment type (OCONUS -> OCONUS) with service items', () => { - it.each([['International UB'], ['International UB pack'], ['International UB unpack']])( + it.each([['International UB price'], ['International UB pack'], ['International UB unpack']])( 'expects %s to be in the document', async (serviceItem) => { render(); From ea981ca98a005ba54e89b234e398ac650a4c3d91 Mon Sep 17 00:00:00 2001 From: Steven Gleason <180579696+stevengleason-caci@users.noreply.github.com> Date: Thu, 16 Jan 2025 16:04:29 +0000 Subject: [PATCH 08/14] B-21569 move the service item sorting from the insert proc to the UI. Include the sort field in listMTOServiceItems operation. Update the sorting logic in ServiceItemsTable.jsx. Remove my changes to the stored procedure. Move testing from shipment_approver to the service and UI tests. --- migrations/app/migrations_manifest.txt | 1 - ...60244_update_ordering_service_items.up.sql | 203 ------------------ pkg/gen/ghcapi/embedded_spec.go | 10 + pkg/gen/ghcmessages/m_t_o_service_item.go | 3 + .../internal/payloads/model_to_payload.go | 5 + pkg/handlers/ghcapi/mto_service_items.go | 1 + pkg/handlers/ghcapi/mto_service_items_test.go | 32 ++- pkg/models/re_service.go | 1 + .../mto_shipment/shipment_approver_test.go | 18 +- .../RequestedServiceItemsTable.test.jsx | 41 ++++ .../ServiceItemsTable/ServiceItemsTable.jsx | 14 +- swagger-def/definitions/MTOServiceItem.yaml | 4 + swagger/ghc.yaml | 6 + 13 files changed, 121 insertions(+), 218 deletions(-) delete mode 100644 migrations/app/schema/20250110160244_update_ordering_service_items.up.sql diff --git a/migrations/app/migrations_manifest.txt b/migrations/app/migrations_manifest.txt index 0d5a220354b..fc7c590f4e7 100644 --- a/migrations/app/migrations_manifest.txt +++ b/migrations/app/migrations_manifest.txt @@ -1060,5 +1060,4 @@ 20241227153723_remove_empty_string_emplid_values.up.sql 20241230190638_remove_AK_zips_from_zip3.up.sql 20241230190647_add_missing_AK_zips_to_zip3_distances.up.sql -20250110160244_update_ordering_service_items.up.sql 20250113152050_rename_ubp.up.sql diff --git a/migrations/app/schema/20250110160244_update_ordering_service_items.up.sql b/migrations/app/schema/20250110160244_update_ordering_service_items.up.sql deleted file mode 100644 index 7b5196b9272..00000000000 --- a/migrations/app/schema/20250110160244_update_ordering_service_items.up.sql +++ /dev/null @@ -1,203 +0,0 @@ -CREATE OR REPLACE PROCEDURE create_approved_service_items_for_shipment( - IN shipment_id UUID -) -AS ' -DECLARE - s_status mto_shipment_status; - s_type mto_shipment_type; - m_code market_code_enum; - move_id UUID; - pickup_address_id UUID; - destination_address_id UUID; - is_pickup_oconus BOOLEAN; - is_destination_oconus BOOLEAN; - service_item RECORD; -BEGIN - -- get shipment type, market code, move_id, and address IDs based on shipment_id - SELECT ms.shipment_type, ms.market_code, ms.move_id, ms.pickup_address_id, ms.destination_address_id, ms.status - INTO s_type, m_code, move_id, pickup_address_id, destination_address_id, s_status - FROM mto_shipments ms - WHERE ms.id = shipment_id; - - IF s_type IS NULL OR m_code IS NULL THEN - RAISE EXCEPTION ''Shipment with ID % not found or missing required details.'', shipment_id; - END IF; - - IF s_status IN (''APPROVED'') THEN - RAISE EXCEPTION ''Shipment with ID % is already in APPROVED status'', shipment_id; - END IF; - - -- get the is_oconus values for both pickup and destination addresses - this determines POD/POE creation - is_pickup_oconus := get_is_oconus(pickup_address_id); - is_destination_oconus := get_is_oconus(destination_address_id); - - -- determine which service item to create based on shipment direction - -- collect the service items into a temporary table for sorting - CREATE TEMPORARY TABLE temp_mto_service_items ( - mto_shipment_id uuid, - move_id uuid, - re_service_id uuid, - service_location service_location_enum, - status service_item_status, - sort text - ) ON COMMIT DROP; - - -- first create the direction-specific service item (POEFSC or PODFSC) - IF is_pickup_oconus AND NOT is_destination_oconus THEN - -- Shipment is OCONUS to CONUS, create PODFSC item - FOR service_item IN - SELECT rsi.id, - rs.id AS re_service_id, - rs.service_location, - rsi.is_auto_approved, - rsi.sort - FROM re_service_items rsi - JOIN re_services rs ON rsi.service_id = rs.id - WHERE rsi.shipment_type = s_type - AND rsi.market_code = m_code - AND rs.code = ''PODFSC'' - AND rsi.is_auto_approved = true - LOOP - BEGIN - IF NOT does_service_item_exist(service_item.re_service_id, shipment_id) THEN - INSERT INTO temp_mto_service_items ( - mto_shipment_id, - move_id, - re_service_id, - service_location, - status, - sort - ) - VALUES ( - shipment_id, - move_id, - service_item.re_service_id, - service_item.service_location, - ''APPROVED''::service_item_status, - service_item.sort - ); - END IF; - EXCEPTION - WHEN OTHERS THEN - RAISE EXCEPTION ''Error creating PODFSC service item for shipment %: %'', shipment_id, SQLERRM; - END; - END LOOP; - ELSIF NOT is_pickup_oconus AND is_destination_oconus THEN - -- Shipment is CONUS to OCONUS, create POEFSC item - FOR service_item IN - SELECT rsi.id, - rs.id AS re_service_id, - rs.service_location, - rsi.is_auto_approved, - rsi.sort - FROM re_service_items rsi - JOIN re_services rs ON rsi.service_id = rs.id - WHERE rsi.shipment_type = s_type - AND rsi.market_code = m_code - AND rs.code = ''POEFSC'' - AND rsi.is_auto_approved = true - LOOP - BEGIN - IF NOT does_service_item_exist(service_item.re_service_id, shipment_id) THEN - INSERT INTO temp_mto_service_items ( - mto_shipment_id, - move_id, - re_service_id, - service_location, - status, - sort - ) - VALUES ( - shipment_id, - move_id, - service_item.re_service_id, - service_item.service_location, - ''APPROVED''::service_item_status, - service_item.sort - ); - END IF; - EXCEPTION - WHEN OTHERS THEN - RAISE EXCEPTION ''Error creating POEFSC service item for shipment %: %'', shipment_id, SQLERRM; - END; - END LOOP; - END IF; - - -- create all other auto-approved service items, filtering out the POEFSC or PODFSC service items - FOR service_item IN - SELECT rsi.id, - rs.id AS re_service_id, - rs.service_location, - rsi.is_auto_approved, - rsi.sort - FROM re_service_items rsi - JOIN re_services rs ON rsi.service_id = rs.id - WHERE rsi.shipment_type = s_type - AND rsi.market_code = m_code - AND rsi.is_auto_approved = true - AND rs.code NOT IN (''POEFSC'', ''PODFSC'') - LOOP - BEGIN - IF NOT does_service_item_exist(service_item.re_service_id, shipment_id) THEN - INSERT INTO temp_mto_service_items ( - mto_shipment_id, - move_id, - re_service_id, - service_location, - status, - sort - ) - VALUES ( - shipment_id, - move_id, - service_item.re_service_id, - service_item.service_location, - ''APPROVED''::service_item_status, - service_item.sort - ); - End IF; - EXCEPTION - WHEN OTHERS THEN - RAISE EXCEPTION ''Error creating other service item for shipment %: %'', shipment_id, SQLERRM; - END; - END LOOP; - - -- Insert the mto_service_items in order - FOR service_item IN - SELECT tmsi.mto_shipment_id, - tmsi.move_id, - tmsi.re_service_id, - tmsi.service_location, - tmsi.status - FROM temp_mto_service_items tmsi - ORDER BY sort - LOOP - BEGIN - INSERT INTO mto_service_items ( - mto_shipment_id, - move_id, - re_service_id, - service_location, - status, - created_at, - updated_at, - approved_at - ) - VALUES ( - service_item.mto_shipment_id, - service_item.move_id, - service_item.re_service_id, - service_item.service_location, - service_item.status, - NOW(), - NOW(), - NOW() - ); - EXCEPTION - WHEN OTHERS THEN - RAISE EXCEPTION ''Error creating service items from temp table for shipment %: %'', shipment_id, SQLERRM; - END; - END LOOP; -END; -' -LANGUAGE plpgsql; diff --git a/pkg/gen/ghcapi/embedded_spec.go b/pkg/gen/ghcapi/embedded_spec.go index 361f666c222..acd64a9aad1 100644 --- a/pkg/gen/ghcapi/embedded_spec.go +++ b/pkg/gen/ghcapi/embedded_spec.go @@ -9300,6 +9300,11 @@ func init() { "format": "date", "x-nullable": true }, + "sort": { + "description": "Sort order for service items to be displayed for a given shipment type.", + "type": "string", + "x-nullable": true + }, "standaloneCrate": { "type": "boolean", "x-nullable": true @@ -26044,6 +26049,11 @@ func init() { "format": "date", "x-nullable": true }, + "sort": { + "description": "Sort order for service items to be displayed for a given shipment type.", + "type": "string", + "x-nullable": true + }, "standaloneCrate": { "type": "boolean", "x-nullable": true diff --git a/pkg/gen/ghcmessages/m_t_o_service_item.go b/pkg/gen/ghcmessages/m_t_o_service_item.go index 9f20d0d5a7b..7498cf4674f 100644 --- a/pkg/gen/ghcmessages/m_t_o_service_item.go +++ b/pkg/gen/ghcmessages/m_t_o_service_item.go @@ -161,6 +161,9 @@ type MTOServiceItem struct { // Format: date SitRequestedDelivery *strfmt.Date `json:"sitRequestedDelivery,omitempty"` + // Sort order for service items to be displayed for a given shipment type. + Sort *string `json:"sort,omitempty"` + // standalone crate StandaloneCrate *bool `json:"standaloneCrate,omitempty"` diff --git a/pkg/handlers/ghcapi/internal/payloads/model_to_payload.go b/pkg/handlers/ghcapi/internal/payloads/model_to_payload.go index caa4aef11d5..bff1ea835c8 100644 --- a/pkg/handlers/ghcapi/internal/payloads/model_to_payload.go +++ b/pkg/handlers/ghcapi/internal/payloads/model_to_payload.go @@ -1863,6 +1863,10 @@ func MTOServiceItemModel(s *models.MTOServiceItem, storer storage.FileStorer) *g serviceRequestDocs[i] = payload } } + var sort *string = nil + if s.ReService.ReServiceItem != nil { + sort = s.ReService.ReServiceItem.Sort + } payload := &ghcmessages.MTOServiceItem{ ID: handlers.FmtUUID(s.ID), MoveTaskOrderID: handlers.FmtUUID(s.MoveTaskOrderID), @@ -1878,6 +1882,7 @@ func MTOServiceItemModel(s *models.MTOServiceItem, storer storage.FileStorer) *g SitDepartureDate: handlers.FmtDateTimePtr(s.SITDepartureDate), SitCustomerContacted: handlers.FmtDatePtr(s.SITCustomerContacted), SitRequestedDelivery: handlers.FmtDatePtr(s.SITRequestedDelivery), + Sort: sort, Status: ghcmessages.MTOServiceItemStatus(s.Status), Description: handlers.FmtStringPtr(s.Description), Dimensions: MTOServiceItemDimensions(s.Dimensions), diff --git a/pkg/handlers/ghcapi/mto_service_items.go b/pkg/handlers/ghcapi/mto_service_items.go index 60b8db84c04..7e7bddaee8d 100644 --- a/pkg/handlers/ghcapi/mto_service_items.go +++ b/pkg/handlers/ghcapi/mto_service_items.go @@ -350,6 +350,7 @@ func (h ListMTOServiceItemsHandler) Handle(params mtoserviceitemop.ListMTOServic query.NewQueryAssociation("SITDestinationFinalAddress"), query.NewQueryAssociation("SITOriginHHGOriginalAddress"), query.NewQueryAssociation("SITOriginHHGActualAddress"), + query.NewQueryAssociation("ReService.ReServiceItem.Sort"), }) var serviceItems models.MTOServiceItems diff --git a/pkg/handlers/ghcapi/mto_service_items_test.go b/pkg/handlers/ghcapi/mto_service_items_test.go index b1ff7e9c405..18f605121c7 100644 --- a/pkg/handlers/ghcapi/mto_service_items_test.go +++ b/pkg/handlers/ghcapi/mto_service_items_test.go @@ -129,7 +129,28 @@ func (suite *HandlerSuite) TestListMTOServiceItemHandler() { }, }, nil) - serviceItems := models.MTOServiceItems{serviceItem, originSit, destinationSit} + poeFsc := factory.BuildMTOServiceItem(suite.DB(), []factory.Customization{ + { + Model: models.MTOServiceItem{ + Status: models.MTOServiceItemStatusApproved, + }, + }, + { + Model: mto, + LinkOnly: true, + }, + { + Model: mtoShipment, + LinkOnly: true, + }, + { + Model: models.ReService{ + Code: models.ReServiceCodePOEFSC, + }, + }, + }, nil) + + serviceItems := models.MTOServiceItems{serviceItem, originSit, destinationSit, poeFsc} return requestUser, serviceItems } @@ -178,7 +199,7 @@ func (suite *HandlerSuite) TestListMTOServiceItemHandler() { suite.NoError(okResponse.Payload.Validate(strfmt.Default)) fmt.Println(okResponse.Payload) - suite.Len(okResponse.Payload, 3) + suite.Len(okResponse.Payload, 4) for _, serviceItem := range serviceItems { for _, payload := range okResponse.Payload { // Validate that the Customer Contacts were included in the payload @@ -200,6 +221,13 @@ func (suite *HandlerSuite) TestListMTOServiceItemHandler() { } } } + + // Validate that sort field is populated for service items which have it (ie. POEFSC) + for _, payload := range okResponse.Payload { + if payload.ReServiceCode != nil && *payload.ReServiceCode == models.ReServiceCodePOEFSC.String() { + suite.Equal("2", *payload.Sort) + } + } }) suite.Run("Failure list fetch - Internal Server Error", func() { diff --git a/pkg/models/re_service.go b/pkg/models/re_service.go index 75d62317f1c..91cfbcfe82c 100644 --- a/pkg/models/re_service.go +++ b/pkg/models/re_service.go @@ -147,6 +147,7 @@ type ReService struct { Priority int `db:"priority" rw:"r"` Name string `json:"name" db:"name" rw:"r"` ServiceLocation *ServiceLocationType `db:"service_location" rw:"r"` + ReServiceItem *ReServiceItem `has_one:"re_service_item" fk_id:"service_id"` CreatedAt time.Time `json:"created_at" db:"created_at" rw:"r"` UpdatedAt time.Time `json:"updated_at" db:"updated_at" rw:"r"` } diff --git a/pkg/services/mto_shipment/shipment_approver_test.go b/pkg/services/mto_shipment/shipment_approver_test.go index aef6158ddc1..b2b1c062f83 100644 --- a/pkg/services/mto_shipment/shipment_approver_test.go +++ b/pkg/services/mto_shipment/shipment_approver_test.go @@ -849,8 +849,10 @@ func (suite *MTOShipmentServiceSuite) TestApproveShipment() { suite.Equal(4, len(serviceItems)) for i := 0; i < len(serviceItems); i++ { - suite.Equal(expectedReServiceCodes[i], serviceItems[i].ReService.Code) - suite.Equal(expectedReServiceNames[i], serviceItems[i].ReService.Name) + actualReServiceCode := serviceItems[i].ReService.Code + actualReServiceName := serviceItems[i].ReService.Name + suite.True(slices.Contains(expectedReServiceCodes, actualReServiceCode), "Contains unexpected code: "+actualReServiceCode.String()) + suite.True(slices.Contains(expectedReServiceNames, actualReServiceName), "Contains unexpected name: "+actualReServiceName) } }) @@ -915,8 +917,10 @@ func (suite *MTOShipmentServiceSuite) TestApproveShipment() { suite.Equal(4, len(serviceItems)) for i := 0; i < len(serviceItems); i++ { - suite.Equal(expectedReServiceCodes[i], serviceItems[i].ReService.Code) - suite.Equal(expectedReServiceNames[i], serviceItems[i].ReService.Name) + actualReServiceCode := serviceItems[i].ReService.Code + actualReServiceName := serviceItems[i].ReService.Name + suite.True(slices.Contains(expectedReServiceCodes, actualReServiceCode), "Contains unexpected code: "+actualReServiceCode.String()) + suite.True(slices.Contains(expectedReServiceNames, actualReServiceName), "Contains unexpected name: "+actualReServiceName) } }) @@ -979,8 +983,10 @@ func (suite *MTOShipmentServiceSuite) TestApproveShipment() { suite.Equal(3, len(serviceItems)) for i := 0; i < len(serviceItems); i++ { - suite.Equal(expectedReServiceCodes[i], serviceItems[i].ReService.Code) - suite.Equal(expectedReServiceNames[i], serviceItems[i].ReService.Name) + actualReServiceCode := serviceItems[i].ReService.Code + actualReServiceName := serviceItems[i].ReService.Name + suite.True(slices.Contains(expectedReServiceCodes, actualReServiceCode), "Contains unexpected code: "+actualReServiceCode.String()) + suite.True(slices.Contains(expectedReServiceNames, actualReServiceName), "Contains unexpected name: "+actualReServiceName) } }) diff --git a/src/components/Office/RequestedServiceItemsTable/RequestedServiceItemsTable.test.jsx b/src/components/Office/RequestedServiceItemsTable/RequestedServiceItemsTable.test.jsx index b18f4ef24ac..b4e2c83fd33 100644 --- a/src/components/Office/RequestedServiceItemsTable/RequestedServiceItemsTable.test.jsx +++ b/src/components/Office/RequestedServiceItemsTable/RequestedServiceItemsTable.test.jsx @@ -61,6 +61,30 @@ const serviceItemWithDetails = { }, }; +const serviceItemUBP = { + id: 'ubp123', + createdAt: '2025-01-15', + serviceItem: 'International UB price', + code: 'UBP', + sort: '1', +}; + +const serviceItemIUBPK = { + id: 'iubpk123', + createdAt: '2025-01-15', + serviceItem: 'International UB pack', + code: 'IUBPK', + sort: '3', +}; + +const serviceItemIUBUPK = { + id: 'iubupk123', + createdAt: '2025-01-15', + serviceItem: 'International UB unpack', + code: 'IUBUPK', + sort: '4', +}; + const testDetails = (wrapper) => { const detailTypes = wrapper.find('.detailType'); const detailDefinitions = wrapper.find('.detail dd'); @@ -226,4 +250,21 @@ describe('RequestedServiceItemsTable', () => { expect(approveTextButton.at(1).text().includes('Approve')).toBe(true); expect(approveTextButton.at(2).text().includes('Approve')).toBe(true); }); + + it('displays sorted service items in order', () => { + const serviceItems = [serviceItemIUBPK, serviceItemUBP, serviceItemIUBUPK]; + const wrapper = mount( + + + , + ); + + expect(wrapper.find('.codeName').at(0).text()).toBe('International UB price'); + expect(wrapper.find('.codeName').at(1).text()).toBe('International UB pack'); + expect(wrapper.find('.codeName').at(2).text()).toBe('International UB unpack'); + }); }); diff --git a/src/components/Office/ServiceItemsTable/ServiceItemsTable.jsx b/src/components/Office/ServiceItemsTable/ServiceItemsTable.jsx index 1b8aeb3383c..7ce407b887a 100644 --- a/src/components/Office/ServiceItemsTable/ServiceItemsTable.jsx +++ b/src/components/Office/ServiceItemsTable/ServiceItemsTable.jsx @@ -48,12 +48,14 @@ function sortServiceItems(items) { ); // Filter all service items that are not specifically sorted - const remainingServiceItems = items.filter( - (item) => - !haulTypeServiceItemCodes.includes(item.code) && - !destinationServiceItemCodes.includes(item.code) && - !originServiceItemCodes.includes(item.code), - ); + const remainingServiceItems = items + .filter( + (item) => + !haulTypeServiceItemCodes.includes(item.code) && + !destinationServiceItemCodes.includes(item.code) && + !originServiceItemCodes.includes(item.code), + ) + .sort((a, b) => a.sort.localeCompare(b.sort)); return [ ...sortedHaulTypeServiceItems, diff --git a/swagger-def/definitions/MTOServiceItem.yaml b/swagger-def/definitions/MTOServiceItem.yaml index 9cd6119df81..c71ec5568f6 100644 --- a/swagger-def/definitions/MTOServiceItem.yaml +++ b/swagger-def/definitions/MTOServiceItem.yaml @@ -153,3 +153,7 @@ properties: example: CONUS description: 'To identify whether the service was provided within (CONUS) or (OCONUS)' x-nullable: true + sort: + type: string + description: 'Sort order for service items to be displayed for a given shipment type.' + x-nullable: true diff --git a/swagger/ghc.yaml b/swagger/ghc.yaml index ab568b434c8..a5254af620a 100644 --- a/swagger/ghc.yaml +++ b/swagger/ghc.yaml @@ -8763,6 +8763,12 @@ definitions: To identify whether the service was provided within (CONUS) or (OCONUS) x-nullable: true + sort: + type: string + description: >- + Sort order for service items to be displayed for a given shipment + type. + x-nullable: true MTOServiceItems: description: A list of service items connected to this shipment. type: array From f3c0599eed023798e495ad67a0f54d6a23ed1836 Mon Sep 17 00:00:00 2001 From: Steven Gleason <180579696+stevengleason-caci@users.noreply.github.com> Date: Thu, 16 Jan 2025 19:51:01 +0000 Subject: [PATCH 09/14] B-21569 fix null-safe issue with comparing the sort values, added test and ensure nulls are sorted After those with sort values. --- .../RequestedServiceItemsTable.test.jsx | 18 ++++++++++++++++++ .../ServiceItemsTable/ServiceItemsTable.jsx | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/components/Office/RequestedServiceItemsTable/RequestedServiceItemsTable.test.jsx b/src/components/Office/RequestedServiceItemsTable/RequestedServiceItemsTable.test.jsx index b4e2c83fd33..3e3a973a64c 100644 --- a/src/components/Office/RequestedServiceItemsTable/RequestedServiceItemsTable.test.jsx +++ b/src/components/Office/RequestedServiceItemsTable/RequestedServiceItemsTable.test.jsx @@ -267,4 +267,22 @@ describe('RequestedServiceItemsTable', () => { expect(wrapper.find('.codeName').at(1).text()).toBe('International UB pack'); expect(wrapper.find('.codeName').at(2).text()).toBe('International UB unpack'); }); + + it('displays sorted service items in order along with non-sorted service items', () => { + const serviceItems = [serviceItemIUBPK, serviceItemUBP, serviceItemWithCrating, serviceItemIUBUPK]; + const wrapper = mount( + + + , + ); + + expect(wrapper.find('.codeName').at(0).text()).toBe('International UB price'); + expect(wrapper.find('.codeName').at(1).text()).toBe('International UB pack'); + expect(wrapper.find('.codeName').at(2).text()).toBe('International UB unpack'); + expect(wrapper.find('.codeName').at(3).text()).toBe('Domestic crating'); + }); }); diff --git a/src/components/Office/ServiceItemsTable/ServiceItemsTable.jsx b/src/components/Office/ServiceItemsTable/ServiceItemsTable.jsx index 7ce407b887a..83069e90567 100644 --- a/src/components/Office/ServiceItemsTable/ServiceItemsTable.jsx +++ b/src/components/Office/ServiceItemsTable/ServiceItemsTable.jsx @@ -55,7 +55,7 @@ function sortServiceItems(items) { !destinationServiceItemCodes.includes(item.code) && !originServiceItemCodes.includes(item.code), ) - .sort((a, b) => a.sort.localeCompare(b.sort)); + .sort((a, b) => (a.sort || 'z').localeCompare(b.sort || 'z')); return [ ...sortedHaulTypeServiceItems, From 1b59d9dac6e0a89d1bd3aa1970a404f864c7eb3b Mon Sep 17 00:00:00 2001 From: Steven Gleason <180579696+stevengleason-caci@users.noreply.github.com> Date: Thu, 16 Jan 2025 23:02:31 +0000 Subject: [PATCH 10/14] B-21569 nullSafeStringCompare function in utils.js. --- .../ServiceItemsTable/ServiceItemsTable.jsx | 3 +- src/shared/utils.js | 21 ++++++++ src/shared/utils.test.js | 54 +++++++++++++++++++ 3 files changed, 77 insertions(+), 1 deletion(-) diff --git a/src/components/Office/ServiceItemsTable/ServiceItemsTable.jsx b/src/components/Office/ServiceItemsTable/ServiceItemsTable.jsx index 83069e90567..7213e0345d1 100644 --- a/src/components/Office/ServiceItemsTable/ServiceItemsTable.jsx +++ b/src/components/Office/ServiceItemsTable/ServiceItemsTable.jsx @@ -19,6 +19,7 @@ import { selectDateFieldByStatus, selectDatePrefixByStatus } from 'utils/dates'; import { useGHCGetMoveHistory, useMovePaymentRequestsQueries } from 'hooks/queries'; import ToolTip from 'shared/ToolTip/ToolTip'; import { ShipmentShape } from 'types'; +import { nullSafeStringCompare } from 'shared/utils'; // Sorts service items in an order preferred by the customer // Currently only SIT & shorthaul/linehaul receives special sorting @@ -55,7 +56,7 @@ function sortServiceItems(items) { !destinationServiceItemCodes.includes(item.code) && !originServiceItemCodes.includes(item.code), ) - .sort((a, b) => (a.sort || 'z').localeCompare(b.sort || 'z')); + .sort((a, b) => nullSafeStringCompare(a.sort, b.sort)); return [ ...sortedHaulTypeServiceItems, diff --git a/src/shared/utils.js b/src/shared/utils.js index ac6baa1307b..314631ec343 100644 --- a/src/shared/utils.js +++ b/src/shared/utils.js @@ -171,3 +171,24 @@ export function isEmpty(obj) { export function isNullUndefinedOrWhitespace(value) { return value == null || value === undefined || value.trim() === ''; } + +/** + * Compare strings. Null, undefined, and blanks are after other values. + * @returns -1, 0, 1 + */ +export function nullSafeStringCompare(a, b) { + const A_BEFORE = -1; + const A_AFTER = 1; + const SAME = 0; + + if (isNullUndefinedOrWhitespace(a) && isNullUndefinedOrWhitespace(b)) { + return SAME; + } + if (isNullUndefinedOrWhitespace(a)) { + return A_AFTER; + } + if (isNullUndefinedOrWhitespace(b)) { + return A_BEFORE; + } + return a.localeCompare(b); +} diff --git a/src/shared/utils.test.js b/src/shared/utils.test.js index eb5ea3e0160..6f174bfb4d8 100644 --- a/src/shared/utils.test.js +++ b/src/shared/utils.test.js @@ -86,4 +86,58 @@ describe('utils', () => { }); }); }); + describe('nullSafeComparison', () => { + const A_BEFORE = -1; + const A_AFTER = 1; + const SAME = 0; + it('same value', () => { + const res = utils.nullSafeStringCompare('1', '1'); + expect(res).toEqual(SAME); + }); + it('greater than', () => { + const res = utils.nullSafeStringCompare('2', '1'); + expect(res).toEqual(A_AFTER); + }); + it('less than', () => { + const res = utils.nullSafeStringCompare('1', '2'); + expect(res).toEqual(A_BEFORE); + }); + it('both null', () => { + const res = utils.nullSafeStringCompare(null, null); + expect(res).toEqual(SAME); + }); + it('null and value', () => { + const res = utils.nullSafeStringCompare(null, '1'); + expect(res).toEqual(A_AFTER); + }); + it('value and null', () => { + const res = utils.nullSafeStringCompare('1', null); + expect(res).toEqual(A_BEFORE); + }); + it('both undefined', () => { + var udefA, udefB; + const res = utils.nullSafeStringCompare(udefA, udefB); + expect(res).toEqual(SAME); + }); + it('undefined and null', () => { + var udefA; + const res = utils.nullSafeStringCompare(udefA, null); + expect(res).toEqual(SAME); + }); + it('null and undefined', () => { + var udefB; + const res = utils.nullSafeStringCompare(null, udefB); + expect(res).toEqual(SAME); + }); + it('undefined and value', () => { + var udefA; + const res = utils.nullSafeStringCompare(udefA, '2'); + expect(res).toEqual(A_AFTER); + }); + it('value and undefined', () => { + var udefB; + const res = utils.nullSafeStringCompare('1', udefB); + expect(res).toEqual(A_BEFORE); + }); + }); }); From f7d0ccdff0cfe69168c0e04095c54179875b5df4 Mon Sep 17 00:00:00 2001 From: Steven Gleason <180579696+stevengleason-caci@users.noreply.github.com> Date: Wed, 22 Jan 2025 13:35:53 +0000 Subject: [PATCH 11/14] B-21569 Fix the 1-N relationship for ReService and ReServiceItem. Add logic in model_to_payload to match on ShipmentType and MarketCode. Update test to use matching shipment info. --- .../internal/payloads/model_to_payload.go | 9 +- pkg/handlers/ghcapi/mto_service_items.go | 6 +- pkg/handlers/ghcapi/mto_service_items_test.go | 103 +++++++++++++++--- pkg/models/re_service.go | 2 +- 4 files changed, 103 insertions(+), 17 deletions(-) diff --git a/pkg/handlers/ghcapi/internal/payloads/model_to_payload.go b/pkg/handlers/ghcapi/internal/payloads/model_to_payload.go index bff1ea835c8..0cb6654f1af 100644 --- a/pkg/handlers/ghcapi/internal/payloads/model_to_payload.go +++ b/pkg/handlers/ghcapi/internal/payloads/model_to_payload.go @@ -1864,8 +1864,13 @@ func MTOServiceItemModel(s *models.MTOServiceItem, storer storage.FileStorer) *g } } var sort *string = nil - if s.ReService.ReServiceItem != nil { - sort = s.ReService.ReServiceItem.Sort + if s.ReService.ReServiceItems != nil { + for _, reServiceItem := range *s.ReService.ReServiceItems { + if (s.MTOShipment.MarketCode == "" || s.MTOShipment.MarketCode == reServiceItem.MarketCode) && (s.MTOShipment.ShipmentType == "" || s.MTOShipment.ShipmentType == reServiceItem.ShipmentType) { + sort = reServiceItem.Sort + break + } + } } payload := &ghcmessages.MTOServiceItem{ ID: handlers.FmtUUID(s.ID), diff --git a/pkg/handlers/ghcapi/mto_service_items.go b/pkg/handlers/ghcapi/mto_service_items.go index 7e7bddaee8d..dab87016436 100644 --- a/pkg/handlers/ghcapi/mto_service_items.go +++ b/pkg/handlers/ghcapi/mto_service_items.go @@ -350,7 +350,11 @@ func (h ListMTOServiceItemsHandler) Handle(params mtoserviceitemop.ListMTOServic query.NewQueryAssociation("SITDestinationFinalAddress"), query.NewQueryAssociation("SITOriginHHGOriginalAddress"), query.NewQueryAssociation("SITOriginHHGActualAddress"), - query.NewQueryAssociation("ReService.ReServiceItem.Sort"), + query.NewQueryAssociation("ReService.ReServiceItems.Sort"), + query.NewQueryAssociation("ReService.ReServiceItems.MarketCode"), + query.NewQueryAssociation("ReService.ReServiceItems.ShipmentType"), + query.NewQueryAssociation("MTOShipment.MarketCode"), + query.NewQueryAssociation("MTOShipment.ShipmentType"), }) var serviceItems models.MTOServiceItems diff --git a/pkg/handlers/ghcapi/mto_service_items_test.go b/pkg/handlers/ghcapi/mto_service_items_test.go index 18f605121c7..e1746a62e79 100644 --- a/pkg/handlers/ghcapi/mto_service_items_test.go +++ b/pkg/handlers/ghcapi/mto_service_items_test.go @@ -38,14 +38,12 @@ import ( ) func (suite *HandlerSuite) TestListMTOServiceItemHandler() { - reServiceID, _ := uuid.NewV4() - serviceItemID, _ := uuid.NewV4() - mtoShipmentID, _ := uuid.NewV4() - var mtoID uuid.UUID - setupTestData := func() (models.User, models.MTOServiceItems) { + setupTestData := func() (models.User, models.MTOServiceItems, uuid.UUID) { + reServiceID, _ := uuid.NewV4() + serviceItemID, _ := uuid.NewV4() + mtoShipmentID, _ := uuid.NewV4() mto := factory.BuildMove(suite.DB(), nil, nil) - mtoID = mto.ID reService := factory.FetchReService(suite.DB(), []factory.Customization{ { Model: models.ReService{ @@ -129,6 +127,25 @@ func (suite *HandlerSuite) TestListMTOServiceItemHandler() { }, }, nil) + serviceItems := models.MTOServiceItems{serviceItem, originSit, destinationSit} + + return requestUser, serviceItems, mto.ID + } + + setupIUBTestData := func() (models.User, models.MTOServiceItems, uuid.UUID) { + mtoShipmentID, _ := uuid.NewV4() + mto := factory.BuildMove(suite.DB(), nil, nil) + mtoShipment := factory.BuildMTOShipment(suite.DB(), []factory.Customization{ + { + Model: models.MTOShipment{ + ID: mtoShipmentID, + ShipmentType: models.MTOShipmentTypeUnaccompaniedBaggage, + MarketCode: models.MarketCodeInternational, + }, + }, + }, nil) + requestUser := factory.BuildUser(nil, nil, nil) + poeFsc := factory.BuildMTOServiceItem(suite.DB(), []factory.Customization{ { Model: models.MTOServiceItem{ @@ -150,13 +167,34 @@ func (suite *HandlerSuite) TestListMTOServiceItemHandler() { }, }, nil) - serviceItems := models.MTOServiceItems{serviceItem, originSit, destinationSit, poeFsc} + ubp := factory.BuildMTOServiceItem(suite.DB(), []factory.Customization{ + { + Model: models.MTOServiceItem{ + Status: models.MTOServiceItemStatusApproved, + }, + }, + { + Model: mto, + LinkOnly: true, + }, + { + Model: mtoShipment, + LinkOnly: true, + }, + { + Model: models.ReService{ + Code: models.ReServiceCodeUBP, + }, + }, + }, nil) - return requestUser, serviceItems + serviceItems := models.MTOServiceItems{poeFsc, ubp} + + return requestUser, serviceItems, mto.ID } suite.Run("Successful list fetch - Integration Test", func() { - requestUser, serviceItems := setupTestData() + requestUser, serviceItems, mtoID := setupTestData() req := httptest.NewRequest("GET", fmt.Sprintf("/move_task_orders/%s/mto_service_items", mtoID.String()), nil) req = suite.AuthenticateUserRequest(req, requestUser) @@ -199,7 +237,7 @@ func (suite *HandlerSuite) TestListMTOServiceItemHandler() { suite.NoError(okResponse.Payload.Validate(strfmt.Default)) fmt.Println(okResponse.Payload) - suite.Len(okResponse.Payload, 4) + suite.Len(okResponse.Payload, 3) for _, serviceItem := range serviceItems { for _, payload := range okResponse.Payload { // Validate that the Customer Contacts were included in the payload @@ -221,17 +259,56 @@ func (suite *HandlerSuite) TestListMTOServiceItemHandler() { } } } + }) + + suite.Run("Successful sorted serviceItems for UB", func() { + requestUser, serviceItems, mtoID := setupIUBTestData() + req := httptest.NewRequest("GET", fmt.Sprintf("/move_task_orders/%s/mto_service_items", mtoID.String()), nil) + req = suite.AuthenticateUserRequest(req, requestUser) + + params := mtoserviceitemop.ListMTOServiceItemsParams{ + HTTPRequest: req, + MoveTaskOrderID: *handlers.FmtUUID(serviceItems[0].MoveTaskOrderID), + } - // Validate that sort field is populated for service items which have it (ie. POEFSC) + queryBuilder := query.NewQueryBuilder() + listFetcher := fetch.NewListFetcher(queryBuilder) + fetcher := fetch.NewFetcher(queryBuilder) + counselingPricer := ghcrateengine.NewCounselingServicesPricer() + moveManagementPricer := ghcrateengine.NewManagementServicesPricer() + handler := ListMTOServiceItemsHandler{ + suite.createS3HandlerConfig(), + listFetcher, + fetcher, + counselingPricer, + moveManagementPricer, + } + + // Validate incoming payload: no body to validate + + response := handler.Handle(params) + suite.IsType(&mtoserviceitemop.ListMTOServiceItemsOK{}, response) + okResponse := response.(*mtoserviceitemop.ListMTOServiceItemsOK) + + // Validate outgoing payload + suite.NoError(okResponse.Payload.Validate(strfmt.Default)) + fmt.Println(okResponse.Payload) + + suite.Len(okResponse.Payload, 2) + // Validate that sort field is populated for service items which have it. + // These test values can be updated to match any DB changes. for _, payload := range okResponse.Payload { if payload.ReServiceCode != nil && *payload.ReServiceCode == models.ReServiceCodePOEFSC.String() { suite.Equal("2", *payload.Sort) } + if payload.ReServiceCode != nil && *payload.ReServiceCode == models.ReServiceCodeUBP.String() { + suite.Equal("1", *payload.Sort) + } } }) suite.Run("Failure list fetch - Internal Server Error", func() { - requestUser, serviceItems := setupTestData() + requestUser, serviceItems, mtoID := setupTestData() req := httptest.NewRequest("GET", fmt.Sprintf("/move_task_orders/%s/mto_service_items", mtoID.String()), nil) req = suite.AuthenticateUserRequest(req, requestUser) @@ -279,7 +356,7 @@ func (suite *HandlerSuite) TestListMTOServiceItemHandler() { }) suite.Run("Failure list fetch - 404 Not Found - Move Task Order ID", func() { - requestUser, serviceItems := setupTestData() + requestUser, serviceItems, mtoID := setupTestData() req := httptest.NewRequest("GET", fmt.Sprintf("/move_task_orders/%s/mto_service_items", mtoID.String()), nil) req = suite.AuthenticateUserRequest(req, requestUser) diff --git a/pkg/models/re_service.go b/pkg/models/re_service.go index 91cfbcfe82c..bab371ac5d4 100644 --- a/pkg/models/re_service.go +++ b/pkg/models/re_service.go @@ -147,7 +147,7 @@ type ReService struct { Priority int `db:"priority" rw:"r"` Name string `json:"name" db:"name" rw:"r"` ServiceLocation *ServiceLocationType `db:"service_location" rw:"r"` - ReServiceItem *ReServiceItem `has_one:"re_service_item" fk_id:"service_id"` + ReServiceItems *ReServiceItems `has_many:"re_service_items" fk_id:"service_id"` CreatedAt time.Time `json:"created_at" db:"created_at" rw:"r"` UpdatedAt time.Time `json:"updated_at" db:"updated_at" rw:"r"` } From 24d6ebb72b37fb711639a4ad7dd4f7aceffbafe9 Mon Sep 17 00:00:00 2001 From: Steven Gleason <180579696+stevengleason-caci@users.noreply.github.com> Date: Wed, 22 Jan 2025 23:42:17 +0000 Subject: [PATCH 12/14] B-21569 cleanup: additional test in model_to_payload_test, no need to compare to blank strings because those are non-null DB enums, fix the multiple joins on the MTOServiceItem query, remove the unneeded UUID generation in mto_service_items_test. --- .../internal/payloads/model_to_payload.go | 2 +- .../payloads/model_to_payload_test.go | 45 +++++++++++++++++++ pkg/handlers/ghcapi/mto_service_items.go | 7 +-- pkg/handlers/ghcapi/mto_service_items_test.go | 16 +------ 4 files changed, 50 insertions(+), 20 deletions(-) diff --git a/pkg/handlers/ghcapi/internal/payloads/model_to_payload.go b/pkg/handlers/ghcapi/internal/payloads/model_to_payload.go index 0cb6654f1af..8cecc324744 100644 --- a/pkg/handlers/ghcapi/internal/payloads/model_to_payload.go +++ b/pkg/handlers/ghcapi/internal/payloads/model_to_payload.go @@ -1866,7 +1866,7 @@ func MTOServiceItemModel(s *models.MTOServiceItem, storer storage.FileStorer) *g var sort *string = nil if s.ReService.ReServiceItems != nil { for _, reServiceItem := range *s.ReService.ReServiceItems { - if (s.MTOShipment.MarketCode == "" || s.MTOShipment.MarketCode == reServiceItem.MarketCode) && (s.MTOShipment.ShipmentType == "" || s.MTOShipment.ShipmentType == reServiceItem.ShipmentType) { + if s.MTOShipment.MarketCode == reServiceItem.MarketCode && s.MTOShipment.ShipmentType == reServiceItem.ShipmentType { sort = reServiceItem.Sort break } 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 ec3072d2ca0..676f709c709 100644 --- a/pkg/handlers/ghcapi/internal/payloads/model_to_payload_test.go +++ b/pkg/handlers/ghcapi/internal/payloads/model_to_payload_test.go @@ -949,4 +949,49 @@ func (suite *PayloadsSuite) TestMTOServiceItemModel() { suite.NotNil(result, "Expected result to not be nil for valid MTOServiceItem") suite.Equal(handlers.FmtString(models.MarketOconus.FullString()), result.Market, "Expected Market to be OCONUS") }) + + suite.Run("sets Sort from correct serviceItem", func() { + reServiceID := uuid.Must(uuid.NewV4()) + + reServiceItems := make(models.ReServiceItems, 3) + mockReService := models.ReService{ + ID: reServiceID, + Code: models.ReServiceCodeUBP, + Name: "Test ReService", + ReServiceItems: &reServiceItems, + } + + mockMTOShipment := models.MTOShipment{ + ShipmentType: models.MTOShipmentTypeUnaccompaniedBaggage, + MarketCode: models.MarketCodeInternational, + } + + reServiceItems[0] = models.ReServiceItem{ + ReService: mockReService, + ShipmentType: models.MTOShipmentTypeHHG, + MarketCode: models.MarketCodeInternational, + Sort: models.StringPointer("0"), + } + reServiceItems[1] = models.ReServiceItem{ + ReService: mockReService, + ShipmentType: models.MTOShipmentTypeUnaccompaniedBaggage, + MarketCode: models.MarketCodeInternational, + Sort: models.StringPointer("1"), + } + reServiceItems[2] = models.ReServiceItem{ + ReService: mockReService, + ShipmentType: models.MTOShipmentTypeUnaccompaniedBaggage, + MarketCode: models.MarketCodeDomestic, + Sort: models.StringPointer("2"), + } + + mockMtoServiceItem := models.MTOServiceItem{ + ReService: mockReService, + MTOShipment: mockMTOShipment, + } + + result := MTOServiceItemModel(&mockMtoServiceItem, suite.storer) + suite.NotNil(result, "Expected result to not be nil for valid MTOServiceItem") + suite.Equal("1", *result.Sort, "Expected to get the Sort value by matching the correct ReServiceItem using ShipmentType and MarketCode.") + }) } diff --git a/pkg/handlers/ghcapi/mto_service_items.go b/pkg/handlers/ghcapi/mto_service_items.go index dab87016436..e5f5572580d 100644 --- a/pkg/handlers/ghcapi/mto_service_items.go +++ b/pkg/handlers/ghcapi/mto_service_items.go @@ -350,11 +350,8 @@ func (h ListMTOServiceItemsHandler) Handle(params mtoserviceitemop.ListMTOServic query.NewQueryAssociation("SITDestinationFinalAddress"), query.NewQueryAssociation("SITOriginHHGOriginalAddress"), query.NewQueryAssociation("SITOriginHHGActualAddress"), - query.NewQueryAssociation("ReService.ReServiceItems.Sort"), - query.NewQueryAssociation("ReService.ReServiceItems.MarketCode"), - query.NewQueryAssociation("ReService.ReServiceItems.ShipmentType"), - query.NewQueryAssociation("MTOShipment.MarketCode"), - query.NewQueryAssociation("MTOShipment.ShipmentType"), + query.NewQueryAssociation("ReService.ReServiceItems"), + query.NewQueryAssociation("MTOShipment"), }) var serviceItems models.MTOServiceItems diff --git a/pkg/handlers/ghcapi/mto_service_items_test.go b/pkg/handlers/ghcapi/mto_service_items_test.go index e1746a62e79..9e2c19fbb86 100644 --- a/pkg/handlers/ghcapi/mto_service_items_test.go +++ b/pkg/handlers/ghcapi/mto_service_items_test.go @@ -40,29 +40,19 @@ import ( func (suite *HandlerSuite) TestListMTOServiceItemHandler() { setupTestData := func() (models.User, models.MTOServiceItems, uuid.UUID) { - reServiceID, _ := uuid.NewV4() - serviceItemID, _ := uuid.NewV4() - mtoShipmentID, _ := uuid.NewV4() mto := factory.BuildMove(suite.DB(), nil, nil) reService := factory.FetchReService(suite.DB(), []factory.Customization{ { Model: models.ReService{ - ID: reServiceID, Code: "TEST10000", }, }, }, nil) - mtoShipment := factory.BuildMTOShipment(suite.DB(), []factory.Customization{ - { - Model: models.MTOShipment{ID: mtoShipmentID}, - }, - }, nil) + mtoShipment := factory.BuildMTOShipment(suite.DB(), nil, nil) requestUser := factory.BuildUser(nil, nil, nil) serviceItem := factory.BuildMTOServiceItem(suite.DB(), []factory.Customization{ { - Model: models.MTOServiceItem{ - ID: serviceItemID, - }, + Model: models.MTOServiceItem{}, }, { Model: mto, @@ -133,12 +123,10 @@ func (suite *HandlerSuite) TestListMTOServiceItemHandler() { } setupIUBTestData := func() (models.User, models.MTOServiceItems, uuid.UUID) { - mtoShipmentID, _ := uuid.NewV4() mto := factory.BuildMove(suite.DB(), nil, nil) mtoShipment := factory.BuildMTOShipment(suite.DB(), []factory.Customization{ { Model: models.MTOShipment{ - ID: mtoShipmentID, ShipmentType: models.MTOShipmentTypeUnaccompaniedBaggage, MarketCode: models.MarketCodeInternational, }, From bc37b12379b2da896f064c174e40caee93f81533 Mon Sep 17 00:00:00 2001 From: Steven Gleason <180579696+stevengleason-caci@users.noreply.github.com> Date: Thu, 23 Jan 2025 15:44:55 +0000 Subject: [PATCH 13/14] B-21569 move compare function from shared/utils. to new utils/string.js. --- .../ServiceItemsTable/ServiceItemsTable.jsx | 2 +- src/shared/utils.js | 21 ------- src/shared/utils.test.js | 54 ----------------- src/utils/string.js | 23 +++++++ src/utils/string.test.js | 60 +++++++++++++++++++ 5 files changed, 84 insertions(+), 76 deletions(-) create mode 100644 src/utils/string.js create mode 100644 src/utils/string.test.js diff --git a/src/components/Office/ServiceItemsTable/ServiceItemsTable.jsx b/src/components/Office/ServiceItemsTable/ServiceItemsTable.jsx index 7213e0345d1..60f48a93f84 100644 --- a/src/components/Office/ServiceItemsTable/ServiceItemsTable.jsx +++ b/src/components/Office/ServiceItemsTable/ServiceItemsTable.jsx @@ -19,7 +19,7 @@ import { selectDateFieldByStatus, selectDatePrefixByStatus } from 'utils/dates'; import { useGHCGetMoveHistory, useMovePaymentRequestsQueries } from 'hooks/queries'; import ToolTip from 'shared/ToolTip/ToolTip'; import { ShipmentShape } from 'types'; -import { nullSafeStringCompare } from 'shared/utils'; +import { nullSafeStringCompare } from 'utils/string'; // Sorts service items in an order preferred by the customer // Currently only SIT & shorthaul/linehaul receives special sorting diff --git a/src/shared/utils.js b/src/shared/utils.js index 314631ec343..ac6baa1307b 100644 --- a/src/shared/utils.js +++ b/src/shared/utils.js @@ -171,24 +171,3 @@ export function isEmpty(obj) { export function isNullUndefinedOrWhitespace(value) { return value == null || value === undefined || value.trim() === ''; } - -/** - * Compare strings. Null, undefined, and blanks are after other values. - * @returns -1, 0, 1 - */ -export function nullSafeStringCompare(a, b) { - const A_BEFORE = -1; - const A_AFTER = 1; - const SAME = 0; - - if (isNullUndefinedOrWhitespace(a) && isNullUndefinedOrWhitespace(b)) { - return SAME; - } - if (isNullUndefinedOrWhitespace(a)) { - return A_AFTER; - } - if (isNullUndefinedOrWhitespace(b)) { - return A_BEFORE; - } - return a.localeCompare(b); -} diff --git a/src/shared/utils.test.js b/src/shared/utils.test.js index 6f174bfb4d8..eb5ea3e0160 100644 --- a/src/shared/utils.test.js +++ b/src/shared/utils.test.js @@ -86,58 +86,4 @@ describe('utils', () => { }); }); }); - describe('nullSafeComparison', () => { - const A_BEFORE = -1; - const A_AFTER = 1; - const SAME = 0; - it('same value', () => { - const res = utils.nullSafeStringCompare('1', '1'); - expect(res).toEqual(SAME); - }); - it('greater than', () => { - const res = utils.nullSafeStringCompare('2', '1'); - expect(res).toEqual(A_AFTER); - }); - it('less than', () => { - const res = utils.nullSafeStringCompare('1', '2'); - expect(res).toEqual(A_BEFORE); - }); - it('both null', () => { - const res = utils.nullSafeStringCompare(null, null); - expect(res).toEqual(SAME); - }); - it('null and value', () => { - const res = utils.nullSafeStringCompare(null, '1'); - expect(res).toEqual(A_AFTER); - }); - it('value and null', () => { - const res = utils.nullSafeStringCompare('1', null); - expect(res).toEqual(A_BEFORE); - }); - it('both undefined', () => { - var udefA, udefB; - const res = utils.nullSafeStringCompare(udefA, udefB); - expect(res).toEqual(SAME); - }); - it('undefined and null', () => { - var udefA; - const res = utils.nullSafeStringCompare(udefA, null); - expect(res).toEqual(SAME); - }); - it('null and undefined', () => { - var udefB; - const res = utils.nullSafeStringCompare(null, udefB); - expect(res).toEqual(SAME); - }); - it('undefined and value', () => { - var udefA; - const res = utils.nullSafeStringCompare(udefA, '2'); - expect(res).toEqual(A_AFTER); - }); - it('value and undefined', () => { - var udefB; - const res = utils.nullSafeStringCompare('1', udefB); - expect(res).toEqual(A_BEFORE); - }); - }); }); diff --git a/src/utils/string.js b/src/utils/string.js new file mode 100644 index 00000000000..5e4544fd0ff --- /dev/null +++ b/src/utils/string.js @@ -0,0 +1,23 @@ +/* eslint-disable import/prefer-default-export */ +import { isNullUndefinedOrWhitespace } from 'shared/utils'; + +/** + * Compare strings. Null, undefined, and blanks are after other values. + * @returns -1, 0, 1 + */ +export function nullSafeStringCompare(a, b) { + const A_BEFORE = -1; + const A_AFTER = 1; + const SAME = 0; + + if (isNullUndefinedOrWhitespace(a) && isNullUndefinedOrWhitespace(b)) { + return SAME; + } + if (isNullUndefinedOrWhitespace(a)) { + return A_AFTER; + } + if (isNullUndefinedOrWhitespace(b)) { + return A_BEFORE; + } + return a.localeCompare(b); +} diff --git a/src/utils/string.test.js b/src/utils/string.test.js new file mode 100644 index 00000000000..d222f0e4f9b --- /dev/null +++ b/src/utils/string.test.js @@ -0,0 +1,60 @@ +/* eslint-disable import/prefer-default-export */ +import * as stringUtils from 'utils/string'; + +describe('utils string', () => { + describe('nullSafeComparison', () => { + const A_BEFORE = -1; + const A_AFTER = 1; + const SAME = 0; + it('same value', () => { + const res = stringUtils.nullSafeStringCompare('1', '1'); + expect(res).toEqual(SAME); + }); + it('greater than', () => { + const res = stringUtils.nullSafeStringCompare('2', '1'); + expect(res).toEqual(A_AFTER); + }); + it('less than', () => { + const res = stringUtils.nullSafeStringCompare('1', '2'); + expect(res).toEqual(A_BEFORE); + }); + it('both null', () => { + const res = stringUtils.nullSafeStringCompare(null, null); + expect(res).toEqual(SAME); + }); + it('null and value', () => { + const res = stringUtils.nullSafeStringCompare(null, '1'); + expect(res).toEqual(A_AFTER); + }); + it('value and null', () => { + const res = stringUtils.nullSafeStringCompare('1', null); + expect(res).toEqual(A_BEFORE); + }); + it('both undefined', () => { + let udefA; + let udefB; + const res = stringUtils.nullSafeStringCompare(udefA, udefB); + expect(res).toEqual(SAME); + }); + it('undefined and null', () => { + let udefA; + const res = stringUtils.nullSafeStringCompare(udefA, null); + expect(res).toEqual(SAME); + }); + it('null and undefined', () => { + let udefB; + const res = stringUtils.nullSafeStringCompare(null, udefB); + expect(res).toEqual(SAME); + }); + it('undefined and value', () => { + let udefA; + const res = stringUtils.nullSafeStringCompare(udefA, '2'); + expect(res).toEqual(A_AFTER); + }); + it('value and undefined', () => { + let udefB; + const res = stringUtils.nullSafeStringCompare('1', udefB); + expect(res).toEqual(A_BEFORE); + }); + }); +}); From e99936164d18b505c9230513931d435ef0ca4b11 Mon Sep 17 00:00:00 2001 From: Steven Gleason <180579696+stevengleason-caci@users.noreply.github.com> Date: Thu, 23 Jan 2025 18:41:32 +0000 Subject: [PATCH 14/14] B-21569 per peer preference, no nill now needed --- pkg/handlers/ghcapi/internal/payloads/model_to_payload.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/handlers/ghcapi/internal/payloads/model_to_payload.go b/pkg/handlers/ghcapi/internal/payloads/model_to_payload.go index 8cecc324744..55c44b81d98 100644 --- a/pkg/handlers/ghcapi/internal/payloads/model_to_payload.go +++ b/pkg/handlers/ghcapi/internal/payloads/model_to_payload.go @@ -1863,7 +1863,7 @@ func MTOServiceItemModel(s *models.MTOServiceItem, storer storage.FileStorer) *g serviceRequestDocs[i] = payload } } - var sort *string = nil + var sort *string if s.ReService.ReServiceItems != nil { for _, reServiceItem := range *s.ReService.ReServiceItems { if s.MTOShipment.MarketCode == reServiceItem.MarketCode && s.MTOShipment.ShipmentType == reServiceItem.ShipmentType {