Skip to content

Commit

Permalink
Implement review match API.
Browse files Browse the repository at this point in the history
  • Loading branch information
Antoine Popineau committed Jan 22, 2025
1 parent 1553b59 commit 94d1536
Show file tree
Hide file tree
Showing 9 changed files with 196 additions and 32 deletions.
33 changes: 33 additions & 0 deletions api/handle_sanction_checks.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"net/http"

"github.com/checkmarble/marble-backend/dto"
"github.com/checkmarble/marble-backend/models"
"github.com/checkmarble/marble-backend/pure_utils"
"github.com/checkmarble/marble-backend/usecases"
"github.com/gin-gonic/gin"
Expand Down Expand Up @@ -31,3 +32,35 @@ func handleListSanctionChecks(uc usecases.Usecases) func(c *gin.Context) {
c.JSON(http.StatusOK, sanctionCheckJson)
}
}

func handleUpdateSanctionCheckMatchStatus(uc usecases.Usecases) func(c *gin.Context) {
return func(c *gin.Context) {
ctx := c.Request.Context()
matchId := c.Param("id")

var update dto.SanctionCheckMatchUpdateDto

if presentError(ctx, c, c.ShouldBindJSON(&update)) ||
presentError(ctx, c, update.Validate()) {
return
}

uc := usecasesWithCreds(ctx, uc).NewSanctionCheckUsecase()

match, err := uc.UpdateMatchStatus(ctx, matchId, models.SanctionCheckMatchUpdate{
Status: update.Status,
})

if presentError(ctx, c, err) {
return
}

c.JSON(http.StatusOK, dto.AdaptSanctionCheckMatchDto(match))
}
}

