Skip to content

Commit

Permalink
Add endpoint to manually enrich match with full details.
Browse files Browse the repository at this point in the history
  • Loading branch information
apognu committed Feb 25, 2025
1 parent 5d45d45 commit b62f08b
Show file tree
Hide file tree
Showing 5 changed files with 122 additions and 18 deletions.
16 changes: 16 additions & 0 deletions api/handle_sanction_checks.go
Original file line number Diff line number Diff line change
Expand Up @@ -255,3 +255,19 @@ func handleSearchSanctionCheck(uc usecases.Usecases) func(c *gin.Context) {
}))
}
}

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

uc := usecasesWithCreds(ctx, uc).NewSanctionCheckUsecase()
newMatch, err := uc.EnrichMatch(ctx, matchId)

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

c.JSON(http.StatusOK, dto.AdaptSanctionCheckMatchDto(newMatch))
}
}
1 change: 1 addition & 0 deletions api/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ func addRoutes(r *gin.Engine, conf Configuration, uc usecases.Usecases, auth Aut
router.GET("/sanction-checks/:sanctionCheckId/files/:fileId", tom,
handleDownloadSanctionCheckMatchFile(uc))
router.PATCH("/sanction-checks/matches/:id", tom, handleUpdateSanctionCheckMatchStatus(uc))
router.POST("/sanction-checks/matches/:id/enrich", tom, handleEnrichSanctionCheckMatch(uc))

router.GET("/scenario-publications", tom, handleListScenarioPublications(uc))
router.POST("/scenario-publications", tom, handleCreateScenarioPublication(uc))
Expand Down
79 changes: 61 additions & 18 deletions repositories/opensanctions_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,56 @@ func (repo OpenSanctionsRepository) Search(ctx context.Context, query models.Ope
return sanctionCheck, err
}

func (repo OpenSanctionsRepository) EnrichMatch(ctx context.Context, match models.SanctionCheckMatch) ([]byte, error) {
requestUrl := fmt.Sprintf("%s/entities/%s", repo.opensanctions.Host(), match.EntityId)

if qs := repo.buildQueryString(nil, nil); len(qs) > 0 {
requestUrl = fmt.Sprintf("%s?%s", requestUrl, qs.Encode())
}

req, err := http.NewRequest(http.MethodGet, requestUrl, nil)
if err != nil {
return nil, err
}

repo.authenticateRequest(req)

resp, err := repo.opensanctions.Client().Do(req)
if err != nil {
return nil,
errors.Wrap(err, "could not enrich sanction check match")
}

if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf(
"sanction check API returned status %d on enrichment", resp.StatusCode)
}

defer resp.Body.Close()

var newMatch json.RawMessage

if err := json.NewDecoder(resp.Body).Decode(&newMatch); err != nil {
return nil, errors.Wrap(err,
"could not parse sanction check response")
}

return newMatch, nil
}

func (repo OpenSanctionsRepository) authenticateRequest(req *http.Request) {
if repo.opensanctions.IsSelfHosted() {
switch repo.opensanctions.AuthMethod() {
case infra.OPEN_SANCTIONS_AUTH_BEARER:
req.Header.Set("authorization", "Bearer "+repo.opensanctions.Credentials())
case infra.OPEN_SANCTIONS_AUTH_BASIC:
u, p, _ := strings.Cut(repo.opensanctions.Credentials(), ":")

req.SetBasicAuth(u, p)
}
}
}

