diff --git a/api/handle_sanction_checks.go b/api/handle_sanction_checks.go new file mode 100644 index 00000000..324c3684 --- /dev/null +++ b/api/handle_sanction_checks.go @@ -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) + } +} diff --git a/api/routes.go b/api/routes.go index 0447a76a..dbe13dc5 100644 --- a/api/routes.go +++ b/api/routes.go @@ -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, diff --git a/dto/sanction_check_dto.go b/dto/sanction_check_dto.go new file mode 100644 index 00000000..d90bb547 --- /dev/null +++ b/dto/sanction_check_dto.go @@ -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 +} diff --git a/models/sanction_check.go b/models/sanction_check.go index 6e249a59..acef3c78 100644 --- a/models/sanction_check.go +++ b/models/sanction_check.go @@ -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 { @@ -17,10 +18,9 @@ type SanctionCheckExecution struct { } type SanctionCheckExecutionMatch struct { - Raw []byte + Payload []byte Id string - Schema string EntityId string QueryIds []string Datasets []string diff --git a/repositories/dbmodels/db_sanction_check.go b/repositories/dbmodels/db_sanction_check.go index df5801ad..3e0f5f00 100644 --- a/repositories/dbmodels/db_sanction_check.go +++ b/repositories/dbmodels/db_sanction_check.go @@ -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"` @@ -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, } diff --git a/repositories/dbmodels/db_sanction_check_match.go b/repositories/dbmodels/db_sanction_check_match.go index 43f205a6..4b90e34f 100644 --- a/repositories/dbmodels/db_sanction_check_match.go +++ b/repositories/dbmodels/db_sanction_check_match.go @@ -1,6 +1,7 @@ package dbmodels import ( + "encoding/json" "time" "github.com/checkmarble/marble-backend/models" @@ -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 diff --git a/repositories/httpmodels/http_opensanctions_result.go b/repositories/httpmodels/http_opensanctions_result.go index a38d781a..81bd1938 100644 --- a/repositories/httpmodels/http_opensanctions_result.go +++ b/repositories/httpmodels/http_opensanctions_result.go @@ -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, } diff --git a/repositories/opensanctions_repository.go b/repositories/opensanctions_repository.go index 63b980a2..688bbd9f 100644 --- a/repositories/opensanctions_repository.go +++ b/repositories/opensanctions_repository.go @@ -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 } @@ -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)), } @@ -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()) @@ -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 { diff --git a/repositories/sanction_check_repository.go b/repositories/sanction_check_repository.go index 0ebb470a..34cb3f9b 100644 --- a/repositories/sanction_check_repository.go +++ b/repositories/sanction_check_repository.go @@ -5,12 +5,43 @@ 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") @@ -18,6 +49,7 @@ func (*MarbleDbRepository) InsertResults(ctx context.Context, exec Executor, if err := validateMarbleDbExecutor(exec); err != nil { return *decision.SanctionCheckExecution, err } + sql := NewQueryBuilder(). Insert(dbmodels.TABLE_SANCTION_CHECKS).Columns( "decision_id", @@ -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) diff --git a/usecases/decision_usecase.go b/usecases/decision_usecase.go index c6f9d4f5..abd343d0 100644 --- a/usecases/decision_usecase.go +++ b/usecases/decision_usecase.go @@ -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") diff --git a/usecases/sanction_check_usecase.go b/usecases/sanction_check_usecase.go index d8d3f6fc..b0a2a6e8 100644 --- a/usecases/sanction_check_usecase.go +++ b/usecases/sanction_check_usecase.go @@ -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" ) @@ -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) { @@ -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) } diff --git a/usecases/usecases_with_creds.go b/usecases/usecases_with_creds.go index 11bfc1ba..7cf4915e 100644 --- a/usecases/usecases_with_creds.go +++ b/usecases/usecases_with_creds.go @@ -119,10 +119,12 @@ func (usecases *UsecasesWithCreds) NewDecisionUsecase() DecisionUsecase { func (usecases *UsecasesWithCreds) NewSanctionCheckUsecase() SanctionCheckUsecase { return SanctionCheckUsecase{ - organizationRepository: usecases.Repositories.OrganizationRepository, - openSanctionsProvider: usecases.Repositories.OpenSanctionsRepository, - repository: &usecases.Repositories.MarbleDbRepository, - executorFactory: usecases.NewExecutorFactory(), + enforceSecurityDecision: usecases.NewEnforceDecisionSecurity(), + organizationRepository: usecases.Repositories.OrganizationRepository, + decisionRepository: &usecases.Repositories.MarbleDbRepository, + openSanctionsProvider: usecases.Repositories.OpenSanctionsRepository, + repository: &usecases.Repositories.MarbleDbRepository, + executorFactory: usecases.NewExecutorFactory(), } }