Skip to main content

datasynth_banking/models/
beneficial_owner.rs

1//! Beneficial ownership structures for KYC/AML.
2
3use chrono::NaiveDate;
4use rust_decimal::Decimal;
5use serde::{Deserialize, Serialize};
6use uuid::Uuid;
7
8/// A beneficial owner (UBO) of an entity.
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct BeneficialOwner {
11    /// Unique beneficial owner identifier
12    pub ubo_id: Uuid,
13    /// Name of beneficial owner
14    pub name: String,
15    /// Date of birth (for individuals)
16    pub date_of_birth: Option<NaiveDate>,
17    /// Country of residence
18    pub country_of_residence: String,
19    /// Country of citizenship
20    pub citizenship_country: Option<String>,
21    /// Ownership percentage
22    #[serde(with = "rust_decimal::serde::str")]
23    pub ownership_percentage: Decimal,
24    /// Control type
25    pub control_type: ControlType,
26    /// Is this a direct or indirect owner
27    pub is_direct: bool,
28    /// Intermediary entity (if indirect)
29    pub intermediary_entity: Option<IntermediaryEntity>,
30    /// Is a PEP
31    pub is_pep: bool,
32    /// Is on sanctions list
33    pub is_sanctioned: bool,
34    /// Verification status
35    pub verification_status: VerificationStatus,
36    /// Verification date
37    pub verification_date: Option<NaiveDate>,
38    /// Source of wealth
39    pub source_of_wealth: Option<String>,
40
41    // Ground truth
42    /// Whether this is a true UBO (vs nominee)
43    pub is_true_ubo: bool,
44    /// Hidden behind shell structure
45    pub is_hidden: bool,
46}
47
48impl BeneficialOwner {
49    /// Create a new beneficial owner.
50    pub fn new(ubo_id: Uuid, name: &str, country: &str, ownership_percentage: Decimal) -> Self {
51        Self {
52            ubo_id,
53            name: name.to_string(),
54            date_of_birth: None,
55            country_of_residence: country.to_string(),
56            citizenship_country: Some(country.to_string()),
57            ownership_percentage,
58            control_type: ControlType::OwnershipInterest,
59            is_direct: true,
60            intermediary_entity: None,
61            is_pep: false,
62            is_sanctioned: false,
63            verification_status: VerificationStatus::Verified,
64            verification_date: None,
65            source_of_wealth: None,
66            is_true_ubo: true,
67            is_hidden: false,
68        }
69    }
70
71    /// Set as indirect ownership.
72    pub fn with_intermediary(mut self, intermediary: IntermediaryEntity) -> Self {
73        self.is_direct = false;
74        self.intermediary_entity = Some(intermediary);
75        self
76    }
77
78    /// Set as PEP.
79    pub fn as_pep(mut self) -> Self {
80        self.is_pep = true;
81        self
82    }
83
84    /// Mark as nominee (not true UBO).
85    pub fn as_nominee(mut self) -> Self {
86        self.is_true_ubo = false;
87        self
88    }
89
90    /// Mark as hidden behind shell structure.
91    pub fn as_hidden(mut self) -> Self {
92        self.is_hidden = true;
93        self.is_true_ubo = false;
94        self
95    }
96
97    /// Calculate risk score.
98    pub fn calculate_risk_score(&self) -> u8 {
99        let mut score = 0.0;
100
101        // Ownership level risk
102        let ownership_f64: f64 = self.ownership_percentage.try_into().unwrap_or(0.0);
103        if ownership_f64 >= 25.0 {
104            score += 20.0;
105        } else if ownership_f64 >= 10.0 {
106            score += 10.0;
107        }
108
109        // Control type risk
110        score += self.control_type.risk_weight() * 10.0;
111
112        // Indirect ownership risk
113        if !self.is_direct {
114            score += 15.0;
115        }
116
117        // PEP risk
118        if self.is_pep {
119            score += 25.0;
120        }
121
122        // Sanctions risk
123        if self.is_sanctioned {
124            score += 50.0;
125        }
126
127        // Verification status
128        if self.verification_status != VerificationStatus::Verified {
129            score += 10.0;
130        }
131
132        // Hidden structure risk
133        if self.is_hidden {
134            score += 30.0;
135        }
136
137        score.min(100.0) as u8
138    }
139}
140
141/// Type of control over entity.
142#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
143#[serde(rename_all = "snake_case")]
144pub enum ControlType {
145    /// Direct ownership interest
146    #[default]
147    OwnershipInterest,
148    /// Voting rights
149    VotingRights,
150    /// Board control
151    BoardControl,
152    /// Management control
153    ManagementControl,
154    /// Contract-based control
155    ContractualControl,
156    /// Family relationship control
157    FamilyRelationship,
158    /// Trust arrangement
159    TrustArrangement,
160    /// Nominee arrangement
161    NomineeArrangement,
162}
163
164impl ControlType {
165    /// Risk weight for AML scoring.
166    pub fn risk_weight(&self) -> f64 {
167        match self {
168            Self::OwnershipInterest => 1.0,
169            Self::VotingRights => 1.1,
170            Self::BoardControl | Self::ManagementControl => 1.2,
171            Self::ContractualControl => 1.4,
172            Self::FamilyRelationship => 1.3,
173            Self::TrustArrangement => 1.6,
174            Self::NomineeArrangement => 2.0,
175        }
176    }
177}
178
179/// Verification status for UBO.
180#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
181#[serde(rename_all = "snake_case")]
182pub enum VerificationStatus {
183    /// Fully verified
184    #[default]
185    Verified,
186    /// Partially verified
187    PartiallyVerified,
188    /// Pending verification
189    Pending,
190    /// Unable to verify
191    UnableToVerify,
192    /// Verification expired
193    Expired,
194}
195
196/// Intermediary entity in ownership chain.
197#[derive(Debug, Clone, Serialize, Deserialize)]
198pub struct IntermediaryEntity {
199    /// Entity identifier
200    pub entity_id: Uuid,
201    /// Entity name
202    pub name: String,
203    /// Entity type
204    pub entity_type: IntermediaryType,
205    /// Jurisdiction (country)
206    pub jurisdiction: String,
207    /// Ownership percentage through this entity
208    #[serde(with = "rust_decimal::serde::str")]
209    pub ownership_percentage: Decimal,
210    /// Is this a shell company
211    pub is_shell: bool,
212    /// Registration number
213    pub registration_number: Option<String>,
214}
215
216impl IntermediaryEntity {
217    /// Create a new intermediary entity.
218    pub fn new(
219        entity_id: Uuid,
220        name: &str,
221        entity_type: IntermediaryType,
222        jurisdiction: &str,
223        ownership_percentage: Decimal,
224    ) -> Self {
225        Self {
226            entity_id,
227            name: name.to_string(),
228            entity_type,
229            jurisdiction: jurisdiction.to_string(),
230            ownership_percentage,
231            is_shell: false,
232            registration_number: None,
233        }
234    }
235
236    /// Mark as shell company.
237    pub fn as_shell(mut self) -> Self {
238        self.is_shell = true;
239        self
240    }
241}
242
243/// Type of intermediary entity.
244#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
245#[serde(rename_all = "snake_case")]
246pub enum IntermediaryType {
247    /// Holding company
248    HoldingCompany,
249    /// Special purpose vehicle
250    SPV,
251    /// Trust
252    Trust,
253    /// Foundation
254    Foundation,
255    /// Limited partnership
256    LimitedPartnership,
257    /// LLC
258    LLC,
259    /// Other corporate entity
260    Other,
261}
262
263impl IntermediaryType {
264    /// Risk weight for AML scoring.
265    pub fn risk_weight(&self) -> f64 {
266        match self {
267            Self::HoldingCompany => 1.2,
268            Self::SPV => 1.5,
269            Self::Trust => 1.6,
270            Self::Foundation => 1.5,
271            Self::LimitedPartnership => 1.3,
272            Self::LLC => 1.2,
273            Self::Other => 1.4,
274        }
275    }
276}
277
278/// Ownership chain for complex structures.
279#[derive(Debug, Clone, Serialize, Deserialize)]
280pub struct OwnershipChain {
281    /// Ultimate beneficial owner
282    pub ultimate_owner: BeneficialOwner,
283    /// Chain of intermediaries (from UBO to entity)
284    pub intermediaries: Vec<IntermediaryEntity>,
285    /// Total layers in ownership structure
286    pub total_layers: u8,
287    /// Effective ownership percentage
288    #[serde(with = "rust_decimal::serde::str")]
289    pub effective_ownership: Decimal,
290}
291
292impl OwnershipChain {
293    /// Create a new ownership chain.
294    pub fn new(owner: BeneficialOwner) -> Self {
295        let effective = owner.ownership_percentage;
296        Self {
297            ultimate_owner: owner,
298            intermediaries: Vec::new(),
299            total_layers: 1,
300            effective_ownership: effective,
301        }
302    }
303
304    /// Add an intermediary layer.
305    pub fn add_intermediary(&mut self, intermediary: IntermediaryEntity) {
306        // Adjust effective ownership
307        let intermediary_pct: f64 = intermediary
308            .ownership_percentage
309            .try_into()
310            .unwrap_or(100.0);
311        let current_effective: f64 = self.effective_ownership.try_into().unwrap_or(0.0);
312        self.effective_ownership =
313            Decimal::from_f64_retain(current_effective * intermediary_pct / 100.0)
314                .unwrap_or(Decimal::ZERO);
315
316        self.intermediaries.push(intermediary);
317        self.total_layers += 1;
318    }
319
320    /// Calculate complexity score.
321    pub fn complexity_score(&self) -> u8 {
322        let mut score = (self.total_layers as f64 - 1.0) * 10.0;
323
324        for intermediary in &self.intermediaries {
325            score += intermediary.entity_type.risk_weight() * 5.0;
326            if intermediary.is_shell {
327                score += 20.0;
328            }
329        }
330
331        if !self.ultimate_owner.is_true_ubo {
332            score += 30.0;
333        }
334
335        score.min(100.0) as u8
336    }
337}
338
339#[cfg(test)]
340mod tests {
341    use super::*;
342
343    #[test]
344    fn test_beneficial_owner() {
345        let owner = BeneficialOwner::new(Uuid::new_v4(), "John Doe", "US", Decimal::from(50));
346        assert!(owner.is_true_ubo);
347        assert!(owner.is_direct);
348    }
349
350    #[test]
351    fn test_ownership_chain() {
352        let owner = BeneficialOwner::new(Uuid::new_v4(), "John Doe", "US", Decimal::from(100));
353        let mut chain = OwnershipChain::new(owner);
354
355        let holding = IntermediaryEntity::new(
356            Uuid::new_v4(),
357            "Holding Co Ltd",
358            IntermediaryType::HoldingCompany,
359            "KY",
360            Decimal::from(80),
361        );
362        chain.add_intermediary(holding);
363
364        assert_eq!(chain.total_layers, 2);
365        assert!(chain.effective_ownership < Decimal::from(100));
366    }
367
368    #[test]
369    fn test_risk_scoring() {
370        let base_owner = BeneficialOwner::new(Uuid::new_v4(), "Jane Doe", "US", Decimal::from(30));
371        let base_score = base_owner.calculate_risk_score();
372
373        let pep_owner =
374            BeneficialOwner::new(Uuid::new_v4(), "Minister Smith", "US", Decimal::from(30))
375                .as_pep();
376        let pep_score = pep_owner.calculate_risk_score();
377
378        assert!(pep_score > base_score);
379    }
380}