-
Notifications
You must be signed in to change notification settings - Fork 18
Case mass-update endpoint. #1231
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
package repositories | ||
|
||
import ( | ||
"context" | ||
|
||
"github.com/Masterminds/squirrel" | ||
"github.com/checkmarble/marble-backend/models" | ||
"github.com/checkmarble/marble-backend/repositories/dbmodels" | ||
"github.com/google/uuid" | ||
"github.com/jackc/pgx/v5" | ||
) | ||
|
||
func (repo *MarbleDbRepository) GetMassCasesByIds(ctx context.Context, exec Executor, caseIds []uuid.UUID) ([]models.Case, error) { | ||
if err := validateMarbleDbExecutor(exec); err != nil { | ||
return nil, err | ||
} | ||
|
||
query := NewQueryBuilder(). | ||
Select(dbmodels.SelectCaseColumn...). | ||
From(dbmodels.TABLE_CASES). | ||
Where(squirrel.Eq{"id": caseIds}) | ||
|
||
return SqlToListOfModels(ctx, exec, query, dbmodels.AdaptCase) | ||
} | ||
|
||
func (repo *MarbleDbRepository) CaseMassChangeStatus(ctx context.Context, tx Transaction, caseIds []uuid.UUID, status models.CaseStatus) ([]uuid.UUID, error) { | ||
if err := validateMarbleDbExecutor(tx); err != nil { | ||
return nil, err | ||
} | ||
|
||
query := NewQueryBuilder(). | ||
Update(dbmodels.TABLE_CASES). | ||
Set("status", status). | ||
Set("boost", nil). | ||
Where(squirrel.And{ | ||
squirrel.Eq{"id": caseIds}, | ||
squirrel.NotEq{"status": status}, | ||
}). | ||
Suffix("returning id") | ||
|
||
return caseMassUpdateExecAndReturnedChanged(ctx, tx, query) | ||
} | ||
|
||
func (repo *MarbleDbRepository) CaseMassAssign(ctx context.Context, tx Transaction, caseIds []uuid.UUID, assigneeId uuid.UUID) ([]uuid.UUID, error) { | ||
if err := validateMarbleDbExecutor(tx); err != nil { | ||
return nil, err | ||
} | ||
|
||
query := NewQueryBuilder(). | ||
Update(dbmodels.TABLE_CASES). | ||
Set("assigned_to", assigneeId). | ||
Set("boost", nil). | ||
Where(squirrel.And{ | ||
squirrel.Eq{"id": caseIds}, | ||
squirrel.Or{ | ||
squirrel.Eq{"assigned_to": nil}, | ||
squirrel.NotEq{"assigned_to": assigneeId}, | ||
}, | ||
}). | ||
Suffix("returning id") | ||
|
||
return caseMassUpdateExecAndReturnedChanged(ctx, tx, query) | ||
} | ||
|
||
func (repo *MarbleDbRepository) CaseMassMoveToInbox(ctx context.Context, tx Transaction, caseIds []uuid.UUID, inboxId uuid.UUID) ([]uuid.UUID, error) { | ||
if err := validateMarbleDbExecutor(tx); err != nil { | ||
return nil, err | ||
} | ||
|
||
query := NewQueryBuilder(). | ||
Update(dbmodels.TABLE_CASES). | ||
Set("inbox_id", inboxId). | ||
Set("boost", nil). | ||
Where(squirrel.And{ | ||
squirrel.Eq{"id": caseIds}, | ||
squirrel.Or{ | ||
squirrel.Eq{"inbox_id": nil}, | ||
squirrel.NotEq{"inbox_id": inboxId}, | ||
}, | ||
}). | ||
Suffix("returning id") | ||
|
||
return caseMassUpdateExecAndReturnedChanged(ctx, tx, query) | ||
} | ||
|
||
func caseMassUpdateExecAndReturnedChanged(ctx context.Context, tx Transaction, query squirrel.UpdateBuilder) ([]uuid.UUID, error) { | ||
sql, args, err := query.ToSql() | ||
|
||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
rows, err := tx.Query(ctx, sql, args...) | ||
|
||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
defer rows.Close() | ||
|
||
var tmp uuid.UUID | ||
|
||
ids, err := pgx.CollectRows(rows, func(row pgx.CollectableRow) (uuid.UUID, error) { | ||
if err := row.Scan(&tmp); err != nil { | ||
return uuid.Nil, err | ||
} | ||
|
||
return tmp, nil | ||
}) | ||
|
||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return ids, nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,11 +4,13 @@ import ( | |
"context" | ||
"fmt" | ||
"io" | ||
"maps" | ||
"mime/multipart" | ||
"slices" | ||
"strings" | ||
"time" | ||
|
||
"github.com/checkmarble/marble-backend/dto" | ||
"github.com/checkmarble/marble-backend/models" | ||
"github.com/checkmarble/marble-backend/pure_utils" | ||
"github.com/checkmarble/marble-backend/repositories" | ||
|
@@ -69,6 +71,11 @@ type CaseUseCaseRepository interface { | |
GetNextCase(ctx context.Context, exec repositories.Executor, c models.Case) (string, error) | ||
|
||
UserById(ctx context.Context, exec repositories.Executor, userId string) (models.User, error) | ||
|
||
GetMassCasesByIds(ctx context.Context, exec repositories.Executor, caseIds []uuid.UUID) ([]models.Case, error) | ||
CaseMassChangeStatus(ctx context.Context, tx repositories.Transaction, caseIds []uuid.UUID, status models.CaseStatus) ([]uuid.UUID, error) | ||
CaseMassAssign(ctx context.Context, tx repositories.Transaction, caseIds []uuid.UUID, assigneeId uuid.UUID) ([]uuid.UUID, error) | ||
CaseMassMoveToInbox(ctx context.Context, tx repositories.Transaction, caseIds []uuid.UUID, inboxId uuid.UUID) ([]uuid.UUID, error) | ||
} | ||
|
||
type CaseUsecaseScreeningRepository interface { | ||
|
@@ -1663,3 +1670,155 @@ func (uc *CaseUseCase) triggerAutoAssignment(ctx context.Context, tx repositorie | |
|
||
return nil | ||
} | ||
|
||
func (uc *CaseUseCase) MassUpdate(ctx context.Context, req dto.CaseMassUpdateDto) error { | ||
exec := uc.executorFactory.NewExecutor() | ||
orgId := uc.enforceSecurity.OrgId() | ||
userId := uc.enforceSecurity.UserId() | ||
|
||
sourceCases := make(map[string]models.Case, len(req.CaseIds)) | ||
events := make(map[string]models.CreateCaseEventAttributes, len(req.CaseIds)) | ||
|
||
var newAssignee models.User | ||
|
||
cases, err := uc.repository.GetMassCasesByIds(ctx, exec, req.CaseIds) | ||
if err != nil { | ||
return errors.Wrap(err, "could not retrieve requested cases for mass update") | ||
} | ||
|
||
if len(cases) != len(req.CaseIds) { | ||
return errors.New("some requested cases for mass update do not exist") | ||
} | ||
|
||
casesMap := pure_utils.MapSliceToMap(cases, func(c models.Case) (string, models.Case) { | ||
return c.Id, c | ||
}) | ||
|
||
availableInboxIds, err := uc.getAvailableInboxIds(ctx, exec, orgId) | ||
if err != nil { | ||
return errors.Wrap(err, "could not retrieve available inboxes") | ||
} | ||
|
||
if req.Action == models.CaseMassUpdateAssign { | ||
var err error | ||
|
||
newAssignee, err = uc.repository.UserById(ctx, exec, req.Assign.AssigneeId.String()) | ||
if err != nil { | ||
return errors.Wrap(err, "target user for assignment not found") | ||
} | ||
} | ||
|
||
// For all cases in the mass update, we need to check the current user can manage them. | ||
for _, caseId := range req.CaseIds { | ||
c, ok := casesMap[caseId.String()] | ||
if !ok { | ||
return errors.Newf("requested cases '%s' for mass update does not exist", caseId) | ||
} | ||
|
||
if err := uc.enforceSecurity.ReadOrUpdateCase(c.GetMetadata(), availableInboxIds); err != nil { | ||
return err | ||
} | ||
|
||
// If we are trying to mass-assign, we need to check, for each case, that the target user can manage the case. | ||
if req.Action == models.CaseMassUpdateAssign { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thinking out loud: I think it's correct to make this check in the backend and let the endpoint accept cases from different inboxes, however I think we may want to restrict the action of (re)assigning cases in batch to the "inbox" view, meaning not allow in the "my cases" view, because doing so would create a level of headaches related to selection of eligible users and error handling that is beyond the scope of what we planned to do, cc @ChibiBlasphem |
||
if err := security.EnforceSecurityCaseForUser(newAssignee).ReadOrUpdateCase(c.GetMetadata(), availableInboxIds); err != nil { | ||
return errors.Wrap(err, "target user lacks case permissions for assignment") | ||
} | ||
} | ||
|
||
sourceCases[c.Id] = c | ||
} | ||
|
||
// When changing the cases' inboxes, the user needs to have access to the target inbox. | ||
if req.Action == models.CaseMassUpdateMoveToInbox { | ||
if _, err := uc.inboxReader.GetInboxById(ctx, req.MoveToInbox.InboxId); err != nil { | ||
return errors.Wrap(err, fmt.Sprintf("user does not have access the new inbox %s", req.MoveToInbox.InboxId)) | ||
} | ||
} | ||
|
||
return uc.transactionFactory.Transaction(ctx, func(tx repositories.Transaction) error { | ||
switch models.CaseMassUpdateActionFromString(req.Action) { | ||
case models.CaseMassUpdateClose: | ||
updatedIds, err := uc.repository.CaseMassChangeStatus(ctx, tx, req.CaseIds, models.CaseClosed) | ||
if err != nil { | ||
return errors.Wrap(err, "could not update case status in mass update") | ||
} | ||
|
||
for _, updatedId := range updatedIds { | ||
events[updatedId.String()] = models.CreateCaseEventAttributes{ | ||
UserId: userId, | ||
CaseId: updatedId.String(), | ||
EventType: models.CaseStatusUpdated, | ||
PreviousValue: utils.Ptr(string(sourceCases[updatedId.String()].Status)), | ||
NewValue: utils.Ptr(string(models.CaseClosed)), | ||
} | ||
} | ||
|
||
case models.CaseMassUpdateReopen: | ||
updatedIds, err := uc.repository.CaseMassChangeStatus(ctx, tx, req.CaseIds, models.CasePending) | ||
|
||
if err != nil { | ||
return errors.Wrap(err, "could not updaet case status in mass update") | ||
} | ||
|
||
for _, updatedId := range updatedIds { | ||
events[updatedId.String()] = models.CreateCaseEventAttributes{ | ||
UserId: userId, | ||
CaseId: updatedId.String(), | ||
EventType: models.CaseStatusUpdated, | ||
PreviousValue: utils.Ptr(string(sourceCases[updatedId.String()].Status)), | ||
NewValue: utils.Ptr(string(models.CasePending)), | ||
} | ||
} | ||
|
||
case models.CaseMassUpdateAssign: | ||
updatedIds, err := uc.repository.CaseMassAssign(ctx, tx, req.CaseIds, req.Assign.AssigneeId) | ||
|
||
if err != nil { | ||
return errors.Wrap(err, "could not assign cases in mass update") | ||
} | ||
|
||
for _, updatedId := range updatedIds { | ||
events[updatedId.String()] = models.CreateCaseEventAttributes{ | ||
UserId: userId, | ||
CaseId: updatedId.String(), | ||
EventType: models.CaseAssigned, | ||
PreviousValue: (*string)(sourceCases[updatedId.String()].AssignedTo), | ||
NewValue: utils.Ptr(req.Assign.AssigneeId.String()), | ||
} | ||
} | ||
|
||
case models.CaseMassUpdateMoveToInbox: | ||
updatedIds, err := uc.repository.CaseMassMoveToInbox(ctx, tx, req.CaseIds, req.MoveToInbox.InboxId) | ||
|
||
if err != nil { | ||
return errors.Wrap(err, "could not change case inbox in mass update") | ||
} | ||
|
||
for _, updatedId := range updatedIds { | ||
events[updatedId.String()] = models.CreateCaseEventAttributes{ | ||
UserId: userId, | ||
CaseId: updatedId.String(), | ||
EventType: models.CaseInboxChanged, | ||
PreviousValue: utils.Ptr(sourceCases[updatedId.String()].InboxId.String()), | ||
NewValue: utils.Ptr(req.MoveToInbox.InboxId.String()), | ||
} | ||
} | ||
|
||
default: | ||
return errors.Newf("unknown case mass update action %s", req.Action) | ||
} | ||
|
||
if len(events) == 0 { | ||
return nil | ||
} | ||
|
||
// TODO: perform relevant side effects | ||
|
||
if err := uc.repository.BatchCreateCaseEvents(ctx, tx, slices.Collect(maps.Values(events))); err != nil { | ||
return errors.Wrap(err, "could not create case events in mass update") | ||
} | ||
|
||
return nil | ||
}) | ||
} |
Uh oh!
There was an error while loading. Please reload this page.