//! Watch-list domain model: sanctions lists, and the PEP and adverse-media //! datasets screened beside them. //! //! Each authority publishes a slightly different schema, but they all //! agree on the same skeleton: an entity (person, organisation, vessel, //! or aircraft) carries a primary name, aliases, identifying numbers, //! addresses, and one-or-more sanctions programs it is named under. //! [`SanctionedEntity`] is the union we ingest into. A PEP or adverse-media //! entry takes the same shape, and its [`WatchList`] says it is not a //! sanctions designation. use chrono::{DateTime, NaiveDate, Utc}; use serde::{Deserialize, Serialize}; /// The list an entry came from: a sanctions authority's list, or a /// politically-exposed-person or adverse-media dataset. /// /// These are different findings, and a match records which it was. A /// sanctions match is a designation (block, report); a PEP match calls for /// enhanced due diligence and is not a designation; an adverse-media match is /// a reason to review. A match is never recorded under a list it did not come /// from: the label of an entry's list is what the hash-chained audit row /// ([`crate::AuditLog`]) states, so a PEP entry tagged with a sanctions /// authority would state a sanctions match that never happened. /// [`WatchList::is_sanctions`] tells the two apart. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum WatchList { /// US Treasury — Specially Designated Nationals (SDN). OfacSdn, /// US Treasury — Non-SDN Consolidated Sanctions list. OfacConsolidated, /// EU Council — consolidated financial sanctions list. EuConsolidated, /// UN Security Council — consolidated list. UnConsolidated, /// HM Treasury (UK) — consolidated list of financial sanctions targets. HmtUk, /// Australia DFAT — consolidated list. AustraliaDfat, /// Canada SEMA — Special Economic Measures Act regulations. CanadaSema, /// Japan Ministry of Finance. JapanMof, /// A politically-exposed-persons dataset (FATF Recommendation 12), /// commercial or operator-maintained. Not a sanctions list. Pep, /// An adverse-media dataset (negative news). Not a sanctions list. AdverseMedia, } impl WatchList { /// Human-readable short name. #[must_use] pub const fn label(self) -> &'static str { match self { Self::OfacSdn => "OFAC SDN", Self::OfacConsolidated => "OFAC Consolidated", Self::EuConsolidated => "EU Consolidated", Self::UnConsolidated => "UN Consolidated", Self::HmtUk => "HMT UK", Self::AustraliaDfat => "Australia DFAT", Self::CanadaSema => "Canada SEMA", Self::JapanMof => "Japan MOF", Self::Pep => "PEP list", Self::AdverseMedia => "Adverse media", } } /// Whether the list is a sanctions authority's: a match on it is a /// designation. False for [`WatchList::Pep`] and /// [`WatchList::AdverseMedia`]. #[must_use] pub const fn is_sanctions(self) -> bool { !matches!(self, Self::Pep | Self::AdverseMedia) } } /// The former name of [`WatchList`], from when it held only sanctions /// authorities. #[deprecated(note = "renamed `WatchList`: it also names PEP and adverse-media lists")] pub type SanctionsList = WatchList; /// What kind of thing the entry names. /// /// Sanctions programs apply to natural persons, legal entities, and to /// specific transportation assets (vessel IMO numbers, aircraft tail /// numbers) that have themselves been designated. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum EntityType { /// A natural person. Individual, /// A company, organisation, or other legal entity. Entity, /// A maritime vessel (named by IMO number and ship name). Vessel, /// An aircraft (named by registration / tail number). Aircraft, } /// ISO 3166-1 alpha-2 country code. #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct CountryCode(pub String); /// Postal / business address attached to an entry. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Address { /// Street address line(s), joined with newlines. pub street: Option, /// City or locality. pub city: Option, /// State, province, or region. pub region: Option, /// Postal code. pub postal_code: Option, /// ISO 3166-1 alpha-2 country, if known. pub country: Option, } /// The kind of identifying document or number attached to an entry. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum IdentificationKind { /// National identity card. NationalId, /// Passport. Passport, /// Driver's licence. DriversLicence, /// Tax identification number. TaxId, /// Maritime IMO number (vessels). Imo, /// Aircraft registration / tail number. AircraftTail, /// Anything not in the enum above; carry the raw label. Other, } /// A piece of identifying information published with an entry. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Identification { /// What sort of identifier this is. pub kind: IdentificationKind, /// Free-text label as published (e.g. `"Cedula No."`, `"DNI"`). pub label: Option, /// The identifier value itself. pub value: String, /// Issuing country, if known. pub country: Option, } /// A single sanctioned entity, normalised across all upstream list formats. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct SanctionedEntity { /// Stable per-list identifier (e.g. OFAC's `uid`, EU `logicalId`). pub id: String, /// Canonical primary name as published. pub name: String, /// AKAs, FKAs, and other published aliases. pub name_aliases: Vec, /// Individual / Entity / Vessel / Aircraft. pub entity_type: EntityType, /// Date of birth (individuals only). pub dob: Option, /// Place of birth, free-text (individuals only). pub place_of_birth: Option, /// Known addresses. pub addresses: Vec
, /// Nationalities / citizenships. pub nationalities: Vec, /// Published identifications. pub identifications: Vec, /// Sanctions programmes the entry sits under (e.g. `"SDGT"`, `"UKRAINE-EO13662"`). pub programs: Vec, /// When the upstream list last updated this entry. pub last_updated: DateTime, /// Which list this entry came from. A PEP or adverse-media entry names /// its own list here, never a sanctions authority's. pub source_list: WatchList, } #[cfg(test)] mod tests { use super::WatchList; /// Every list, in an order the match below keeps complete: a new list /// does not compile until it is here, and so in both tests. const ALL: [WatchList; 10] = [ WatchList::OfacSdn, WatchList::OfacConsolidated, WatchList::EuConsolidated, WatchList::UnConsolidated, WatchList::HmtUk, WatchList::AustraliaDfat, WatchList::CanadaSema, WatchList::JapanMof, WatchList::Pep, WatchList::AdverseMedia, ]; #[allow(dead_code)] fn all_is_complete(l: WatchList) -> usize { match l { WatchList::OfacSdn => 0, WatchList::OfacConsolidated => 1, WatchList::EuConsolidated => 2, WatchList::UnConsolidated => 3, WatchList::HmtUk => 4, WatchList::AustraliaDfat => 5, WatchList::CanadaSema => 6, WatchList::JapanMof => 7, WatchList::Pep => 8, WatchList::AdverseMedia => 9, } } /// The eight authorities are sanctions lists; the PEP and adverse-media /// datasets are not. #[test] fn only_the_authorities_are_sanctions_lists() { for (i, l) in ALL.into_iter().enumerate() { assert_eq!(all_is_complete(l), i); assert_eq!(l.is_sanctions(), !matches!(l, WatchList::Pep | WatchList::AdverseMedia), "{l:?}"); } } /// Each list states itself: no two share a label, which is what an /// audit row records. #[test] fn every_list_has_its_own_label() { let labels: std::collections::HashSet<_> = ALL.iter().map(|l| l.label()).collect(); assert_eq!(labels.len(), ALL.len()); } }