diff --git a/pkg/handlers/primeapi/mto_service_item_test.go b/pkg/handlers/primeapi/mto_service_item_test.go index 6041a62b377..54ef8eb8cb4 100644 --- a/pkg/handlers/primeapi/mto_service_item_test.go +++ b/pkg/handlers/primeapi/mto_service_item_test.go @@ -1097,8 +1097,8 @@ func (suite *HandlerSuite) TestCreateMTOServiceItemOriginSITHandlerWithDOFSITWit }, }, nil) factory.FetchReServiceByCode(suite.DB(), models.ReServiceCodeDOFSIT) - sitEntryDate := time.Date(2024, time.February, 28, 0, 0, 0, 0, time.UTC) - sitDepartureDate := time.Date(2024, time.February, 27, 0, 0, 0, 0, time.UTC) + sitEntryDate := time.Date(2024, time.February, 27, 0, 0, 0, 0, time.UTC) + sitDepartureDate := time.Date(2024, time.February, 28, 0, 0, 0, 0, time.UTC) sitPostalCode := "00000" // Original customer pickup address diff --git a/pkg/services/mto_service_item/mto_service_item_creator.go b/pkg/services/mto_service_item/mto_service_item_creator.go index 285783f615b..71be9917e5b 100644 --- a/pkg/services/mto_service_item/mto_service_item_creator.go +++ b/pkg/services/mto_service_item/mto_service_item_creator.go @@ -940,6 +940,12 @@ func (o *mtoServiceItemCreator) validateFirstDaySITServiceItem(appCtx appcontext return nil, err } + //SIT Entry Date must be before SIT Departure Date + err = o.checkSITEntryDateBeforeDepartureDate(serviceItem) + if err != nil { + return nil, err + } + verrs := validate.NewErrors() // check if the address IDs are nil diff --git a/pkg/services/mto_service_item/mto_service_item_creator_test.go b/pkg/services/mto_service_item/mto_service_item_creator_test.go index 5989301776f..7e560cd4ea5 100644 --- a/pkg/services/mto_service_item/mto_service_item_creator_test.go +++ b/pkg/services/mto_service_item/mto_service_item_creator_test.go @@ -1354,6 +1354,99 @@ func (suite *MTOServiceItemServiceSuite) TestCreateOriginSITServiceItem() { suite.IsType(apperror.ConflictError{}, err) }) + suite.Run("Do not create DOFSIT if departure date is after entry date", func() { + shipment := setupTestData() + originAddress := factory.BuildAddress(suite.DB(), nil, nil) + reServiceDOFSIT := factory.FetchReServiceByCode(suite.DB(), models.ReServiceCodeDOFSIT) + serviceItemDOFSIT := factory.BuildMTOServiceItem(nil, []factory.Customization{ + { + Model: models.MTOServiceItem{ + SITEntryDate: models.TimePointer(time.Now().AddDate(0, 0, 1)), + SITDepartureDate: models.TimePointer(time.Now()), + }, + }, + { + Model: reServiceDOFSIT, + LinkOnly: true, + }, + { + Model: shipment, + LinkOnly: true, + }, + { + Model: originAddress, + LinkOnly: true, + Type: &factory.Addresses.SITOriginHHGOriginalAddress, + }, + }, nil) + builder := query.NewQueryBuilder() + moveRouter := moverouter.NewMoveRouter() + planner := &mocks.Planner{} + planner.On("ZipTransitDistance", + mock.AnythingOfType("*appcontext.appContext"), + mock.Anything, + mock.Anything, + false, + false, + ).Return(400, nil) + creator := NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) + _, _, err := creator.CreateMTOServiceItem(suite.AppContextForTest(), &serviceItemDOFSIT) + suite.Error(err) + expectedError := fmt.Sprintf( + "the SIT Departure Date (%s) must be after the SIT Entry Date (%s)", + serviceItemDOFSIT.SITDepartureDate.Format("2006-01-02"), + serviceItemDOFSIT.SITEntryDate.Format("2006-01-02"), + ) + suite.Contains(err.Error(), expectedError) + }) + + suite.Run("Do not create DOFSIT if departure date is the same as entry date", func() { + today := models.TimePointer(time.Now()) + shipment := setupTestData() + originAddress := factory.BuildAddress(suite.DB(), nil, nil) + reServiceDOFSIT := factory.FetchReServiceByCode(suite.DB(), models.ReServiceCodeDOFSIT) + serviceItemDOFSIT := factory.BuildMTOServiceItem(nil, []factory.Customization{ + { + Model: models.MTOServiceItem{ + SITEntryDate: today, + SITDepartureDate: today, + }, + }, + { + Model: reServiceDOFSIT, + LinkOnly: true, + }, + { + Model: shipment, + LinkOnly: true, + }, + { + Model: originAddress, + LinkOnly: true, + Type: &factory.Addresses.SITOriginHHGOriginalAddress, + }, + }, nil) + builder := query.NewQueryBuilder() + moveRouter := moverouter.NewMoveRouter() + planner := &mocks.Planner{} + planner.On("ZipTransitDistance", + mock.AnythingOfType("*appcontext.appContext"), + mock.Anything, + mock.Anything, + false, + false, + ).Return(400, nil) + creator := NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) + _, _, err := creator.CreateMTOServiceItem(suite.AppContextForTest(), &serviceItemDOFSIT) + suite.Error(err) + expectedError := fmt.Sprintf( + "the SIT Departure Date (%s) must be after the SIT Entry Date (%s)", + serviceItemDOFSIT.SITDepartureDate.Format("2006-01-02"), + serviceItemDOFSIT.SITEntryDate.Format("2006-01-02"), + ) + suite.Contains(err.Error(), expectedError) + }) + suite.Run("Do not create standalone DOPSIT service item", func() { // TESTCASE SCENARIO // Under test: CreateMTOServiceItem function @@ -1779,6 +1872,63 @@ func (suite *MTOServiceItemServiceSuite) TestCreateDestSITServiceItem() { suite.Contains(err.Error(), expectedError) }) + suite.Run("Do not create DDFSIT if departure date is after entry date", func() { + shipment, creator, reServiceDDFSIT := setupTestData() + serviceItemDDFSIT := factory.BuildMTOServiceItem(nil, []factory.Customization{ + { + Model: models.MTOServiceItem{ + SITEntryDate: models.TimePointer(time.Now().AddDate(0, 0, 1)), + SITDepartureDate: models.TimePointer(time.Now()), + }, + }, + { + Model: reServiceDDFSIT, + LinkOnly: true, + }, + { + Model: shipment, + LinkOnly: true, + }, + }, nil) + _, _, err := creator.CreateMTOServiceItem(suite.AppContextForTest(), &serviceItemDDFSIT) + suite.Error(err) + expectedError := fmt.Sprintf( + "the SIT Departure Date (%s) must be after the SIT Entry Date (%s)", + serviceItemDDFSIT.SITDepartureDate.Format("2006-01-02"), + serviceItemDDFSIT.SITEntryDate.Format("2006-01-02"), + ) + suite.Contains(err.Error(), expectedError) + }) + + suite.Run("Do not create DDFSIT if departure date is the same as entry date", func() { + today := models.TimePointer(time.Now()) + shipment, creator, reServiceDDFSIT := setupTestData() + serviceItemDDFSIT := factory.BuildMTOServiceItem(nil, []factory.Customization{ + { + Model: models.MTOServiceItem{ + SITEntryDate: today, + SITDepartureDate: today, + }, + }, + { + Model: reServiceDDFSIT, + LinkOnly: true, + }, + { + Model: shipment, + LinkOnly: true, + }, + }, nil) + _, _, err := creator.CreateMTOServiceItem(suite.AppContextForTest(), &serviceItemDDFSIT) + suite.Error(err) + expectedError := fmt.Sprintf( + "the SIT Departure Date (%s) must be after the SIT Entry Date (%s)", + serviceItemDDFSIT.SITDepartureDate.Format("2006-01-02"), + serviceItemDDFSIT.SITEntryDate.Format("2006-01-02"), + ) + suite.Contains(err.Error(), expectedError) + }) + // Successful creation of DDASIT service item suite.Run("Success - DDASIT creation approved", func() { shipment, creator, reServiceDDFSIT := setupTestData() diff --git a/pkg/services/mto_service_item/mto_service_item_validators.go b/pkg/services/mto_service_item/mto_service_item_validators.go index b885b078d30..786f18d9944 100644 --- a/pkg/services/mto_service_item/mto_service_item_validators.go +++ b/pkg/services/mto_service_item/mto_service_item_validators.go @@ -838,3 +838,16 @@ func (o *mtoServiceItemCreator) checkSITEntryDateAndFADD(serviceItem *models.MTO return nil } + +func (o *mtoServiceItemCreator) checkSITEntryDateBeforeDepartureDate(serviceItem *models.MTOServiceItem) error { + if serviceItem.SITEntryDate == nil || serviceItem.SITDepartureDate == nil { + return nil + } + + //Departure Date has to be after the Entry Date + if !serviceItem.SITDepartureDate.After(*serviceItem.SITEntryDate) { + return apperror.NewUnprocessableEntityError(fmt.Sprintf("the SIT Departure Date (%s) must be after the SIT Entry Date (%s)", + serviceItem.SITDepartureDate.Format("2006-01-02"), serviceItem.SITEntryDate.Format("2006-01-02"))) + } + return nil +} diff --git a/pkg/services/mto_service_item/mto_service_item_validators_test.go b/pkg/services/mto_service_item/mto_service_item_validators_test.go index 4c5528887eb..4b74be7b68f 100644 --- a/pkg/services/mto_service_item/mto_service_item_validators_test.go +++ b/pkg/services/mto_service_item/mto_service_item_validators_test.go @@ -1025,7 +1025,8 @@ func (suite *MTOServiceItemServiceSuite) TestUpdateMTOServiceItemData() { }, }, nil) newSITServiceItem := oldSITServiceItem - newSITServiceItem.SITDepartureDate = &later + newSITDepartureDate := later.AddDate(0, 0, 1) + newSITServiceItem.SITDepartureDate = &newSITDepartureDate serviceItemData := updateMTOServiceItemData{ updatedServiceItem: newSITServiceItem, oldServiceItem: oldSITServiceItem, @@ -1117,72 +1118,6 @@ func (suite *MTOServiceItemServiceSuite) TestUpdateMTOServiceItemData() { } }) - suite.Run("SITDepartureDate - Does not error or update shipment auth end date when set after the authorized end date - international", func() { - // Under test: checkSITDepartureDate checks that - // the SITDepartureDate is not later than the authorized end date - // Set up: Create an old and new IOPSIT and IDDSIT, with a date later than the - // shipment and try to update. - // Expected outcome: No ERROR if departure date comes after the end date. - // Shipment auth end date does not change - mtoShipment := factory.BuildMTOShipment(suite.DB(), []factory.Customization{ - { - Model: models.MTOShipment{OriginSITAuthEndDate: &now, - DestinationSITAuthEndDate: &now}, - }, - }, nil) - testCases := []struct { - reServiceCode models.ReServiceCode - }{ - { - reServiceCode: models.ReServiceCodeIOPSIT, - }, - { - reServiceCode: models.ReServiceCodeIDDSIT, - }, - } - for _, tc := range testCases { - oldSITServiceItem := factory.BuildMTOServiceItem(nil, []factory.Customization{ - { - Model: models.ReService{ - Code: tc.reServiceCode, - }, - }, - { - Model: mtoShipment, - LinkOnly: true, - }, - { - Model: models.MTOServiceItem{ - SITEntryDate: &later, - }, - }, - }, nil) - newSITServiceItem := oldSITServiceItem - newSITServiceItem.SITDepartureDate = &later - serviceItemData := updateMTOServiceItemData{ - updatedServiceItem: newSITServiceItem, - oldServiceItem: oldSITServiceItem, - verrs: validate.NewErrors(), - } - err := serviceItemData.checkSITDepartureDate(suite.AppContextForTest()) - suite.NoError(err) - suite.False(serviceItemData.verrs.HasAny()) - - // Double check the shipment and ensure that the SITDepartureDate is in fact after the authorized end date - var postUpdateShipment models.MTOShipment - err = suite.DB().Find(&postUpdateShipment, mtoShipment.ID) - suite.NoError(err) - if tc.reServiceCode == models.ReServiceCodeIOPSIT { - suite.True(mtoShipment.OriginSITAuthEndDate.Truncate(24 * time.Hour).Equal(postUpdateShipment.OriginSITAuthEndDate.Truncate(24 * time.Hour))) - suite.True(newSITServiceItem.SITEntryDate.Truncate(24 * time.Hour).After(postUpdateShipment.OriginSITAuthEndDate.Truncate(24 * time.Hour))) - } - if tc.reServiceCode == models.ReServiceCodeIDDSIT { - suite.True(mtoShipment.DestinationSITAuthEndDate.Truncate(24 * time.Hour).Equal(postUpdateShipment.DestinationSITAuthEndDate.Truncate(24 * time.Hour))) - suite.True(newSITServiceItem.SITEntryDate.Truncate(24 * time.Hour).After(postUpdateShipment.DestinationSITAuthEndDate.Truncate(24 * time.Hour))) - } - } - }) - suite.Run("SITDepartureDate - errors when set before or equal the SIT entry date", func() { mtoShipment := factory.BuildMTOShipment(suite.DB(), []factory.Customization{ { @@ -1333,104 +1268,6 @@ func (suite *MTOServiceItemServiceSuite) TestUpdateMTOServiceItemData() { }) - suite.Run("SITDepartureDate - errors when set equal to the SIT entry date", func() { - mtoShipment := factory.BuildMTOShipment(suite.DB(), []factory.Customization{ - { - Model: models.MTOShipment{OriginSITAuthEndDate: &now, - DestinationSITAuthEndDate: &now}, - }, - }, nil) - testCases := []struct { - reServiceCode models.ReServiceCode - }{ - { - reServiceCode: models.ReServiceCodeDOPSIT, - }, - { - reServiceCode: models.ReServiceCodeDDDSIT, - }, - } - for _, tc := range testCases { - oldSITServiceItem := factory.BuildMTOServiceItem(nil, []factory.Customization{ - { - Model: models.ReService{ - Code: tc.reServiceCode, - }, - }, - { - Model: mtoShipment, - LinkOnly: true, - }, - { - Model: models.MTOServiceItem{ - SITEntryDate: &now, - }, - }, - }, nil) - newSITServiceItem := oldSITServiceItem - newSITServiceItem.SITDepartureDate = &now - serviceItemData := updateMTOServiceItemData{ - updatedServiceItem: newSITServiceItem, - oldServiceItem: oldSITServiceItem, - verrs: validate.NewErrors(), - } - err := serviceItemData.checkSITDepartureDate(suite.AppContextForTest()) - suite.NoError(err) // Just verrs - suite.True(serviceItemData.verrs.HasAny()) - suite.Contains(serviceItemData.verrs.Keys(), "SITDepartureDate") - suite.Contains(serviceItemData.verrs.Get("SITDepartureDate"), "SIT departure date cannot be set before or equal to the SIT entry date.") - } - }) - - suite.Run("SITDepartureDate - errors when set before the SIT entry date - international", func() { - mtoShipment := factory.BuildMTOShipment(suite.DB(), []factory.Customization{ - { - Model: models.MTOShipment{OriginSITAuthEndDate: &now, - DestinationSITAuthEndDate: &now}, - }, - }, nil) - testCases := []struct { - reServiceCode models.ReServiceCode - }{ - { - reServiceCode: models.ReServiceCodeIOPSIT, - }, - { - reServiceCode: models.ReServiceCodeIDDSIT, - }, - } - for _, tc := range testCases { - oldSITServiceItem := factory.BuildMTOServiceItem(nil, []factory.Customization{ - { - Model: models.ReService{ - Code: tc.reServiceCode, - }, - }, - { - Model: mtoShipment, - LinkOnly: true, - }, - { - Model: models.MTOServiceItem{ - SITEntryDate: &later, - }, - }, - }, nil) - newSITServiceItem := oldSITServiceItem - newSITServiceItem.SITDepartureDate = &before - serviceItemData := updateMTOServiceItemData{ - updatedServiceItem: newSITServiceItem, - oldServiceItem: oldSITServiceItem, - verrs: validate.NewErrors(), - } - err := serviceItemData.checkSITDepartureDate(suite.AppContextForTest()) - suite.NoError(err) // Just verrs - suite.True(serviceItemData.verrs.HasAny()) - suite.Contains(serviceItemData.verrs.Keys(), "SITDepartureDate") - suite.Contains(serviceItemData.verrs.Get("SITDepartureDate"), "SIT departure date cannot be set before the SIT entry date.") - } - }) - suite.Run("SITDepartureDate - errors when service item is missing a shipment ID", func() { oldSITServiceItem := factory.BuildMTOServiceItem(nil, []factory.Customization{ @@ -1916,4 +1753,49 @@ func (suite *MTOServiceItemServiceSuite) TestCreateMTOServiceItemValidators() { ) suite.Contains(err.Error(), expectedError) }) + + suite.Run("checkSITEntryDateBeforeDepartureDate - success when the SIT entry date is before the SIT departure date", func() { + s := mtoServiceItemCreator{} + serviceItem := setupTestData() + //Set SIT entry date = today, SIT departure date = tomorrow + serviceItem.SITEntryDate = models.TimePointer(time.Now()) + serviceItem.SITDepartureDate = models.TimePointer(time.Now().AddDate(0, 0, 1)) + err := s.checkSITEntryDateBeforeDepartureDate(&serviceItem) + suite.NoError(err) + }) + + suite.Run("checkSITEntryDateBeforeDepartureDate - error when the SIT entry date is after the SIT departure date", func() { + s := mtoServiceItemCreator{} + serviceItem := setupTestData() + //Set SIT entry date = tomorrow, SIT departure date = today + serviceItem.SITEntryDate = models.TimePointer(time.Now().AddDate(0, 0, 1)) + serviceItem.SITDepartureDate = models.TimePointer(time.Now()) + err := s.checkSITEntryDateBeforeDepartureDate(&serviceItem) + suite.Error(err) + suite.IsType(apperror.UnprocessableEntityError{}, err) + expectedError := fmt.Sprintf( + "the SIT Departure Date (%s) must be after the SIT Entry Date (%s)", + serviceItem.SITDepartureDate.Format("2006-01-02"), + serviceItem.SITEntryDate.Format("2006-01-02"), + ) + suite.Contains(err.Error(), expectedError) + }) + + suite.Run("checkSITEntryDateBeforeDepartureDate - error when the SIT entry date is the same as the SIT departure date", func() { + s := mtoServiceItemCreator{} + serviceItem := setupTestData() + //Set SIT entry date = today, SIT departure date = today + today := models.TimePointer(time.Now()) + serviceItem.SITEntryDate = today + serviceItem.SITDepartureDate = today + err := s.checkSITEntryDateBeforeDepartureDate(&serviceItem) + suite.Error(err) + suite.IsType(apperror.UnprocessableEntityError{}, err) + expectedError := fmt.Sprintf( + "the SIT Departure Date (%s) must be after the SIT Entry Date (%s)", + serviceItem.SITDepartureDate.Format("2006-01-02"), + serviceItem.SITEntryDate.Format("2006-01-02"), + ) + suite.Contains(err.Error(), expectedError) + }) } diff --git a/pkg/services/sit_entry_date_update/sit_entry_date_updater.go b/pkg/services/sit_entry_date_update/sit_entry_date_updater.go index 0e9d3d82397..54012f13bb0 100644 --- a/pkg/services/sit_entry_date_update/sit_entry_date_updater.go +++ b/pkg/services/sit_entry_date_update/sit_entry_date_updater.go @@ -2,6 +2,7 @@ package sitentrydateupdate import ( "database/sql" + "fmt" "time" "github.com/transcom/mymove/pkg/appcontext" @@ -86,12 +87,18 @@ func (p sitEntryDateUpdater) UpdateSitEntryDate(appCtx appcontext.AppContext, s // updating sister service item to have the next day for SIT entry date if s.SITEntryDate == nil { return nil, apperror.NewUnprocessableEntityError("You must provide the SIT entry date in the request") - } else if s.SITEntryDate != nil { - serviceItem.SITEntryDate = s.SITEntryDate - dayAfter := s.SITEntryDate.Add(24 * time.Hour) - serviceItemAdditionalDays.SITEntryDate = &dayAfter } + // The new SIT entry date must be before SIT departure date + if serviceItem.SITDepartureDate != nil && !s.SITEntryDate.Before(*serviceItem.SITDepartureDate) { + return nil, apperror.NewUnprocessableEntityError(fmt.Sprintf("the SIT Entry Date (%s) must be before the SIT Departure Date (%s)", + s.SITEntryDate.Format("2006-01-02"), serviceItem.SITDepartureDate.Format("2006-01-02"))) + } + + serviceItem.SITEntryDate = s.SITEntryDate + dayAfter := s.SITEntryDate.Add(24 * time.Hour) + serviceItemAdditionalDays.SITEntryDate = &dayAfter + // Make the update to both service items and create a InvalidInputError if there were validation issues transactionError := appCtx.NewTransaction(func(txnCtx appcontext.AppContext) error { diff --git a/pkg/services/sit_entry_date_update/sit_entry_date_updater_test.go b/pkg/services/sit_entry_date_update/sit_entry_date_updater_test.go index e9c0bb65de6..a5e79799f3c 100644 --- a/pkg/services/sit_entry_date_update/sit_entry_date_updater_test.go +++ b/pkg/services/sit_entry_date_update/sit_entry_date_updater_test.go @@ -1,6 +1,7 @@ package sitentrydateupdate import ( + "fmt" "time" "github.com/gofrs/uuid" @@ -164,4 +165,167 @@ func (suite *UpdateSitEntryDateServiceSuite) TestUpdateSitEntryDate() { suite.Equal(idaServiceItem.SITEntryDate.Local(), newSitEntryDateNextDay.Local()) }) + suite.Run("Fails to update when DOFSIT entry date is after DOFSIT departure date", func() { + today := models.TimePointer(time.Now()) + tomorrow := models.TimePointer(time.Now()) + move := factory.BuildMove(suite.DB(), nil, nil) + shipment := factory.BuildMTOShipment(suite.DB(), []factory.Customization{ + { + Model: move, + LinkOnly: true, + }, + }, nil) + dofsitServiceItem := factory.BuildMTOServiceItem(suite.DB(), []factory.Customization{ + { + Model: models.MTOServiceItem{ + SITEntryDate: today, + SITDepartureDate: tomorrow, + }, + }, + { + Model: shipment, + LinkOnly: true, + }, + { + Model: models.ReService{ + Code: models.ReServiceCodeDOFSIT, + }, + }, + }, nil) + updatedServiceItem := models.SITEntryDateUpdate{ + ID: dofsitServiceItem.ID, + SITEntryDate: models.TimePointer(tomorrow.AddDate(0, 0, 1)), + } + _, err := updater.UpdateSitEntryDate(suite.AppContextForTest(), &updatedServiceItem) + suite.Error(err) + expectedError := fmt.Sprintf( + "the SIT Entry Date (%s) must be before the SIT Departure Date (%s)", + updatedServiceItem.SITEntryDate.Format("2006-01-02"), + dofsitServiceItem.SITDepartureDate.Format("2006-01-02"), + ) + suite.Contains(err.Error(), expectedError) + }) + + suite.Run("Fails to update when DOFSIT entry date is the same as DOFSIT departure date", func() { + today := models.TimePointer(time.Now()) + tomorrow := models.TimePointer(time.Now()) + move := factory.BuildMove(suite.DB(), nil, nil) + shipment := factory.BuildMTOShipment(suite.DB(), []factory.Customization{ + { + Model: move, + LinkOnly: true, + }, + }, nil) + dofsitServiceItem := factory.BuildMTOServiceItem(suite.DB(), []factory.Customization{ + { + Model: models.MTOServiceItem{ + SITEntryDate: today, + SITDepartureDate: tomorrow, + }, + }, + { + Model: shipment, + LinkOnly: true, + }, + { + Model: models.ReService{ + Code: models.ReServiceCodeDOFSIT, + }, + }, + }, nil) + updatedServiceItem := models.SITEntryDateUpdate{ + ID: dofsitServiceItem.ID, + SITEntryDate: tomorrow, + } + _, err := updater.UpdateSitEntryDate(suite.AppContextForTest(), &updatedServiceItem) + suite.Error(err) + expectedError := fmt.Sprintf( + "the SIT Entry Date (%s) must be before the SIT Departure Date (%s)", + updatedServiceItem.SITEntryDate.Format("2006-01-02"), + dofsitServiceItem.SITDepartureDate.Format("2006-01-02"), + ) + suite.Contains(err.Error(), expectedError) + }) + + suite.Run("Fails to update when DDFSIT entry date is after DDFSIT departure date", func() { + today := models.TimePointer(time.Now()) + tomorrow := models.TimePointer(time.Now()) + move := factory.BuildMove(suite.DB(), nil, nil) + shipment := factory.BuildMTOShipment(suite.DB(), []factory.Customization{ + { + Model: move, + LinkOnly: true, + }, + }, nil) + ddfsitServiceItem := factory.BuildMTOServiceItem(suite.DB(), []factory.Customization{ + { + Model: models.MTOServiceItem{ + SITEntryDate: today, + SITDepartureDate: tomorrow, + }, + }, + { + Model: shipment, + LinkOnly: true, + }, + { + Model: models.ReService{ + Code: models.ReServiceCodeDDFSIT, + }, + }, + }, nil) + updatedServiceItem := models.SITEntryDateUpdate{ + ID: ddfsitServiceItem.ID, + SITEntryDate: models.TimePointer(tomorrow.AddDate(0, 0, 1)), + } + _, err := updater.UpdateSitEntryDate(suite.AppContextForTest(), &updatedServiceItem) + suite.Error(err) + expectedError := fmt.Sprintf( + "the SIT Entry Date (%s) must be before the SIT Departure Date (%s)", + updatedServiceItem.SITEntryDate.Format("2006-01-02"), + ddfsitServiceItem.SITDepartureDate.Format("2006-01-02"), + ) + suite.Contains(err.Error(), expectedError) + }) + + suite.Run("Fails to update when DDFSIT entry date is the same as DDFSIT departure date", func() { + today := models.TimePointer(time.Now()) + tomorrow := models.TimePointer(time.Now()) + move := factory.BuildMove(suite.DB(), nil, nil) + shipment := factory.BuildMTOShipment(suite.DB(), []factory.Customization{ + { + Model: move, + LinkOnly: true, + }, + }, nil) + ddfsitServiceItem := factory.BuildMTOServiceItem(suite.DB(), []factory.Customization{ + { + Model: models.MTOServiceItem{ + SITEntryDate: today, + SITDepartureDate: tomorrow, + }, + }, + { + Model: shipment, + LinkOnly: true, + }, + { + Model: models.ReService{ + Code: models.ReServiceCodeDDFSIT, + }, + }, + }, nil) + updatedServiceItem := models.SITEntryDateUpdate{ + ID: ddfsitServiceItem.ID, + SITEntryDate: tomorrow, + } + _, err := updater.UpdateSitEntryDate(suite.AppContextForTest(), &updatedServiceItem) + suite.Error(err) + expectedError := fmt.Sprintf( + "the SIT Entry Date (%s) must be before the SIT Departure Date (%s)", + updatedServiceItem.SITEntryDate.Format("2006-01-02"), + ddfsitServiceItem.SITDepartureDate.Format("2006-01-02"), + ) + suite.Contains(err.Error(), expectedError) + }) } diff --git a/src/pages/Office/MoveTaskOrder/MoveTaskOrder.jsx b/src/pages/Office/MoveTaskOrder/MoveTaskOrder.jsx index a1fe5abec1c..d2b71885c0a 100644 --- a/src/pages/Office/MoveTaskOrder/MoveTaskOrder.jsx +++ b/src/pages/Office/MoveTaskOrder/MoveTaskOrder.jsx @@ -798,6 +798,16 @@ export const MoveTaskOrder = (props) => { setAlertMessage('SIT entry date updated'); setAlertType('success'); }, + onError: (error) => { + let errorMessage = 'There was a problem updating the SIT entry date'; + if (error.response.status === 422) { + const responseData = JSON.parse(error?.response?.data); + errorMessage = responseData?.detail; + setAlertMessage(errorMessage); + setAlertType('error'); + } + setIsEditSitEntryDateModalVisible(false); + }, }, ); }; diff --git a/src/pages/Office/MoveTaskOrder/MoveTaskOrder.test.jsx b/src/pages/Office/MoveTaskOrder/MoveTaskOrder.test.jsx index 81a65bd6098..d07cd167678 100644 --- a/src/pages/Office/MoveTaskOrder/MoveTaskOrder.test.jsx +++ b/src/pages/Office/MoveTaskOrder/MoveTaskOrder.test.jsx @@ -1,6 +1,8 @@ import React from 'react'; import { mount } from 'enzyme'; -import { render, screen } from '@testing-library/react'; +import { render, screen, within, cleanup } from '@testing-library/react'; +import * as reactQuery from '@tanstack/react-query'; +import userEvent from '@testing-library/user-event'; import { unapprovedMTOQuery, @@ -22,6 +24,7 @@ import { multiplePaymentRequests, moveHistoryTestData, actualPPMWeightQuery, + approvedMTOWithApprovedSitItemsQuery, } from './moveTaskOrderUnitTestData'; import { MoveTaskOrder } from 'pages/Office/MoveTaskOrder/MoveTaskOrder'; @@ -543,6 +546,153 @@ describe('MoveTaskOrder', () => { }); }); + describe('SIT entry date update', () => { + const mockMutateServiceItemSitEntryDate = jest.fn(); + jest.spyOn(reactQuery, 'useMutation').mockImplementation(() => ({ + mutate: mockMutateServiceItemSitEntryDate, + })); + beforeEach(() => { + // Reset the mock before each test + mockMutateServiceItemSitEntryDate.mockReset(); + }); + afterEach(() => { + cleanup(); // This will unmount the component after each test + }); + + const renderComponent = () => { + useMoveTaskOrderQueries.mockReturnValue(approvedMTOWithApprovedSitItemsQuery); + useMovePaymentRequestsQueries.mockReturnValue({ paymentRequests: [] }); + useGHCGetMoveHistory.mockReturnValue(moveHistoryTestData); + const isMoveLocked = false; + render( + + + , + ); + }; + it('shows error message when SIT entry date is invalid', async () => { + renderComponent(); + // Set up the mock to simulate an error + mockMutateServiceItemSitEntryDate.mockImplementation((data, options) => { + options.onError({ + response: { + status: 422, + data: JSON.stringify({ + detail: + 'UpdateSitEntryDate failed for service item: the SIT Entry Date (2025-03-05) must be before the SIT Departure Date (2025-02-27)', + }), + }, + }); + }); + const approvedServiceItems = await screen.findByTestId('ApprovedServiceItemsTable'); + expect(approvedServiceItems).toBeInTheDocument(); + const spanElement = within(approvedServiceItems).getByText(/Domestic origin 1st day SIT/i); + expect(spanElement).toBeInTheDocument(); + // Search for the edit button within the approvedServiceItems div + const editButton = within(approvedServiceItems).getByRole('button', { name: /edit/i }); + expect(editButton).toBeInTheDocument(); + await userEvent.click(editButton); + const modal = await screen.findByTestId('modal'); + expect(modal).toBeInTheDocument(); + const heading = within(modal).getByRole('heading', { name: /Edit SIT Entry Date/i, level: 2 }); + expect(heading).toBeInTheDocument(); + const formGroups = screen.getAllByTestId('formGroup'); + const sitEntryDateFormGroup = Array.from(formGroups).find( + (group) => + within(group).queryByPlaceholderText('DD MMM YYYY') && + within(group).queryByPlaceholderText('DD MMM YYYY').getAttribute('name') === 'sitEntryDate', + ); + const dateInput = within(sitEntryDateFormGroup).getByPlaceholderText('DD MMM YYYY'); + expect(dateInput).toBeInTheDocument(); + const remarksTextarea = within(modal).getByTestId('officeRemarks'); + expect(remarksTextarea).toBeInTheDocument(); + const saveButton = within(modal).getByRole('button', { name: /Save/ }); + + await userEvent.clear(dateInput); + await userEvent.type(dateInput, '05 Mar 2025'); + await userEvent.type(remarksTextarea, 'Need to update the sit entry date.'); + expect(saveButton).toBeEnabled(); + await userEvent.click(saveButton); + + // Verify that the mutation was called + expect(mockMutateServiceItemSitEntryDate).toHaveBeenCalled(); + + // The modal should close + expect(screen.queryByTestId('modal')).not.toBeInTheDocument(); + + // Verify that the error message is displayed + const alert = screen.getByTestId('alert'); + expect(alert).toBeInTheDocument(); + expect(alert).toHaveClass('usa-alert--error'); + expect(alert).toHaveTextContent( + 'UpdateSitEntryDate failed for service item: the SIT Entry Date (2025-03-05) must be before the SIT Departure Date (2025-02-27)', + ); + }); + + it('shows success message when SIT entry date is valid', async () => { + renderComponent(); + // Set up the mock to simulate an error + mockMutateServiceItemSitEntryDate.mockImplementation((data, options) => { + options.onSuccess({ + response: { + status: 200, + data: JSON.stringify({ + detail: 'SIT entry date updated', + }), + }, + }); + }); + const approvedServiceItems = await screen.findByTestId('ApprovedServiceItemsTable'); + expect(approvedServiceItems).toBeInTheDocument(); + const spanElement = within(approvedServiceItems).getByText(/Domestic origin 1st day SIT/i); + expect(spanElement).toBeInTheDocument(); + // Search for the edit button within the approvedServiceItems div + const editButton = within(approvedServiceItems).getByRole('button', { name: /edit/i }); + expect(editButton).toBeInTheDocument(); + await userEvent.click(editButton); + const modal = await screen.findByTestId('modal'); + expect(modal).toBeInTheDocument(); + const heading = within(modal).getByRole('heading', { name: /Edit SIT Entry Date/i, level: 2 }); + expect(heading).toBeInTheDocument(); + const formGroups = screen.getAllByTestId('formGroup'); + const sitEntryDateFormGroup = Array.from(formGroups).find( + (group) => + within(group).queryByPlaceholderText('DD MMM YYYY') && + within(group).queryByPlaceholderText('DD MMM YYYY').getAttribute('name') === 'sitEntryDate', + ); + const dateInput = within(sitEntryDateFormGroup).getByPlaceholderText('DD MMM YYYY'); + expect(dateInput).toBeInTheDocument(); + const remarksTextarea = within(modal).getByTestId('officeRemarks'); + expect(remarksTextarea).toBeInTheDocument(); + const saveButton = within(modal).getByRole('button', { name: /Save/ }); + + await userEvent.clear(dateInput); + await userEvent.type(dateInput, '03 Mar 2024'); + await userEvent.type(remarksTextarea, 'Need to update the sit entry date.'); + expect(saveButton).toBeEnabled(); + await userEvent.click(saveButton); + + // Verify that the mutation was called + expect(mockMutateServiceItemSitEntryDate).toHaveBeenCalled(); + + // The modal should close + expect(screen.queryByTestId('modal')).not.toBeInTheDocument(); + + // Verify that the error message is displayed + const alert = screen.getByTestId('alert'); + expect(alert).toBeInTheDocument(); + expect(alert).toHaveClass('usa-alert--success'); + expect(alert).toHaveTextContent('SIT entry date updated'); + }); + }); + describe('approved mto with both submitted and approved shipments', () => { useMoveTaskOrderQueries.mockReturnValue(someShipmentsApprovedMTOQuery); useMovePaymentRequestsQueries.mockReturnValue(multiplePaymentRequests); diff --git a/src/pages/Office/MoveTaskOrder/moveTaskOrderUnitTestData.js b/src/pages/Office/MoveTaskOrder/moveTaskOrderUnitTestData.js index 614867fe84b..a1cc6a708ff 100644 --- a/src/pages/Office/MoveTaskOrder/moveTaskOrderUnitTestData.js +++ b/src/pages/Office/MoveTaskOrder/moveTaskOrderUnitTestData.js @@ -3004,3 +3004,76 @@ export const moveHistoryTestData = { ], }, }; + +export const approvedMTOWithApprovedSitItemsQuery = { + orders: { + 1: { + id: '1', + originDutyLocation: { + address: { + streetAddress1: '', + city: 'Fort Knox', + state: 'KY', + postalCode: '40121', + }, + }, + destinationDutyLocation: { + address: { + streetAddress1: '', + city: 'Fort Irwin', + state: 'CA', + postalCode: '92310', + }, + }, + entitlement: { + authorizedWeight: 8000, + totalWeight: 8500, + }, + }, + }, + move: { + id: '2', + status: MOVE_STATUSES.APPROVALS_REQUESTED, + }, + mtoShipments: [ + { + id: '3', + moveTaskOrderID: '2', + shipmentType: SHIPMENT_OPTIONS.HHG, + scheduledPickupDate: '2020-03-16', + requestedPickupDate: '2020-03-15', + pickupAddress: { + streetAddress1: '932 Baltic Avenue', + city: 'Chicago', + state: 'IL', + postalCode: '60601', + eTag: '1234', + }, + destinationAddress: { + streetAddress1: '10 Park Place', + city: 'Atlantic City', + state: 'NJ', + postalCode: '08401', + }, + status: shipmentStatuses.APPROVED, + eTag: '1234', + reweigh: { + id: '00000000-0000-0000-0000-000000000000', + }, + sitExtensions: [], + sitStatus: SITStatusOrigin, + }, + ], + mtoServiceItems: [ + { + id: '5', + mtoShipmentID: '3', + reServiceName: 'Domestic origin 1st day SIT', + status: SERVICE_ITEM_STATUS.APPROVED, + reServiceCode: 'DOFSIT', + }, + ], + isLoading: false, + isError: false, + isSuccess: true, +};