func (repo OpenSanctionsRepository) searchRequest(ctx context.Context,
query models.OpenSanctionsQuery,
) (*http.Request, []byte, error) {
Expand All @@ -242,44 +292,37 @@ func (repo OpenSanctionsRepository) searchRequest(ctx context.Context,

requestUrl := fmt.Sprintf("%s/match/sanctions", repo.opensanctions.Host())

if qs := repo.buildQueryString(query.Config, query); len(qs) > 0 {
if qs := repo.buildQueryString(&query.Config, &query); len(qs) > 0 {
requestUrl = fmt.Sprintf("%s?%s", requestUrl, qs.Encode())
}

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

if repo.opensanctions.IsSelfHosted() {
switch repo.opensanctions.AuthMethod() {
case infra.OPEN_SANCTIONS_AUTH_BEARER:
req.Header.Set("authorization", "Bearer "+repo.opensanctions.Credentials())
case infra.OPEN_SANCTIONS_AUTH_BASIC:
u, p, _ := strings.Cut(repo.opensanctions.Credentials(), ":")

req.SetBasicAuth(u, p)
}
}
repo.authenticateRequest(req)

return req, rawQuery.Bytes(), err
}

func (repo OpenSanctionsRepository) buildQueryString(cfg models.SanctionCheckConfig, query models.OpenSanctionsQuery) url.Values {
func (repo OpenSanctionsRepository) buildQueryString(cfg *models.SanctionCheckConfig, query *models.OpenSanctionsQuery) url.Values {
qs := url.Values{}

if repo.opensanctions.AuthMethod() == infra.OPEN_SANCTIONS_AUTH_SAAS &&
len(repo.opensanctions.Credentials()) > 0 {
qs.Set("api_key", repo.opensanctions.Credentials())
}

if len(cfg.Datasets) > 0 {
if cfg != nil && len(cfg.Datasets) > 0 {
qs["include_dataset"] = cfg.Datasets
}

// Unless determined otherwise, we do not need those results that are *not*
// matches. They could still be filtered further down the chain, but we do not need them returned.
qs.Set("threshold", fmt.Sprintf("%.1f", float64(query.OrgConfig.MatchThreshold)/100))
qs.Set("cutoff", fmt.Sprintf("%.1f", float64(query.OrgConfig.MatchThreshold)/100))
if query != nil {
// Unless determined otherwise, we do not need those results that are *not*
// matches. They could still be filtered further down the chain, but we do not need them returned.
qs.Set("threshold", fmt.Sprintf("%.1f", float64(query.OrgConfig.MatchThreshold)/100))
qs.Set("cutoff", fmt.Sprintf("%.1f", float64(query.OrgConfig.MatchThreshold)/100))

qs.Set("limit", fmt.Sprintf("%d", query.OrgConfig.MatchLimit+query.LimitIncrease))
qs.Set("limit", fmt.Sprintf("%d", query.OrgConfig.MatchLimit+query.LimitIncrease))
}

return qs
}
17 changes: 17 additions & 0 deletions repositories/sanction_check_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,23 @@ func (*MarbleDbRepository) UpdateSanctionCheckStatus(ctx context.Context, exec E
)
}

func (*MarbleDbRepository) UpdateSanctionCheckMatchPayload(ctx context.Context, exec Executor,
match models.SanctionCheckMatch, newPayload []byte,
) (models.SanctionCheckMatch, error) {
if err := validateMarbleDbExecutor(exec); err != nil {
return models.SanctionCheckMatch{}, err
}

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

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

func (*MarbleDbRepository) ListSanctionCheckMatches(
ctx context.Context,
exec Executor,
Expand Down
27 changes: 27 additions & 0 deletions usecases/sanction_check_usecase.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type SanctionCheckProvider interface {
GetCatalog(ctx context.Context) (models.OpenSanctionsCatalog, error)
GetLatestLocalDataset(context.Context) (models.OpenSanctionsDatasetFreshness, error)
Search(context.Context, models.OpenSanctionsQuery) (models.SanctionRawSearchResponseWithMatches, error)
EnrichMatch(ctx context.Context, match models.SanctionCheckMatch) ([]byte, error)
}

type SanctionCheckInboxReader interface {
Expand Down Expand Up @@ -84,6 +85,8 @@ type SanctionCheckRepository interface {
orgId, counterpartyId string, entityId []string) ([]models.SanctionCheckWhitelist, error)
CountWhitelistsForCounterpartyId(ctx context.Context, exec repositories.Executor,
orgId, counterpartyId string) (int, error)
UpdateSanctionCheckMatchPayload(ctx context.Context, exec repositories.Executor,
match models.SanctionCheckMatch, newPayload []byte) (models.SanctionCheckMatch, error)
}

type SanctionsCheckUsecaseExternalRepository interface {
Expand Down Expand Up @@ -478,6 +481,30 @@ func (uc SanctionCheckUsecase) MatchAddComment(ctx context.Context, matchId stri
return uc.repository.AddSanctionCheckMatchComment(ctx, uc.executorFactory.NewExecutor(), comment)
}

func (uc SanctionCheckUsecase) EnrichMatch(ctx context.Context, matchId string) (models.SanctionCheckMatch, error) {
if _, err := uc.enforceCanReadOrUpdateSanctionCheckMatch(ctx, matchId); err != nil {
return models.SanctionCheckMatch{}, err
}

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

newPayload, err := uc.openSanctionsProvider.EnrichMatch(ctx, match)
if err != nil {
return models.SanctionCheckMatch{}, err
}

newMatch, err := uc.repository.UpdateSanctionCheckMatchPayload(ctx,
uc.executorFactory.NewExecutor(), match, newPayload)
if err != nil {
return models.SanctionCheckMatch{}, err
}

return newMatch, nil
}

func (uc SanctionCheckUsecase) CreateFiles(ctx context.Context, creds models.Credentials,
sanctionCheckId string, files []multipart.FileHeader,
) ([]models.SanctionCheckFile, error) {
Expand Down

0 comments on commit b62f08b

Please sign in to comment.