diff --git a/pkg/services/move/move_assignment.go b/pkg/services/move/move_assignment.go index a55211f9e81..61b68c45c4f 100644 --- a/pkg/services/move/move_assignment.go +++ b/pkg/services/move/move_assignment.go @@ -1,6 +1,8 @@ package move import ( + "container/list" + "github.com/gofrs/uuid" "github.com/transcom/mymove/pkg/appcontext" @@ -22,32 +24,55 @@ func (a moveAssigner) BulkMoveAssignment(appCtx appcontext.AppContext, queueType return nil, apperror.NewBadDataError("No moves to assign") } + // make a map to track users and their assignment counts + // and a queue of userIDs + moveAssignments := make(map[uuid.UUID]int) + queue := list.New() + for _, user := range officeUserData { + if user != nil && user.MoveAssignments > 0 { + userID := uuid.FromStringOrNil(user.ID.String()) + moveAssignments[userID] = int(user.MoveAssignments) + queue.PushBack(userID) + } + } + + // point at the index in the movesToAssign set + moveIndex := 0 + transactionErr := appCtx.NewTransaction(func(txnAppCtx appcontext.AppContext) error { - for _, move := range movesToAssign { - for _, officeUser := range officeUserData { - if officeUser != nil && officeUser.MoveAssignments > 0 { - officeUserId := uuid.FromStringOrNil(officeUser.ID.String()) - - switch queueType { - case string(models.QueueTypeCounseling): - move.SCAssignedID = &officeUserId - case string(models.QueueTypeCloseout): - move.SCAssignedID = &officeUserId - case string(models.QueueTypeTaskOrder): - move.TOOAssignedID = &officeUserId - case string(models.QueueTypePaymentRequest): - move.TIOAssignedID = &officeUserId - } - - officeUser.MoveAssignments -= 1 - - verrs, err := appCtx.DB().ValidateAndUpdate(&move) - if err != nil || verrs.HasAny() { - return apperror.NewInvalidInputError(move.ID, err, verrs, "") - } - - break - } + // while we have a queue... + for moveIndex < len(movesToAssign) && queue.Len() > 0 { + // grab that ID off the front + user := queue.Front() + userID := user.Value.(uuid.UUID) + queue.Remove(user) + + // do our assignment logic + move := movesToAssign[moveIndex] + switch queueType { + case string(models.QueueTypeCounseling): + move.SCAssignedID = &userID + case string(models.QueueTypeCloseout): + move.SCAssignedID = &userID + case string(models.QueueTypeTaskOrder): + move.TOOAssignedID = &userID + case string(models.QueueTypePaymentRequest): + move.TIOAssignedID = &userID + } + + verrs, err := appCtx.DB().ValidateAndUpdate(&move) + if err != nil || verrs.HasAny() { + return apperror.NewInvalidInputError(move.ID, err, verrs, "") + } + + // decrement the users assignment count + moveAssignments[userID]-- + // increment our index + moveIndex++ + + // If user still has remaining assignments, re-queue them + if moveAssignments[userID] > 0 { + queue.PushBack(userID) } } diff --git a/pkg/services/move/move_assignment_test.go b/pkg/services/move/move_assignment_test.go index 5c4ced099af..f5b3aa5a89b 100644 --- a/pkg/services/move/move_assignment_test.go +++ b/pkg/services/move/move_assignment_test.go @@ -56,6 +56,143 @@ func (suite *MoveServiceSuite) TestBulkMoveAssignment() { return transportationOffice, move1, move2, move3 } + suite.Run("properly distributes moves", func() { + transportationOffice, move1, move2, move3 := setupTestData() + move4 := factory.BuildMoveWithShipment(suite.DB(), []factory.Customization{ + { + Model: models.Move{ + Status: models.MoveStatusNeedsServiceCounseling, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + }, nil) + move5 := factory.BuildMoveWithShipment(suite.DB(), []factory.Customization{ + { + Model: models.Move{ + Status: models.MoveStatusNeedsServiceCounseling, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + }, nil) + move6 := factory.BuildMoveWithShipment(suite.DB(), []factory.Customization{ + { + Model: models.Move{ + Status: models.MoveStatusNeedsServiceCounseling, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + }, nil) + + officeUser1 := factory.BuildOfficeUserWithPrivileges(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + Email: "officeuser1@example.com", + Active: true, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + { + Model: models.User{ + Privileges: []models.Privilege{ + { + PrivilegeType: models.PrivilegeTypeSupervisor, + }, + }, + Roles: []roles.Role{ + { + RoleType: roles.RoleTypeServicesCounselor, + }, + }, + }, + }, + }, nil) + officeUser2 := factory.BuildOfficeUserWithPrivileges(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + Email: "officeuser2@example.com", + Active: true, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + { + Model: models.User{ + Roles: []roles.Role{ + { + RoleType: roles.RoleTypeServicesCounselor, + }, + }, + }, + }, + }, nil) + officeUser3 := factory.BuildOfficeUserWithPrivileges(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + Email: "officeuser3@example.com", + Active: true, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + { + Model: models.User{ + Roles: []roles.Role{ + { + RoleType: roles.RoleTypeServicesCounselor, + }, + }, + }, + }, + }, nil) + + moves := []models.Move{move1, move2, move3, move4, move5, move6} + userData := []*ghcmessages.BulkAssignmentForUser{ + {ID: strfmt.UUID(officeUser1.ID.String()), MoveAssignments: 1}, + {ID: strfmt.UUID(officeUser2.ID.String()), MoveAssignments: 2}, + {ID: strfmt.UUID(officeUser3.ID.String()), MoveAssignments: 3}, + } + + _, err := moveAssigner.BulkMoveAssignment(suite.AppContextForTest(), string(models.QueueTypeCounseling), userData, moves) + suite.NoError(err) + + // reload move data to check assigned + suite.NoError(suite.DB().Reload(&move1)) + suite.NoError(suite.DB().Reload(&move2)) + suite.NoError(suite.DB().Reload(&move3)) + suite.NoError(suite.DB().Reload(&move4)) + suite.NoError(suite.DB().Reload(&move5)) + suite.NoError(suite.DB().Reload(&move6)) + + suite.Equal(officeUser1.ID, *move1.SCAssignedID) + suite.Equal(officeUser2.ID, *move2.SCAssignedID) + suite.Equal(officeUser3.ID, *move3.SCAssignedID) + suite.Equal(officeUser2.ID, *move4.SCAssignedID) + suite.Equal(officeUser3.ID, *move5.SCAssignedID) + suite.Equal(officeUser3.ID, *move6.SCAssignedID) + }) + suite.Run("successfully assigns multiple counseling moves to a SC user", func() { transportationOffice, move1, move2, move3 := setupTestData() diff --git a/pkg/services/move/move_fetcher.go b/pkg/services/move/move_fetcher.go index e38da9e6000..d6b71224572 100644 --- a/pkg/services/move/move_fetcher.go +++ b/pkg/services/move/move_fetcher.go @@ -66,10 +66,17 @@ func (f moveFetcher) FetchMove(appCtx appcontext.AppContext, locator string, sea func (f moveFetcher) FetchMovesByIdArray(appCtx appcontext.AppContext, moveIds []ghcmessages.BulkAssignmentMoveData) (models.Moves, error) { moves := models.Moves{} - err := appCtx.DB().Q(). - Where("id in (?)", moveIds). + caseExpr := "CASE " + for i, moveId := range moveIds { + caseExpr += "WHEN id = '" + fmt.Sprintf("%v", moveId) + "' THEN " + fmt.Sprintf("%d", i) + " " + } + caseExpr += "ELSE " + fmt.Sprintf("%d", len(moveIds)) + " END" + + query := appCtx.DB().Q(). Where("show = TRUE"). - All(&moves) + Where("id in (?)", moveIds). + Order(caseExpr) + err := query.All(&moves) if err != nil { return nil, err