feat: implement sanctions screening vertical
This commit is contained in:
@@ -0,0 +1,340 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date, datetime
|
||||
from typing import Any, Literal
|
||||
|
||||
from pydantic import BaseModel, Field, model_validator
|
||||
|
||||
|
||||
class ConnectorSnapshotResponse(BaseModel):
|
||||
ref: str
|
||||
provider_id: str
|
||||
publisher: str
|
||||
jurisdiction: str
|
||||
list_type: str
|
||||
source_id: str
|
||||
source_version: str
|
||||
publication_at: datetime | None
|
||||
effective_at: datetime | None
|
||||
acquired_at: datetime
|
||||
byte_count: int
|
||||
sha256: str
|
||||
parser_version: str
|
||||
raw_evidence_ref: str
|
||||
connector_run_id: str
|
||||
licence_notes: str | None = None
|
||||
trust_notes: str | None = None
|
||||
|
||||
|
||||
class ConnectorSnapshotListResponse(BaseModel):
|
||||
available: bool
|
||||
snapshots: list[ConnectorSnapshotResponse]
|
||||
|
||||
|
||||
class ListSnapshotImportRequest(BaseModel):
|
||||
connector_snapshot_ref: str = Field(
|
||||
min_length=1,
|
||||
max_length=300,
|
||||
)
|
||||
|
||||
|
||||
class ListSnapshotResponse(BaseModel):
|
||||
id: str
|
||||
visibility: str
|
||||
provider_id: str
|
||||
publisher: str
|
||||
jurisdiction: str
|
||||
list_type: str
|
||||
source_id: str
|
||||
source_version: str
|
||||
publication_at: datetime | None
|
||||
effective_at: datetime | None
|
||||
acquired_at: datetime
|
||||
sha256: str
|
||||
connector_run_id: str
|
||||
raw_evidence_ref: str
|
||||
source_parser_version: str
|
||||
normalization_version: str
|
||||
signature_evidence: dict[str, Any]
|
||||
provenance: dict[str, Any]
|
||||
entry_count: int
|
||||
status: str
|
||||
imported_by: str | None
|
||||
imported_at: datetime
|
||||
|
||||
|
||||
class ListSnapshotImportResponse(BaseModel):
|
||||
snapshot: ListSnapshotResponse
|
||||
created: bool
|
||||
|
||||
|
||||
class ListSnapshotListResponse(BaseModel):
|
||||
snapshots: list[ListSnapshotResponse]
|
||||
|
||||
|
||||
class ScreeningIdentifierInput(BaseModel):
|
||||
type: str = Field(default="document", max_length=100)
|
||||
value: str = Field(min_length=1, max_length=1000)
|
||||
|
||||
|
||||
class ScreeningAddressInput(BaseModel):
|
||||
street: str | None = Field(default=None, max_length=1000)
|
||||
city: str | None = Field(default=None, max_length=500)
|
||||
region: str | None = Field(default=None, max_length=500)
|
||||
postal_code: str | None = Field(default=None, max_length=100)
|
||||
country: str | None = Field(default=None, max_length=255)
|
||||
|
||||
|
||||
class ScreeningSubjectInput(BaseModel):
|
||||
subject_type: Literal["person", "entity"]
|
||||
subject_ref: str | None = Field(default=None, max_length=500)
|
||||
primary_name: str | None = Field(default=None, max_length=1000)
|
||||
aliases: list[str] = Field(default_factory=list, max_length=100)
|
||||
identifiers: list[ScreeningIdentifierInput] = Field(
|
||||
default_factory=list,
|
||||
max_length=100,
|
||||
)
|
||||
dates: list[str] = Field(default_factory=list, max_length=100)
|
||||
addresses: list[ScreeningAddressInput] = Field(
|
||||
default_factory=list,
|
||||
max_length=100,
|
||||
)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_lookup_fields(self) -> "ScreeningSubjectInput":
|
||||
if not str(self.primary_name or "").strip() and not self.identifiers:
|
||||
raise ValueError("A name or identifier is required.")
|
||||
return self
|
||||
|
||||
|
||||
class ScreeningPolicyInput(BaseModel):
|
||||
fuzzy_threshold: float = Field(default=0.88, ge=0.8, le=1)
|
||||
max_snapshot_age_days: int = Field(default=7, ge=1, le=365)
|
||||
max_candidates: int = Field(default=100, ge=1, le=100)
|
||||
|
||||
|
||||
class ScreeningRunCreateRequest(BaseModel):
|
||||
list_snapshot_id: str = Field(min_length=1, max_length=36)
|
||||
idempotency_key: str = Field(min_length=1, max_length=255)
|
||||
subject: ScreeningSubjectInput
|
||||
policy: ScreeningPolicyInput = Field(
|
||||
default_factory=ScreeningPolicyInput
|
||||
)
|
||||
|
||||
|
||||
class SubjectSnapshotResponse(BaseModel):
|
||||
id: str
|
||||
subject_ref: str | None
|
||||
subject_type: str
|
||||
primary_name: str | None
|
||||
normalized_name: str | None
|
||||
aliases: list[str]
|
||||
identifiers: list[dict[str, str]]
|
||||
dates: list[str]
|
||||
addresses: list[dict[str, str]]
|
||||
fingerprint: str
|
||||
submitted_by: str | None
|
||||
|
||||
|
||||
class SanctionsAliasResponse(BaseModel):
|
||||
name: str
|
||||
normalized_name: str
|
||||
quality: str | None
|
||||
alias_type: str
|
||||
|
||||
|
||||
class SanctionsIdentifierResponse(BaseModel):
|
||||
identifier_type: str
|
||||
value: str
|
||||
issuing_country: str | None
|
||||
note: str | None
|
||||
|
||||
|
||||
class SanctionsDateResponse(BaseModel):
|
||||
date_type: str
|
||||
value: str
|
||||
precision: str
|
||||
|
||||
|
||||
class SanctionsAddressResponse(BaseModel):
|
||||
street: str | None
|
||||
city: str | None
|
||||
region: str | None
|
||||
postal_code: str | None
|
||||
country: str | None
|
||||
note: str | None
|
||||
|
||||
|
||||
class SanctionsEntryResponse(BaseModel):
|
||||
id: str
|
||||
source_entry_id: str
|
||||
subject_type: str
|
||||
primary_name: str
|
||||
normalized_name: str
|
||||
original_script_name: str | None
|
||||
reference_number: str | None
|
||||
listed_on: date | None
|
||||
programmes: list[str]
|
||||
measures: list[str]
|
||||
raw_evidence_locator: str
|
||||
details: dict[str, Any]
|
||||
aliases: list[SanctionsAliasResponse]
|
||||
identifiers: list[SanctionsIdentifierResponse]
|
||||
dates: list[SanctionsDateResponse]
|
||||
addresses: list[SanctionsAddressResponse]
|
||||
|
||||
|
||||
class CandidateResponse(BaseModel):
|
||||
id: str
|
||||
score: int
|
||||
match_kind: str
|
||||
evidence: list[dict[str, Any]]
|
||||
review_status: str
|
||||
current_disposition_id: str | None
|
||||
entry: SanctionsEntryResponse
|
||||
|
||||
|
||||
class ScreeningRunSummaryResponse(BaseModel):
|
||||
id: str
|
||||
subject_name: str | None
|
||||
subject_type: str
|
||||
list_snapshot_id: str
|
||||
list_source_version: str
|
||||
matcher_version: str
|
||||
normalization_version: str
|
||||
policy_version: str
|
||||
status: str
|
||||
outcome: str
|
||||
candidate_count: int
|
||||
started_at: datetime
|
||||
completed_at: datetime | None
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class ScreeningRunResponse(BaseModel):
|
||||
id: str
|
||||
idempotency_key: str
|
||||
request_hash: str
|
||||
matcher_version: str
|
||||
normalization_version: str
|
||||
policy_version: str
|
||||
policy_snapshot: dict[str, Any]
|
||||
status: str
|
||||
outcome: str
|
||||
candidate_count: int
|
||||
started_at: datetime
|
||||
completed_at: datetime | None
|
||||
created_by: str | None
|
||||
created_at: datetime
|
||||
subject: SubjectSnapshotResponse
|
||||
list_snapshot: ListSnapshotResponse
|
||||
candidates: list[CandidateResponse]
|
||||
|
||||
|
||||
class ScreeningRunCreateResponse(BaseModel):
|
||||
run: ScreeningRunResponse
|
||||
created: bool
|
||||
|
||||
|
||||
class ScreeningRunListResponse(BaseModel):
|
||||
runs: list[ScreeningRunSummaryResponse]
|
||||
|
||||
|
||||
class ReviewQueueItemResponse(BaseModel):
|
||||
id: str
|
||||
run_id: str
|
||||
score: int
|
||||
match_kind: str
|
||||
review_status: str
|
||||
subject_name: str | None
|
||||
subject_type: str
|
||||
entry_name: str
|
||||
source_entry_id: str
|
||||
list_source_version: str
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class ReviewQueueResponse(BaseModel):
|
||||
candidates: list[ReviewQueueItemResponse]
|
||||
|
||||
|
||||
class CandidateDetailResponse(BaseModel):
|
||||
candidate: CandidateResponse
|
||||
run: ScreeningRunSummaryResponse
|
||||
subject: SubjectSnapshotResponse
|
||||
list_snapshot: ListSnapshotResponse
|
||||
|
||||
|
||||
class DispositionCreateRequest(BaseModel):
|
||||
decision: Literal[
|
||||
"true_match",
|
||||
"false_positive",
|
||||
"needs_information",
|
||||
"escalated",
|
||||
]
|
||||
reason: str = Field(min_length=3, max_length=10_000)
|
||||
evidence_refs: list[str] = Field(
|
||||
default_factory=list,
|
||||
max_length=100,
|
||||
)
|
||||
exception_scope: Literal[
|
||||
"candidate",
|
||||
"subject_entry",
|
||||
] = "candidate"
|
||||
expires_at: datetime | None = None
|
||||
review_at: datetime | None = None
|
||||
override_reason: str | None = Field(
|
||||
default=None,
|
||||
max_length=10_000,
|
||||
)
|
||||
|
||||
|
||||
class DispositionResponse(BaseModel):
|
||||
id: str
|
||||
candidate_id: str
|
||||
decision: str
|
||||
reason: str
|
||||
evidence_refs: list[str]
|
||||
scope: str
|
||||
expires_at: datetime | None
|
||||
review_at: datetime | None
|
||||
actor_account_id: str | None
|
||||
actor_membership_id: str | None
|
||||
actor_authority: dict[str, Any]
|
||||
separation_status: str
|
||||
override_reason: str | None
|
||||
supersedes_id: str | None
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class DispositionCreateResponse(BaseModel):
|
||||
candidate: CandidateResponse
|
||||
disposition: DispositionResponse
|
||||
|
||||
|
||||
class DispositionListResponse(BaseModel):
|
||||
dispositions: list[DispositionResponse]
|
||||
|
||||
|
||||
__all__ = [
|
||||
"CandidateDetailResponse",
|
||||
"CandidateResponse",
|
||||
"ConnectorSnapshotListResponse",
|
||||
"ConnectorSnapshotResponse",
|
||||
"DispositionCreateRequest",
|
||||
"DispositionCreateResponse",
|
||||
"DispositionListResponse",
|
||||
"DispositionResponse",
|
||||
"ListSnapshotImportRequest",
|
||||
"ListSnapshotImportResponse",
|
||||
"ListSnapshotListResponse",
|
||||
"ListSnapshotResponse",
|
||||
"ReviewQueueItemResponse",
|
||||
"ReviewQueueResponse",
|
||||
"ScreeningRunCreateRequest",
|
||||
"ScreeningRunCreateResponse",
|
||||
"ScreeningRunListResponse",
|
||||
"ScreeningRunResponse",
|
||||
"ScreeningRunSummaryResponse",
|
||||
"SubjectSnapshotResponse",
|
||||
]
|
||||
Reference in New Issue
Block a user