diff --git a/pkg/handlers/ghcapi/queues_test.go b/pkg/handlers/ghcapi/queues_test.go index 631ccb1e52a..98902006ced 100644 --- a/pkg/handlers/ghcapi/queues_test.go +++ b/pkg/handlers/ghcapi/queues_test.go @@ -613,6 +613,11 @@ func (suite *HandlerSuite) TestGetMoveQueuesHandlerFilters() { Status: models.MTOServiceItemStatusSubmitted, }, }, + { + Model: models.ReService{ + Code: models.ReServiceCodeDOFSIT, + }, + }, }, nil) // Service Counseling Completed Move diff --git a/pkg/services/order/order_fetcher.go b/pkg/services/order/order_fetcher.go index 7505c1705cb..524271a345b 100644 --- a/pkg/services/order/order_fetcher.go +++ b/pkg/services/order/order_fetcher.go @@ -121,8 +121,9 @@ func (f orderFetcher) ListOrders(appCtx appcontext.AppContext, officeUserID uuid tooAssignedUserQuery := tooAssignedUserFilter(params.TOOAssignedUser) sortOrderQuery := sortOrder(params.Sort, params.Order, ppmCloseoutGblocs) counselingQuery := counselingOfficeFilter(params.CounselingOffice) + tooDestinationRequestsQuery := tooQueueOriginRequestsFilter(role, params.Locator) // Adding to an array so we can iterate over them and apply the filters after the query structure is set below - options := [20]QueryOption{branchQuery, locatorQuery, dodIDQuery, emplidQuery, customerNameQuery, originDutyLocationQuery, destinationDutyLocationQuery, moveStatusQuery, gblocQuery, submittedAtQuery, appearedInTOOAtQuery, requestedMoveDateQuery, ppmTypeQuery, closeoutInitiatedQuery, closeoutLocationQuery, ppmStatusQuery, sortOrderQuery, scAssignedUserQuery, tooAssignedUserQuery, counselingQuery} + options := [21]QueryOption{branchQuery, locatorQuery, dodIDQuery, emplidQuery, customerNameQuery, originDutyLocationQuery, destinationDutyLocationQuery, moveStatusQuery, gblocQuery, submittedAtQuery, appearedInTOOAtQuery, requestedMoveDateQuery, ppmTypeQuery, closeoutInitiatedQuery, closeoutLocationQuery, ppmStatusQuery, sortOrderQuery, scAssignedUserQuery, tooAssignedUserQuery, counselingQuery, tooDestinationRequestsQuery} var query *pop.Query if ppmCloseoutGblocs { @@ -190,6 +191,10 @@ func (f orderFetcher) ListOrders(appCtx appcontext.AppContext, officeUserID uuid } if role == roles.RoleTypeTOO { query.LeftJoin("office_users as assigned_user", "moves.too_assigned_id = assigned_user.id") + query.LeftJoin("mto_service_items", "mto_shipments.id = mto_service_items.mto_shipment_id"). + LeftJoin("re_services", "mto_service_items.re_service_id = re_services.id"). + LeftJoin("shipment_address_updates", "shipment_address_updates.shipment_id = mto_shipments.id AND shipment_address_updates.new_address_id != mto_shipments.destination_address_id"). + LeftJoin("sit_extensions", "mto_shipments.id = sit_extensions.mto_shipment_id") } if params.NeedsPPMCloseout != nil { @@ -989,3 +994,32 @@ func sortOrder(sort *string, order *string, ppmCloseoutGblocs bool) QueryOption } } } + +// We want to filter out any moves that have ONLY destination type requests to them, such as destination SIT, shuttle, out of the +// task order queue. If the moves have origin SIT, excess weight risks, or sit extensions, they should still appear in the task order +// queue, which is what this query looks for +func tooQueueOriginRequestsFilter(role roles.RoleType, locator *string) QueryOption { + return func(query *pop.Query) { + if role == roles.RoleTypeTOO { + baseQuery := ` + ( + (mto_service_items.status IS NULL OR (mto_service_items.status = 'SUBMITTED' AND re_services.code IN ('DOFSIT', 'DOASIT', 'DOPSIT', 'DOSHUT', 'DOSFSC', 'IOFSIT', 'IOASIT', 'IODSIT', 'IOSHUT', 'IOPSIT', 'ICRT', 'IOSFSC'))) + ) + OR + ( + ((moves.excess_weight_qualified_at IS NOT NULL AND moves.excess_weight_acknowledged_at IS NULL) AND moves.status = 'APPROVALS REQUESTED') + ) + OR + ( + ((sit_extensions.mto_shipment_id IS NOT NULL) AND sit_extensions.status = 'PENDING') + ) + ` + if locator != nil { + query.Where(` + (moves.locator = ?) AND ( `+baseQuery+`)`, strings.ToUpper(*locator)) + } else { + query.Where(baseQuery) + } + } + } +} diff --git a/pkg/services/order/order_fetcher_test.go b/pkg/services/order/order_fetcher_test.go index c69a1b82252..0f9332c1d9e 100644 --- a/pkg/services/order/order_fetcher_test.go +++ b/pkg/services/order/order_fetcher_test.go @@ -580,6 +580,152 @@ func (suite *OrderServiceSuite) TestListOrders() { suite.Equal(1, len(moves)) suite.Equal(createdPPM.Shipment.MoveTaskOrder.Locator, moves[0].Locator) }) + + suite.Run("task order queue does not return move with ONLY a destination address update request", func() { + officeUser := factory.BuildOfficeUserWithRoles(suite.DB(), nil, []roles.RoleType{roles.RoleTypeTOO}) + session := auth.Session{ + ApplicationName: auth.OfficeApp, + Roles: officeUser.User.Roles, + OfficeUserID: officeUser.ID, + IDToken: "fake_token", + AccessToken: "fakeAccessToken", + } + // build a move with a destination address request, should NOT appear in Task Order Queue + move := factory.BuildMove(suite.DB(), []factory.Customization{ + { + Model: models.Move{ + Status: models.MoveStatusAPPROVALSREQUESTED, + Show: models.BoolPointer(true), + }, + }}, nil) + + testUUID := uuid.UUID{} + shipment := factory.BuildMTOShipment(suite.DB(), []factory.Customization{ + { + Model: move, + LinkOnly: true, + }, + { + Model: models.MTOShipment{ + DestinationAddressID: &testUUID, + }, + }, + }, nil) + + suite.NotNil(shipment) + + shipmentAddressUpdate := factory.BuildShipmentAddressUpdate(suite.DB(), []factory.Customization{ + { + Model: shipment, + LinkOnly: true, + }, + { + Model: move, + LinkOnly: true, + }, + { + Model: models.ShipmentAddressUpdate{ + NewAddressID: testUUID, + }, + }, + }, []factory.Trait{factory.GetTraitShipmentAddressUpdateRequested}) + suite.NotNil(shipmentAddressUpdate) + + // build a second move + move2 := factory.BuildMove(suite.DB(), []factory.Customization{ + { + Model: models.Move{ + Status: models.MoveStatusAPPROVALSREQUESTED, + Show: models.BoolPointer(true), + }, + }}, nil) + suite.NotNil(move2) + + moves, moveCount, err := orderFetcher.ListOrders(suite.AppContextWithSessionForTest(&session), officeUser.ID, roles.RoleTypeTOO, &services.ListOrderParams{}) + + suite.FatalNoError(err) + // even though 2 moves were created, only one will be returned from the call to List Orders since we filter out + // the one with only a shipment address update to be routed to the destination requests queue + suite.Equal(1, moveCount) + suite.Equal(1, len(moves)) + }) + + suite.Run("task order queue does not return move with ONLY requested destination SIT service items", func() { + officeUser := factory.BuildOfficeUserWithRoles(suite.DB(), nil, []roles.RoleType{roles.RoleTypeTOO}) + session := auth.Session{ + ApplicationName: auth.OfficeApp, + Roles: officeUser.User.Roles, + OfficeUserID: officeUser.ID, + IDToken: "fake_token", + AccessToken: "fakeAccessToken", + } + + // build a move with a only origin service items + move := factory.BuildMove(suite.DB(), []factory.Customization{ + { + Model: models.Move{ + Status: models.MoveStatusAPPROVALSREQUESTED, + Show: models.BoolPointer(true), + }, + }}, nil) + + shipment := factory.BuildMTOShipment(suite.DB(), []factory.Customization{ + { + Model: move, + LinkOnly: true, + }, + }, nil) + suite.NotNil(shipment) + originSITServiceItem := factory.BuildMTOServiceItem(suite.DB(), []factory.Customization{ + { + Model: shipment, + LinkOnly: true, + }, + { + Model: models.ReService{ + Code: models.ReServiceCodeDOFSIT, + }, + }, + }, nil) + suite.NotNil(originSITServiceItem) + + // build a move with a both origin and destination service items + move2 := factory.BuildMove(suite.DB(), []factory.Customization{ + { + Model: models.Move{ + Status: models.MoveStatusAPPROVALSREQUESTED, + Show: models.BoolPointer(true), + }, + }}, nil) + shipment2 := factory.BuildMTOShipment(suite.DB(), []factory.Customization{ + { + Model: move2, + LinkOnly: true, + }, + }, nil) + + destinationSITServiceItem := factory.BuildMTOServiceItem(suite.DB(), []factory.Customization{ + { + Model: shipment2, + LinkOnly: true, + }, + { + Model: models.ReService{ + Code: models.ReServiceCodeDDFSIT, + }, + }, + }, nil) + + moves, moveCount, err := orderFetcher.ListOrders(suite.AppContextWithSessionForTest(&session), officeUser.ID, roles.RoleTypeTOO, &services.ListOrderParams{}) + + suite.Equal(models.MTOServiceItemStatusSubmitted, destinationSITServiceItem.Status) + + suite.FatalNoError(err) + // even though 2 moves were created, only one will be returned from the call to List Orders since we filter out + // the one with only destination service items + suite.Equal(1, moveCount) + suite.Equal(1, len(moves)) + }) } func (suite *OrderServiceSuite) TestListDestinationRequestsOrders() { diff --git a/playwright/tests/office/txo/tooFlows.spec.js b/playwright/tests/office/txo/tooFlows.spec.js index ebc7a29bfc6..d8517bdfc16 100644 --- a/playwright/tests/office/txo/tooFlows.spec.js +++ b/playwright/tests/office/txo/tooFlows.spec.js @@ -807,17 +807,10 @@ test.describe('TOO user', () => { await page.getByRole('button', { name: 'Select task_ordering_officer' }).click(); }); test('weight-based multiplier prioritizes billed weight', async ({ page }) => { - await page.getByRole('row', { name: 'Select...' }).getByTestId('locator').getByTestId('TextBoxFilter').click(); - await page - .getByRole('row', { name: 'Select...' }) - .getByTestId('locator') - .getByTestId('TextBoxFilter') - .fill(moveLoc); - await page - .getByRole('row', { name: 'Select...' }) - .getByTestId('locator') - .getByTestId('TextBoxFilter') - .press('Enter'); + await page.getByRole('link', { name: 'Search' }).click(); + await page.getByTestId('searchText').click(); + await page.getByTestId('searchText').fill(moveLoc); + await page.getByTestId('searchText').press('Enter'); await page.getByTestId('locator-0').click(); await page.getByRole('link', { name: 'Payment requests' }).click(); await page.getByRole('button', { name: 'Review shipment weights' }).click();