Skip to content

Commit c41ccc0

Browse files
committed
Implement case mass-update endpoint.
1 parent 7f4515f commit c41ccc0

File tree

5 files changed

+267
-0
lines changed

5 files changed

+267
-0
lines changed

api/handle_cases.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -741,3 +741,22 @@ func handleEnrichCasePivotObjects(uc usecases.Usecases) func(c *gin.Context) {
741741
c.JSON(http.StatusOK, agent_dto.AdaptKYCEnrichmentResultsDto(responses))
742742
}
743743
}
744+
745+
func handleCaseMassUpdate(uc usecases.Usecases) func(c *gin.Context) {
746+
return func(c *gin.Context) {
747+
ctx := c.Request.Context()
748+
749+
var params dto.CaseMassUpdateDto
750+
751+
if err := c.ShouldBindJSON(&params); presentError(ctx, c, err) {
752+
c.Status(http.StatusBadRequest)
753+
return
754+
}
755+
756+
uc := usecasesWithCreds(ctx, uc).NewCaseUseCase()
757+
758+
if err := uc.MassUpdate(ctx, params); presentError(ctx, c, err) {
759+
return
760+
}
761+
}
762+
}

api/routes.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@ func addRoutes(r *gin.Engine, conf Configuration, uc usecases.Usecases, auth uti
238238

239239
router.GET("/cases", tom, handleListCases(uc))
240240
router.POST("/cases", tom, handlePostCase(uc))
241+
router.POST("/cases/mass-update", tom, handleCaseMassUpdate(uc))
241242
router.GET("/cases/:case_id", tom, handleGetCase(uc))
242243
router.GET("/cases/:case_id/next", tom, handleGetNextCase(uc))
243244
router.POST("/cases/:case_id/snooze", tom, handleSnoozeCase(uc))

dto/case_dto.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,3 +155,18 @@ type CaseDecisionListPaginationDto struct {
155155
HasMore bool `json:"has_more"`
156156
NextCursorId string `json:"next_cursor_id"`
157157
}
158+
159+
type CaseMassUpdateDto struct {
160+
Action string `json:"action" binding:"required,oneof=close reopen assign move_to_inbox"`
161+
CaseIds []uuid.UUID `json:"case_ids" binding:"required,dive,uuid"`
162+
Assign *CaseMassUpdateAssignDto `json:"assign" binding:"required_if=Action assign"`
163+
MoveToInbox *CaseMassUpdateMoveToInbox `json:"move_to_inbox" binding:"required_if=Action move_to_inbox"`
164+
}
165+
166+
type CaseMassUpdateAssignDto struct {
167+
AssigneeId uuid.UUID `json:"assignee_id" binding:"required"`
168+
}
169+
170+
type CaseMassUpdateMoveToInbox struct {
171+
InboxId uuid.UUID `json:"inbox_id" binding:"required"`
172+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package repositories
2+
3+
import (
4+
"context"
5+
6+
"github.com/Masterminds/squirrel"
7+
"github.com/checkmarble/marble-backend/models"
8+
"github.com/checkmarble/marble-backend/repositories/dbmodels"
9+
"github.com/google/uuid"
10+
"github.com/jackc/pgx/v5"
11+
)
12+
13+
func (repo *MarbleDbRepository) CaseMassChangeStatus(ctx context.Context, tx Transaction, caseIds []uuid.UUID, status models.CaseStatus) ([]uuid.UUID, error) {
14+
query := NewQueryBuilder().
15+
Update(dbmodels.TABLE_CASES).
16+
Set("status", status).
17+
Where(squirrel.And{
18+
squirrel.Eq{"id": caseIds},
19+
squirrel.NotEq{"status": status},
20+
}).
21+
Suffix("returning id")
22+
23+
return caseMassUpdateExecAndReturnedChanged(ctx, tx, query)
24+
}
25+
26+
func (repo *MarbleDbRepository) CaseMassAssign(ctx context.Context, tx Transaction, caseIds []uuid.UUID, assigneeId uuid.UUID) ([]uuid.UUID, error) {
27+
query := NewQueryBuilder().
28+
Update(dbmodels.TABLE_CASES).
29+
Set("assigned_to", assigneeId).
30+
Where(squirrel.And{
31+
squirrel.Eq{"id": caseIds},
32+
squirrel.Or{
33+
squirrel.Eq{"assigned_to": nil},
34+
squirrel.NotEq{"assigned_to": assigneeId},
35+
},
36+
}).
37+
Suffix("returning id")
38+
39+
return caseMassUpdateExecAndReturnedChanged(ctx, tx, query)
40+
}
41+
42+
func (repo *MarbleDbRepository) CaseMassMoveToInbox(ctx context.Context, tx Transaction, caseIds []uuid.UUID, inboxId uuid.UUID) ([]uuid.UUID, error) {
43+
query := NewQueryBuilder().
44+
Update(dbmodels.TABLE_CASES).
45+
Set("inbox_id", inboxId).
46+
Where(squirrel.And{
47+
squirrel.Eq{"id": caseIds},
48+
squirrel.Or{
49+
squirrel.Eq{"inbox_id": nil},
50+
squirrel.NotEq{"inbox_id": inboxId},
51+
},
52+
}).
53+
Suffix("returning id")
54+
55+
return caseMassUpdateExecAndReturnedChanged(ctx, tx, query)
56+
}
57+
58+
func caseMassUpdateExecAndReturnedChanged(ctx context.Context, tx Transaction, query squirrel.UpdateBuilder) ([]uuid.UUID, error) {
59+
sql, args, err := query.ToSql()
60+
61+
if err != nil {
62+
return nil, err
63+
}
64+
65+
rows, err := tx.Query(ctx, sql, args...)
66+
67+
if err != nil {
68+
return nil, err
69+
}
70+
71+
defer rows.Close()
72+
73+
var tmp uuid.UUID
74+
75+
ids, err := pgx.CollectRows(rows, func(row pgx.CollectableRow) (uuid.UUID, error) {
76+
if err := row.Scan(&tmp); err != nil {
77+
return uuid.Nil, err
78+
}
79+
80+
return tmp, nil
81+
})
82+
83+
if err != nil {
84+
return nil, err
85+
}
86+
87+
return ids, nil
88+
}

usecases/case_usecase.go

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ import (
44
"context"
55
"fmt"
66
"io"
7+
"maps"
78
"mime/multipart"
89
"slices"
910
"strings"
1011
"time"
1112

13+
"github.com/checkmarble/marble-backend/dto"
1214
"github.com/checkmarble/marble-backend/models"
1315
"github.com/checkmarble/marble-backend/pure_utils"
1416
"github.com/checkmarble/marble-backend/repositories"
@@ -69,6 +71,10 @@ type CaseUseCaseRepository interface {
6971
GetNextCase(ctx context.Context, exec repositories.Executor, c models.Case) (string, error)
7072

7173
UserById(ctx context.Context, exec repositories.Executor, userId string) (models.User, error)
74+
75+
CaseMassChangeStatus(ctx context.Context, tx repositories.Transaction, caseIds []uuid.UUID, status models.CaseStatus) ([]uuid.UUID, error)
76+
CaseMassAssign(ctx context.Context, tx repositories.Transaction, caseIds []uuid.UUID, assigneeId uuid.UUID) ([]uuid.UUID, error)
77+
CaseMassMoveToInbox(ctx context.Context, tx repositories.Transaction, caseIds []uuid.UUID, inboxId uuid.UUID) ([]uuid.UUID, error)
7278
}
7379

7480
type CaseUsecaseScreeningRepository interface {
@@ -1663,3 +1669,141 @@ func (uc *CaseUseCase) triggerAutoAssignment(ctx context.Context, tx repositorie
16631669

16641670
return nil
16651671
}
1672+
1673+
func (uc *CaseUseCase) MassUpdate(ctx context.Context, req dto.CaseMassUpdateDto) error {
1674+
exec := uc.executorFactory.NewExecutor()
1675+
orgId := uc.enforceSecurity.OrgId()
1676+
userId := uc.enforceSecurity.UserId()
1677+
1678+
sourceCases := make(map[string]models.Case, len(req.CaseIds))
1679+
events := make(map[string]models.CreateCaseEventAttributes, len(req.CaseIds))
1680+
1681+
var newAssignee models.User
1682+
1683+
availableInboxIds, err := uc.getAvailableInboxIds(ctx, exec, orgId)
1684+
if err != nil {
1685+
return err
1686+
}
1687+
1688+
if req.Action == "assign" {
1689+
var err error
1690+
1691+
newAssignee, err = uc.repository.UserById(ctx, exec, req.Assign.AssigneeId.String())
1692+
if err != nil {
1693+
return errors.Wrap(err, "target user for assignment not found")
1694+
}
1695+
}
1696+
1697+
// For all cases in the mass update, we need to check the current user can manage them.
1698+
for _, caseId := range req.CaseIds {
1699+
// TODO: that is harsh, refactor GetCaseById into a single SQL query to
1700+
// retrieve all cases outside of the loop
1701+
c, err := uc.repository.GetCaseById(ctx, exec, caseId.String())
1702+
if err != nil {
1703+
return err
1704+
}
1705+
1706+
if err := uc.enforceSecurity.ReadOrUpdateCase(c.GetMetadata(), availableInboxIds); err != nil {
1707+
return err
1708+
}
1709+
1710+
// If we are trying to mass-assign, we need to check, for each case, that the target user can manage the case.
1711+
if req.Action == "assign" {
1712+
if err := security.EnforceSecurityCaseForUser(newAssignee).ReadOrUpdateCase(c.GetMetadata(), availableInboxIds); err != nil {
1713+
return errors.Wrap(err, "target user lacks case permissions for assignment")
1714+
}
1715+
}
1716+
1717+
sourceCases[c.Id] = c
1718+
}
1719+
1720+
// When changing the cases' inboxes, the user needs to have access to the target inbox.
1721+
if req.Action == "move_to_inbox" {
1722+
if _, err := uc.inboxReader.GetInboxById(ctx, req.MoveToInbox.InboxId); err != nil {
1723+
return errors.Wrap(err, fmt.Sprintf("User does not have access the new inbox %s", req.MoveToInbox.InboxId))
1724+
}
1725+
}
1726+
1727+
return uc.transactionFactory.Transaction(ctx, func(tx repositories.Transaction) error {
1728+
switch req.Action {
1729+
case "close":
1730+
updatedIds, err := uc.repository.CaseMassChangeStatus(ctx, tx, req.CaseIds, models.CaseClosed)
1731+
1732+
if err != nil {
1733+
return err
1734+
}
1735+
1736+
for _, updatedId := range updatedIds {
1737+
events[updatedId.String()] = models.CreateCaseEventAttributes{
1738+
UserId: userId,
1739+
CaseId: updatedId.String(),
1740+
EventType: models.CaseStatusUpdated,
1741+
PreviousValue: utils.Ptr(string(sourceCases[updatedId.String()].Status)),
1742+
NewValue: utils.Ptr(string(models.CaseClosed)),
1743+
}
1744+
}
1745+
1746+
case "reopen":
1747+
updatedIds, err := uc.repository.CaseMassChangeStatus(ctx, tx, req.CaseIds, models.CasePending)
1748+
1749+
if err != nil {
1750+
return err
1751+
}
1752+
1753+
for _, updatedId := range updatedIds {
1754+
events[updatedId.String()] = models.CreateCaseEventAttributes{
1755+
UserId: userId,
1756+
CaseId: updatedId.String(),
1757+
EventType: models.CaseStatusUpdated,
1758+
PreviousValue: utils.Ptr(string(sourceCases[updatedId.String()].Status)),
1759+
NewValue: utils.Ptr(string(models.CasePending)),
1760+
}
1761+
}
1762+
1763+
case "assign":
1764+
updatedIds, err := uc.repository.CaseMassAssign(ctx, tx, req.CaseIds, req.Assign.AssigneeId)
1765+
1766+
if err != nil {
1767+
return err
1768+
}
1769+
1770+
for _, updatedId := range updatedIds {
1771+
events[updatedId.String()] = models.CreateCaseEventAttributes{
1772+
UserId: userId,
1773+
CaseId: updatedId.String(),
1774+
EventType: models.CaseAssigned,
1775+
PreviousValue: (*string)(sourceCases[updatedId.String()].AssignedTo),
1776+
NewValue: utils.Ptr(req.Assign.AssigneeId.String()),
1777+
}
1778+
}
1779+
1780+
case "move_to_inbox":
1781+
updatedIds, err := uc.repository.CaseMassMoveToInbox(ctx, tx, req.CaseIds, req.MoveToInbox.InboxId)
1782+
1783+
if err != nil {
1784+
return err
1785+
}
1786+
1787+
for _, updatedId := range updatedIds {
1788+
events[updatedId.String()] = models.CreateCaseEventAttributes{
1789+
UserId: userId,
1790+
CaseId: updatedId.String(),
1791+
EventType: models.CaseInboxChanged,
1792+
PreviousValue: utils.Ptr(sourceCases[updatedId.String()].InboxId.String()),
1793+
NewValue: utils.Ptr(req.MoveToInbox.InboxId.String()),
1794+
}
1795+
}
1796+
1797+
}
1798+
1799+
if len(events) == 0 {
1800+
return nil
1801+
}
1802+
1803+
if err := uc.repository.BatchCreateCaseEvents(ctx, tx, slices.Collect(maps.Values(events))); err != nil {
1804+
return err
1805+
}
1806+
1807+
return nil
1808+
})
1809+
}

0 commit comments

Comments
 (0)