Skip to content

Commit

Permalink
Added tentative API endpoint to retrieve a sanction check.
Browse files Browse the repository at this point in the history
  • Loading branch information
apognu committed Jan 21, 2025
1 parent cb46917 commit 07caa7c
Show file tree
Hide file tree
Showing 12 changed files with 214 additions and 30 deletions.
33 changes: 33 additions & 0 deletions api/handle_sanction_checks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package api

import (
"net/http"

"github.com/checkmarble/marble-backend/dto"
"github.com/checkmarble/marble-backend/pure_utils"
"github.com/checkmarble/marble-backend/usecases"
"github.com/gin-gonic/gin"
)

func handleListSanctionChecks(uc usecases.Usecases) func(c *gin.Context) {
return func(c *gin.Context) {
ctx := c.Request.Context()
decisionId := c.Query("decision_id")

if decisionId == "" {
c.Status(http.StatusBadRequest)
return
}

uc := usecasesWithCreds(ctx, uc).NewSanctionCheckUsecase()
sanctionChecks, err := uc.ListSanctionChecks(ctx, decisionId)

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

sanctionCheckJson := pure_utils.Map(sanctionChecks, dto.AdaptSanctionCheckDto)

c.JSON(http.StatusOK, sanctionCheckJson)
}
}
2 changes: 2 additions & 0 deletions api/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ func addRoutes(r *gin.Engine, conf Configuration, uc usecases.Usecases, auth Aut
router.PATCH("/scenario-iteration-rules/:rule_id", tom, handleUpdateRule(uc))
router.DELETE("/scenario-iteration-rules/:rule_id", tom, handleDeleteRule(uc))

router.GET("/sanction-checks", tom, handleListSanctionChecks(uc))

router.GET("/scenario-publications", tom, handleListScenarioPublications(uc))
router.POST("/scenario-publications", tom, handleCreateScenarioPublication(uc))
router.GET("/scenario-publications/preparation", tom,
Expand Down
57 changes: 57 additions & 0 deletions dto/sanction_check_dto.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package dto

import (
"encoding/json"

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

type SanctionCheckDto struct {
Id string `json:"id"`
Partial bool `json:"partial"`
Datasets []string `json:"datasets"`
Count int `json:"count"`
Request json.RawMessage `json:"request"`
Matches []SanctionCheckMatchDto `json:"matches"`
}

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

if len(m.Query.OrgConfig.Datasets) > 0 {
sanctionCheck.Datasets = m.Query.OrgConfig.Datasets
}
if len(m.Matches) > 0 {
sanctionCheck.Matches = pure_utils.Map(m.Matches, AdaptSanctionCheckMatchDto)
}

return sanctionCheck
}

type SanctionCheckMatchDto struct {
Id string `json:"id"`
EntityId string `json:"entity_id"`
QueryIds []string `json:"query_ids"`
Datasets []string `json:"datasets"`
Payload json.RawMessage `json:"payload"`
}

func AdaptSanctionCheckMatchDto(m models.SanctionCheckExecutionMatch) SanctionCheckMatchDto {
match := SanctionCheckMatchDto{
Id: m.Id,
EntityId: m.EntityId,
QueryIds: m.QueryIds,
Datasets: make([]string, 0),
Payload: m.Payload,
}

return match
}
8 changes: 4 additions & 4 deletions models/sanction_check.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ package models
type OpenSanctionCheckFilter map[string][]string

type OpenSanctionsQuery struct {
Queries OpenSanctionCheckFilter `json:"queries"`
OrgConfig OrganizationOpenSanctionsConfig `json:"-"`
Queries OpenSanctionCheckFilter `json:"-"`
QueryPayload []byte `json:"queries"` //nolint:tagliatelle
OrgConfig OrganizationOpenSanctionsConfig `json:"-"`
}

type SanctionCheckExecution struct {
Expand All @@ -17,10 +18,9 @@ type SanctionCheckExecution struct {
}

type SanctionCheckExecutionMatch struct {
Raw []byte
Payload []byte

Id string
Schema string
EntityId string
QueryIds []string
Datasets []string
Expand Down
4 changes: 2 additions & 2 deletions repositories/dbmodels/db_sanction_check.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ type DBSanctionCheck struct {
Status string `db:"status"`
SearchInput []byte `db:"search_input"`
SearchDatasets []string `db:"search_datasets"`
SearchThreshold int `db:"search_threshold"`
SearchThreshold *int `db:"search_threshold"`
IsManual bool `db:"is_manual"`
IsPartial bool `db:"is_partial"`
RequestedBy *string `db:"requested_by"`
Expand All @@ -31,7 +31,7 @@ type DBSanctionCheck struct {

func AdaptSanctionCheck(dto DBSanctionCheck) (models.SanctionCheckExecution, error) {
cfg := models.OrganizationOpenSanctionsConfig{
MatchThreshold: &dto.SearchThreshold,
MatchThreshold: dto.SearchThreshold,
Datasets: dto.SearchDatasets,
}

Expand Down
21 changes: 12 additions & 9 deletions repositories/dbmodels/db_sanction_check_match.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package dbmodels

import (
"encoding/json"
"time"

"github.com/checkmarble/marble-backend/models"
Expand All @@ -12,21 +13,23 @@ const TABLE_SANCTION_CHECK_MATCHES = "sanction_check_matches"
var SelectSanctionCheckMatchesColumn = utils.ColumnList[DBSanctionCheckMatch]()

type DBSanctionCheckMatch struct {
Id string `db:"id"`
SanctionCheckId string `db:"sanction_check_id"`
OpenSanctionEntityId string `db:"opensanction_entity_id"`
Status string `db:"status"`
QueryIds []string `db:"query_ids"`
Payload []byte `db:"payload"`
ReviewedBy *string `db:"reviewed_by"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
Id string `db:"id"`
SanctionCheckId string `db:"sanction_check_id"`
OpenSanctionEntityId string `db:"opensanction_entity_id"`
Status string `db:"status"`
QueryIds []string `db:"query_ids"`
Payload json.RawMessage `db:"payload"`
ReviewedBy *string `db:"reviewed_by"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}

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

return match, nil
Expand Down
3 changes: 1 addition & 2 deletions repositories/httpmodels/http_opensanctions_result.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,8 @@ func AdaptOpenSanctionsResult(query models.OpenSanctionsQuery, result HTTPOpenSa

if _, ok := matches[parsed.Id]; !ok {
entity := models.SanctionCheckExecutionMatch{
Raw: match,
Payload: match,
EntityId: parsed.Id,
Schema: parsed.Schema,
Datasets: parsed.Datasets,
}

Expand Down
17 changes: 13 additions & 4 deletions repositories/opensanctions_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func (repo OpenSanctionsRepository) Search(ctx context.Context,
cfg models.SanctionCheckConfig,
query models.OpenSanctionsQuery,
) (models.SanctionCheckExecution, error) {
req, err := repo.searchRequest(ctx, query)
req, queryPayload, err := repo.searchRequest(ctx, query)
if err != nil {
return models.SanctionCheckExecution{}, err
}
Expand All @@ -60,12 +60,20 @@ func (repo OpenSanctionsRepository) Search(ctx context.Context,
"could not parse sanction check response")
}

var payload bytes.Buffer

if err := json.NewEncoder(&payload).Encode(queryPayload); err != nil {
return models.SanctionCheckExecution{}, errors.Wrap(err, "could not encode query")
}

query.QueryPayload = payload.Bytes()

return httpmodels.AdaptOpenSanctionsResult(query, matches)
}

func (repo OpenSanctionsRepository) searchRequest(ctx context.Context,
query models.OpenSanctionsQuery,
) (*http.Request, error) {
) (*http.Request, openSanctionsRequest, error) {
q := openSanctionsRequest{
Queries: make(map[string]openSanctionsRequestQuery, len(query.Queries)),
}
Expand All @@ -80,7 +88,8 @@ func (repo OpenSanctionsRepository) searchRequest(ctx context.Context,
var body bytes.Buffer

if err := json.NewEncoder(&body).Encode(q); err != nil {
return nil, errors.Wrap(err, "could not parse OpenSanctions response")
return nil, openSanctionsRequest{}, errors.Wrap(err,
"could not parse OpenSanctions response")
}

requestUrl := fmt.Sprintf("%s/match/sanctions", repo.opensanctions.Host())
Expand All @@ -91,7 +100,7 @@ func (repo OpenSanctionsRepository) searchRequest(ctx context.Context,

req, err := http.NewRequestWithContext(ctx, http.MethodPost, requestUrl, &body)

return req, err
return req, q, err
}

func (repo OpenSanctionsRepository) buildQueryString(orgCfg models.OrganizationOpenSanctionsConfig) url.Values {
Expand Down
36 changes: 34 additions & 2 deletions repositories/sanction_check_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,51 @@ import (
"fmt"
"strings"

"github.com/Masterminds/squirrel"
"github.com/checkmarble/marble-backend/models"
"github.com/checkmarble/marble-backend/repositories/dbmodels"
"github.com/checkmarble/marble-backend/utils"
)

func (*MarbleDbRepository) InsertResults(ctx context.Context, exec Executor,
func (*MarbleDbRepository) ListSanctionChecksForDecision(ctx context.Context, exec Executor,
decisionId string,
) ([]models.SanctionCheckExecution, error) {
if err := validateMarbleDbExecutor(exec); err != nil {
return nil, err
}

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

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

func (*MarbleDbRepository) ListSanctionCheckMatches(ctx context.Context, exec Executor,
sanctionCheckId string,
) ([]models.SanctionCheckExecutionMatch, error) {
if err := validateMarbleDbExecutor(exec); err != nil {
return nil, err
}

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

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

func (*MarbleDbRepository) InsertSanctionCheck(ctx context.Context, exec Executor,
decision models.DecisionWithRuleExecutions,
) (models.SanctionCheckExecution, error) {
utils.LoggerFromContext(ctx).Debug("SANCTION CHECK: inserting matches in database")

if err := validateMarbleDbExecutor(exec); err != nil {
return *decision.SanctionCheckExecution, err
}

sql := NewQueryBuilder().
Insert(dbmodels.TABLE_SANCTION_CHECKS).Columns(
"decision_id",
Expand Down Expand Up @@ -47,7 +79,7 @@ func (*MarbleDbRepository) InsertResults(ctx context.Context, exec Executor,
Suffix(fmt.Sprintf("RETURNING %s", strings.Join(dbmodels.SelectSanctionCheckMatchesColumn, ",")))

for _, match := range decision.SanctionCheckExecution.Matches {
matchSql = matchSql.Values(result.Id, match.EntityId, match.QueryIds, match.Raw)
matchSql = matchSql.Values(result.Id, match.EntityId, match.QueryIds, match.Payload)
}

matches, err := SqlToListOfModels(ctx, exec, matchSql, dbmodels.AdaptSanctionCheckMatch)
Expand Down
2 changes: 1 addition & 1 deletion usecases/decision_usecase.go
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,7 @@ func (usecase *DecisionUsecase) CreateDecision(
}

if decision.SanctionCheckExecution != nil {
if _, err := usecase.sanctionCheckUsecase.repository.InsertResults(ctx, tx,
if _, err := usecase.sanctionCheckUsecase.repository.InsertSanctionCheck(ctx, tx,
decision); err != nil {
return models.DecisionWithRuleExecutions{},
errors.Wrap(err, "could not store sanction check execution")
Expand Down
51 changes: 49 additions & 2 deletions usecases/sanction_check_usecase.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/checkmarble/marble-backend/models"
"github.com/checkmarble/marble-backend/repositories"
"github.com/checkmarble/marble-backend/usecases/executor_factory"
"github.com/checkmarble/marble-backend/usecases/security"
"github.com/pkg/errors"
)

Expand All @@ -14,17 +15,63 @@ type SanctionCheckProvider interface {
models.OpenSanctionsQuery) (models.SanctionCheckExecution, error)
}

type SanctionCheckDecisionRepository interface {
DecisionsById(ctx context.Context, exec repositories.Executor, decisionIds []string) ([]models.Decision, error)
}

type SanctionCheckRepository interface {
InsertResults(context.Context, repositories.Executor, models.DecisionWithRuleExecutions) (models.SanctionCheckExecution, error)
ListSanctionChecksForDecision(context.Context, repositories.Executor, string) ([]models.SanctionCheckExecution, error)
ListSanctionCheckMatches(ctx context.Context, exec repositories.Executor, sanctionCheckId string) (
[]models.SanctionCheckExecutionMatch, error)
InsertSanctionCheck(context.Context, repositories.Executor,
models.DecisionWithRuleExecutions) (models.SanctionCheckExecution, error)
}

type SanctionCheckUsecase struct {
enforceSecurityDecision security.EnforceSecurityDecision

organizationRepository repositories.OrganizationRepository
decisionRepository SanctionCheckDecisionRepository
openSanctionsProvider SanctionCheckProvider
repository SanctionCheckRepository
executorFactory executor_factory.ExecutorFactory
}

func (uc SanctionCheckUsecase) ListSanctionChecks(ctx context.Context, decisionId string) ([]models.SanctionCheckExecution, error) {
decision, err := uc.decisionRepository.DecisionsById(ctx,
uc.executorFactory.NewExecutor(), []string{decisionId})
if err != nil {
return nil, err
}
if len(decision) == 0 {
return nil, errors.Wrap(models.NotFoundError, "requested decision does not exist")
}

if err := uc.enforceSecurityDecision.ReadDecision(decision[0]); err != nil {
return nil, err
}

sanctionChecks, err := uc.repository.ListSanctionChecksForDecision(ctx,
uc.executorFactory.NewExecutor(), decision[0].DecisionId)
if err != nil {
return nil, errors.Wrap(err, "could not retrieve sanction check")
}

// TODO: anything supports nested queries?
for idx, sc := range sanctionChecks {
matches, err := uc.repository.ListSanctionCheckMatches(ctx,
uc.executorFactory.NewExecutor(), sc.Id)
if err != nil {
return nil, errors.Wrap(err, "could not retrieve sanction check matches")
}

sanctionChecks[idx].Count = len(matches)
sanctionChecks[idx].Matches = matches
}

return sanctionChecks, nil
}

func (uc SanctionCheckUsecase) Execute(ctx context.Context, orgId string, cfg models.SanctionCheckConfig,
query models.OpenSanctionsQuery,
) (models.SanctionCheckExecution, error) {
Expand All @@ -49,5 +96,5 @@ func (uc SanctionCheckUsecase) InsertResults(ctx context.Context,
exec repositories.Executor,
decision models.DecisionWithRuleExecutions,
) (models.SanctionCheckExecution, error) {
return uc.repository.InsertResults(ctx, exec, decision)
return uc.repository.InsertSanctionCheck(ctx, exec, decision)
}
Loading

0 comments on commit 07caa7c

Please sign in to comment.