func handleCreateSanctionCheckMatchComment(uc usecases.Usecases) func(c *gin.Context) {
return func(c *gin.Context) {
c.Status(http.StatusTeapot)
}
}
3 changes: 3 additions & 0 deletions api/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ func addRoutes(r *gin.Engine, conf Configuration, uc usecases.Usecases, auth Aut
router.DELETE("/scenario-iteration-rules/:rule_id", tom, handleDeleteRule(uc))

router.GET("/sanction-checks", tom, handleListSanctionChecks(uc))
router.PATCH("/sanction-checks/matches/:id", tom, handleUpdateSanctionCheckMatchStatus(uc))
router.POST("/sanction-checks/matches/:id/comment", tom,
handleCreateSanctionCheckMatchComment(uc))

router.GET("/scenario-publications", tom, handleListScenarioPublications(uc))
router.POST("/scenario-publications", tom, handleCreateScenarioPublication(uc))
Expand Down
52 changes: 40 additions & 12 deletions dto/sanction_check_dto.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,41 @@ package dto

import (
"encoding/json"
"slices"

"github.com/checkmarble/marble-backend/models"
"github.com/checkmarble/marble-backend/pure_utils"
"github.com/cockroachdb/errors"
)

var (
ValidSanctionCheckStatuses = []string{"in_review", "confirmed_hit", "error"}
ValidSanctionCheckMatchStatuses = []string{"pending", "confirmed_hit", "no_hit"}
)

type SanctionCheckDto struct {
Id string `json:"id"`
Partial bool `json:"partial"`
Datasets []string `json:"datasets"`
Count int `json:"count"`
Request models.OpenSanctionsQuery `json:"request"`
Matches []SanctionCheckMatchDto `json:"matches"`
Id string `json:"id"`
Datasets []string `json:"datasets"`
Request models.OpenSanctionsQuery `json:"request"`
Status string `json:"status"`
Partial bool `json:"partial"`
Count int `json:"count"`
IsManual bool `json:"is_manual"`
RequestedBy *string `json:"requested_by,omitempty"`
Matches []SanctionCheckMatchDto `json:"matches"`
}

func AdaptSanctionCheckDto(m models.SanctionCheck) SanctionCheckDto {
sanctionCheck := SanctionCheckDto{
Id: m.Id,
Partial: m.Partial,
Count: m.Count,
Datasets: make([]string, 0),
Request: m.Query,
Matches: make([]SanctionCheckMatchDto, 0),
Id: m.Id,
Datasets: make([]string, 0),
Request: m.Query,
Status: m.Status,
Partial: m.Partial,
Count: m.Count,
IsManual: m.IsManual,
RequestedBy: m.RequestedBy,
Matches: make([]SanctionCheckMatchDto, 0),
}

if len(m.Query.OrgConfig.Datasets) > 0 {
Expand All @@ -40,6 +53,7 @@ type SanctionCheckMatchDto struct {
Id string `json:"id"`
EntityId string `json:"entity_id"`
QueryIds []string `json:"query_ids"`
Status string `json:"status"`
Datasets []string `json:"datasets"`
Payload json.RawMessage `json:"payload"`
}
Expand All @@ -48,10 +62,24 @@ func AdaptSanctionCheckMatchDto(m models.SanctionCheckMatch) SanctionCheckMatchD
match := SanctionCheckMatchDto{
Id: m.Id,
EntityId: m.EntityId,
Status: m.Status,
QueryIds: m.QueryIds,
Datasets: make([]string, 0),
Payload: m.Payload,
}

return match
}

type SanctionCheckMatchUpdateDto struct {
Status string `json:"status"`
}

func (dto SanctionCheckMatchUpdateDto) Validate() error {
if !slices.Contains(ValidSanctionCheckMatchStatuses, dto.Status) {
return errors.Wrap(models.BadParameterError,
"invalid status for sanction check match")
}

return nil
}
30 changes: 19 additions & 11 deletions models/sanction_check.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,27 @@ type OpenSanctionsQuery struct {
}

type SanctionCheck struct {
Query OpenSanctionsQuery

Id string
Partial bool
Count int
Matches []SanctionCheckMatch
Id string
DecisionId string
Query OpenSanctionsQuery
Partial bool
Count int
Status string
IsManual bool
RequestedBy *string
Matches []SanctionCheckMatch
}

type SanctionCheckMatch struct {
Payload []byte
Id string
SanctionCheckId string
EntityId string
Status string
QueryIds []string
Datasets []string
Payload []byte
}

Id string
EntityId string
QueryIds []string
Datasets []string
type SanctionCheckMatchUpdate struct {
Status string
}
10 changes: 7 additions & 3 deletions repositories/dbmodels/db_sanction_check.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,12 @@ func AdaptSanctionCheck(dto DBSanctionCheck) (models.SanctionCheck, error) {
}

return models.SanctionCheck{
Id: dto.Id,
Query: query,
Partial: dto.IsPartial,
Id: dto.Id,
DecisionId: dto.DecisionId,
Query: query,
Partial: dto.IsPartial,
Status: dto.Status,
IsManual: dto.IsManual,
RequestedBy: dto.RequestedBy,
}, nil
}
10 changes: 6 additions & 4 deletions repositories/dbmodels/db_sanction_check_match.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,12 @@ type DBSanctionCheckMatch struct {

func AdaptSanctionCheckMatch(dto DBSanctionCheckMatch) (models.SanctionCheckMatch, error) {
match := models.SanctionCheckMatch{
Id: dto.Id,
EntityId: dto.OpenSanctionEntityId,
QueryIds: dto.QueryIds,
Payload: dto.Payload,
Id: dto.Id,
SanctionCheckId: dto.SanctionCheckId,
EntityId: dto.OpenSanctionEntityId,
Status: dto.Status,
QueryIds: dto.QueryIds,
Payload: dto.Payload,
}

return match, nil
Expand Down
44 changes: 44 additions & 0 deletions repositories/sanction_check_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,19 @@ func (*MarbleDbRepository) ListSanctionChecksForDecision(ctx context.Context, ex
return SqlToListOfModels(ctx, exec, sql, dbmodels.AdaptSanctionCheck)
}

func (*MarbleDbRepository) GetSanctionCheck(ctx context.Context, exec Executor, id string) (models.SanctionCheck, error) {
if err := validateMarbleDbExecutor(exec); err != nil {
return models.SanctionCheck{}, err
}

sql := NewQueryBuilder().
Select(dbmodels.SelectSanctionChecksColumn...).
From(dbmodels.TABLE_SANCTION_CHECKS).
Where(squirrel.Eq{"id": id})

return SqlToModel(ctx, exec, sql, dbmodels.AdaptSanctionCheck)
}

func (*MarbleDbRepository) ListSanctionCheckMatches(ctx context.Context, exec Executor,
sanctionCheckId string,
) ([]models.SanctionCheckMatch, error) {
Expand All @@ -41,6 +54,37 @@ func (*MarbleDbRepository) ListSanctionCheckMatches(ctx context.Context, exec Ex
return SqlToListOfModels(ctx, exec, sql, dbmodels.AdaptSanctionCheckMatch)
}

func (*MarbleDbRepository) GetSanctionCheckMatch(ctx context.Context, exec Executor,
matchId string,
) (models.SanctionCheckMatch, error) {
if err := validateMarbleDbExecutor(exec); err != nil {
return models.SanctionCheckMatch{}, err
}

sql := NewQueryBuilder().
Select(dbmodels.SelectSanctionCheckMatchesColumn...).
From(dbmodels.TABLE_SANCTION_CHECK_MATCHES).
Where(squirrel.Eq{"id": matchId})

return SqlToModel(ctx, exec, sql, dbmodels.AdaptSanctionCheckMatch)
}

func (*MarbleDbRepository) UpdateSanctionCheckMatchStatus(ctx context.Context, exec Executor,
match models.SanctionCheckMatch, status string,
) (models.SanctionCheckMatch, error) {
if err := validateMarbleDbExecutor(exec); err != nil {
return models.SanctionCheckMatch{}, err
}

sql := NewQueryBuilder().
Update(dbmodels.TABLE_SANCTION_CHECK_MATCHES).
Set("status", status).
Where(squirrel.Eq{"id": match.Id}).
Suffix(fmt.Sprintf("RETURNING %s", strings.Join(dbmodels.SelectSanctionCheckMatchesColumn, ",")))

return SqlToModel(ctx, exec, sql, dbmodels.AdaptSanctionCheckMatch)
}

func (*MarbleDbRepository) InsertSanctionCheck(ctx context.Context, exec Executor,
decision models.DecisionWithRuleExecutions,
) (models.SanctionCheck, error) {
Expand Down
45 changes: 43 additions & 2 deletions usecases/sanction_check_usecase.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,20 @@ type SanctionCheckDecisionRepository interface {

type SanctionCheckRepository interface {
ListSanctionChecksForDecision(context.Context, repositories.Executor, string) ([]models.SanctionCheck, error)
ListSanctionCheckMatches(ctx context.Context, exec repositories.Executor, sanctionCheckId string) (
[]models.SanctionCheckMatch, error)
GetSanctionCheck(context.Context, repositories.Executor, string) (models.SanctionCheck, error)
InsertSanctionCheck(context.Context, repositories.Executor,
models.DecisionWithRuleExecutions) (models.SanctionCheck, error)

ListSanctionCheckMatches(ctx context.Context, exec repositories.Executor, sanctionCheckId string) (
[]models.SanctionCheckMatch, error)
GetSanctionCheckMatch(ctx context.Context, exec repositories.Executor, matchId string) (models.SanctionCheckMatch, error)
UpdateSanctionCheckMatchStatus(ctx context.Context, exec repositories.Executor,
match models.SanctionCheckMatch, status string) (models.SanctionCheckMatch, error)
}

type SanctionCheckUsecase struct {
enforceSecurityDecision security.EnforceSecurityDecision
enforceSecurityCase security.EnforceSecurityCase

organizationRepository repositories.OrganizationRepository
decisionRepository SanctionCheckDecisionRepository
Expand Down Expand Up @@ -98,3 +104,38 @@ func (uc SanctionCheckUsecase) InsertResults(ctx context.Context,
) (models.SanctionCheck, error) {
return uc.repository.InsertSanctionCheck(ctx, exec, decision)
}

func (uc SanctionCheckUsecase) UpdateMatchStatus(ctx context.Context, matchId string,
update models.SanctionCheckMatchUpdate,
) (models.SanctionCheckMatch, error) {
match, err := uc.repository.GetSanctionCheckMatch(ctx, uc.executorFactory.NewExecutor(), matchId)
if err != nil {
return models.SanctionCheckMatch{}, err
}

sanctionCheck, err := uc.repository.GetSanctionCheck(ctx, uc.executorFactory.NewExecutor(), match.SanctionCheckId)
if err != nil {
return models.SanctionCheckMatch{}, err
}

decision, err := uc.decisionRepository.DecisionsById(ctx, uc.executorFactory.NewExecutor(), []string{sanctionCheck.DecisionId})
if err != nil {
return models.SanctionCheckMatch{}, err
}
if len(decision) == 0 {
return models.SanctionCheckMatch{}, errors.Wrap(models.NotFoundError, "requested decision does not exist")
}
if decision[0].Case == nil {
return models.SanctionCheckMatch{}, errors.Wrap(err, "decision is not linked to a case")
}

inboxes := []string{decision[0].Case.InboxId}

if err := uc.enforceSecurityCase.ReadOrUpdateCase(*decision[0].Case, inboxes); err != nil {
return models.SanctionCheckMatch{}, err
}

// TODO: should we also have some form of automatic cascade between matches? Such as "if we have a confirmed hit, all other matches are no hit"?

return uc.repository.UpdateSanctionCheckMatchStatus(ctx, uc.executorFactory.NewExecutor(), match, update.Status)
}
1 change: 1 addition & 0 deletions usecases/usecases_with_creds.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ func (usecases *UsecasesWithCreds) NewDecisionUsecase() DecisionUsecase {
func (usecases *UsecasesWithCreds) NewSanctionCheckUsecase() SanctionCheckUsecase {
return SanctionCheckUsecase{
enforceSecurityDecision: usecases.NewEnforceDecisionSecurity(),
enforceSecurityCase: usecases.NewEnforceCaseSecurity(),
organizationRepository: usecases.Repositories.OrganizationRepository,
decisionRepository: &usecases.Repositories.MarbleDbRepository,
openSanctionsProvider: usecases.Repositories.OpenSanctionsRepository,
Expand Down

0 comments on commit 94d1536

Please sign in to comment.