Skip to main content

datasynth_banking/models/
customer.rs

1//! Banking customer model for KYC/AML simulation.
2
3use chrono::NaiveDate;
4use datasynth_core::models::banking::{
5    BankingCustomerType, BusinessPersona, RetailPersona, RiskTier, TrustPersona,
6};
7use serde::{Deserialize, Serialize};
8use uuid::Uuid;
9
10use super::{BeneficialOwner, KycProfile};
11
12/// Customer name structure supporting various formats.
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct CustomerName {
15    /// Full legal name
16    pub legal_name: String,
17    /// First name (for individuals)
18    pub first_name: Option<String>,
19    /// Last name (for individuals)
20    pub last_name: Option<String>,
21    /// Middle name (for individuals)
22    pub middle_name: Option<String>,
23    /// Trade name / DBA (for businesses)
24    pub trade_name: Option<String>,
25}
26
27impl CustomerName {
28    /// Create a new individual name.
29    pub fn individual(first: &str, last: &str) -> Self {
30        Self {
31            legal_name: format!("{} {}", first, last),
32            first_name: Some(first.to_string()),
33            last_name: Some(last.to_string()),
34            middle_name: None,
35            trade_name: None,
36        }
37    }
38
39    /// Create a new individual name with middle name.
40    pub fn individual_full(first: &str, middle: &str, last: &str) -> Self {
41        Self {
42            legal_name: format!("{} {} {}", first, middle, last),
43            first_name: Some(first.to_string()),
44            last_name: Some(last.to_string()),
45            middle_name: Some(middle.to_string()),
46            trade_name: None,
47        }
48    }
49
50    /// Create a new business name.
51    pub fn business(legal_name: &str) -> Self {
52        Self {
53            legal_name: legal_name.to_string(),
54            first_name: None,
55            last_name: None,
56            middle_name: None,
57            trade_name: None,
58        }
59    }
60
61    /// Create a business name with trade name.
62    pub fn business_with_dba(legal_name: &str, trade_name: &str) -> Self {
63        Self {
64            legal_name: legal_name.to_string(),
65            first_name: None,
66            last_name: None,
67            middle_name: None,
68            trade_name: Some(trade_name.to_string()),
69        }
70    }
71
72    /// Get the display name.
73    pub fn display_name(&self) -> &str {
74        self.trade_name.as_deref().unwrap_or(&self.legal_name)
75    }
76}
77
78/// Customer relationship for linked accounts.
79#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct CustomerRelationship {
81    /// Related customer ID
82    pub related_customer_id: Uuid,
83    /// Relationship type
84    pub relationship_type: RelationshipType,
85    /// Start date of relationship
86    pub start_date: NaiveDate,
87    /// Whether relationship is still active
88    pub is_active: bool,
89}
90
91/// Type of relationship between customers.
92#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
93#[serde(rename_all = "snake_case")]
94pub enum RelationshipType {
95    /// Spouse / domestic partner
96    Spouse,
97    /// Parent-child
98    ParentChild,
99    /// Sibling
100    Sibling,
101    /// Other family member
102    Family,
103    /// Business partner
104    BusinessPartner,
105    /// Employer-employee
106    Employment,
107    /// Authorized signer
108    AuthorizedSigner,
109    /// Beneficiary
110    Beneficiary,
111    /// Guarantor
112    Guarantor,
113    /// Attorney / power of attorney
114    Attorney,
115    /// Trust relationship
116    TrustRelationship,
117    /// Joint account holder
118    JointAccountHolder,
119}
120
121impl RelationshipType {
122    /// Risk weight for AML scoring.
123    pub fn risk_weight(&self) -> f64 {
124        match self {
125            Self::Spouse | Self::ParentChild | Self::Sibling => 1.0,
126            Self::Family => 1.1,
127            Self::BusinessPartner => 1.3,
128            Self::Employment => 0.8,
129            Self::AuthorizedSigner | Self::JointAccountHolder => 1.2,
130            Self::Beneficiary => 1.4,
131            Self::Guarantor => 1.1,
132            Self::Attorney => 1.5,
133            Self::TrustRelationship => 1.6,
134        }
135    }
136}
137
138/// Persona variant for behavioral modeling.
139#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
140#[serde(untagged)]
141pub enum PersonaVariant {
142    /// Retail customer persona
143    Retail(RetailPersona),
144    /// Business customer persona
145    Business(BusinessPersona),
146    /// Trust customer persona
147    Trust(TrustPersona),
148}
149
150/// A banking customer with full KYC information.
151#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct BankingCustomer {
153    /// Unique customer identifier
154    pub customer_id: Uuid,
155    /// Customer type (retail, business, trust)
156    pub customer_type: BankingCustomerType,
157    /// Customer name
158    pub name: CustomerName,
159    /// Behavioral persona
160    pub persona: Option<PersonaVariant>,
161    /// Country of residence (ISO 3166-1 alpha-2)
162    pub residence_country: String,
163    /// Country of citizenship (for individuals)
164    pub citizenship_country: Option<String>,
165    /// Date of birth (for individuals) or incorporation (for entities)
166    pub date_of_birth: Option<NaiveDate>,
167    /// Tax identification number
168    pub tax_id: Option<String>,
169    /// National ID number
170    pub national_id: Option<String>,
171    /// Passport number
172    pub passport_number: Option<String>,
173    /// Customer onboarding date
174    pub onboarding_date: NaiveDate,
175    /// KYC profile with expected activity
176    pub kyc_profile: KycProfile,
177    /// Risk tier assigned
178    pub risk_tier: RiskTier,
179    /// Account IDs owned by this customer
180    pub account_ids: Vec<Uuid>,
181    /// Relationships with other customers
182    pub relationships: Vec<CustomerRelationship>,
183    /// Beneficial owners (for entities/trusts)
184    pub beneficial_owners: Vec<BeneficialOwner>,
185    /// Primary contact email
186    pub email: Option<String>,
187    /// Primary contact phone
188    pub phone: Option<String>,
189    /// Address line 1
190    pub address_line1: Option<String>,
191    /// Address line 2
192    pub address_line2: Option<String>,
193    /// City
194    pub city: Option<String>,
195    /// State/province
196    pub state: Option<String>,
197    /// Postal code
198    pub postal_code: Option<String>,
199    /// Whether customer is active
200    pub is_active: bool,
201    /// Whether customer is a PEP (Politically Exposed Person)
202    pub is_pep: bool,
203    /// PEP category if applicable
204    pub pep_category: Option<PepCategory>,
205    /// Industry/occupation (NAICS code for businesses)
206    pub industry_code: Option<String>,
207    /// Industry description
208    pub industry_description: Option<String>,
209    /// Household ID for linked retail customers
210    pub household_id: Option<Uuid>,
211    /// Date of last KYC review
212    pub last_kyc_review: Option<NaiveDate>,
213    /// Next scheduled KYC review
214    pub next_kyc_review: Option<NaiveDate>,
215
216    // Ground truth labels (for ML)
217    /// Whether this is a mule account (ground truth)
218    pub is_mule: bool,
219    /// Whether KYC information is truthful
220    pub kyc_truthful: bool,
221    /// True source of funds if different from declared
222    pub true_source_of_funds: Option<datasynth_core::models::banking::SourceOfFunds>,
223}
224
225impl BankingCustomer {
226    /// Create a new retail customer.
227    pub fn new_retail(
228        customer_id: Uuid,
229        first_name: &str,
230        last_name: &str,
231        residence_country: &str,
232        onboarding_date: NaiveDate,
233    ) -> Self {
234        Self {
235            customer_id,
236            customer_type: BankingCustomerType::Retail,
237            name: CustomerName::individual(first_name, last_name),
238            persona: None,
239            residence_country: residence_country.to_string(),
240            citizenship_country: Some(residence_country.to_string()),
241            date_of_birth: None,
242            tax_id: None,
243            national_id: None,
244            passport_number: None,
245            onboarding_date,
246            kyc_profile: KycProfile::default(),
247            risk_tier: RiskTier::default(),
248            account_ids: Vec::new(),
249            relationships: Vec::new(),
250            beneficial_owners: Vec::new(),
251            email: None,
252            phone: None,
253            address_line1: None,
254            address_line2: None,
255            city: None,
256            state: None,
257            postal_code: None,
258            is_active: true,
259            is_pep: false,
260            pep_category: None,
261            industry_code: None,
262            industry_description: None,
263            household_id: None,
264            last_kyc_review: Some(onboarding_date),
265            next_kyc_review: None,
266            is_mule: false,
267            kyc_truthful: true,
268            true_source_of_funds: None,
269        }
270    }
271
272    /// Create a new business customer.
273    pub fn new_business(
274        customer_id: Uuid,
275        legal_name: &str,
276        residence_country: &str,
277        onboarding_date: NaiveDate,
278    ) -> Self {
279        Self {
280            customer_id,
281            customer_type: BankingCustomerType::Business,
282            name: CustomerName::business(legal_name),
283            persona: None,
284            residence_country: residence_country.to_string(),
285            citizenship_country: None,
286            date_of_birth: None,
287            tax_id: None,
288            national_id: None,
289            passport_number: None,
290            onboarding_date,
291            kyc_profile: KycProfile::default(),
292            risk_tier: RiskTier::default(),
293            account_ids: Vec::new(),
294            relationships: Vec::new(),
295            beneficial_owners: Vec::new(),
296            email: None,
297            phone: None,
298            address_line1: None,
299            address_line2: None,
300            city: None,
301            state: None,
302            postal_code: None,
303            is_active: true,
304            is_pep: false,
305            pep_category: None,
306            industry_code: None,
307            industry_description: None,
308            household_id: None,
309            last_kyc_review: Some(onboarding_date),
310            next_kyc_review: None,
311            is_mule: false,
312            kyc_truthful: true,
313            true_source_of_funds: None,
314        }
315    }
316
317    /// Set the persona.
318    pub fn with_persona(mut self, persona: PersonaVariant) -> Self {
319        self.persona = Some(persona);
320        self
321    }
322
323    /// Set the risk tier.
324    pub fn with_risk_tier(mut self, tier: RiskTier) -> Self {
325        self.risk_tier = tier;
326        self
327    }
328
329    /// Add an account.
330    pub fn add_account(&mut self, account_id: Uuid) {
331        self.account_ids.push(account_id);
332    }
333
334    /// Add a relationship.
335    pub fn add_relationship(&mut self, relationship: CustomerRelationship) {
336        self.relationships.push(relationship);
337    }
338
339    /// Add a beneficial owner.
340    pub fn add_beneficial_owner(&mut self, owner: BeneficialOwner) {
341        self.beneficial_owners.push(owner);
342    }
343
344    /// Calculate composite risk score.
345    pub fn calculate_risk_score(&self) -> u8 {
346        let mut score = self.risk_tier.score() as f64;
347
348        // Adjust for customer type
349        if self.customer_type.requires_enhanced_dd() {
350            score *= 1.2;
351        }
352
353        // Adjust for PEP status
354        if self.is_pep {
355            score *= 1.5;
356        }
357
358        // Adjust for KYC truthfulness
359        if !self.kyc_truthful {
360            score *= 1.3;
361        }
362
363        // Adjust for mule status
364        if self.is_mule {
365            score *= 2.0;
366        }
367
368        score.min(100.0) as u8
369    }
370}
371
372/// PEP (Politically Exposed Person) category.
373#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
374#[serde(rename_all = "snake_case")]
375pub enum PepCategory {
376    /// Head of state / government
377    HeadOfState,
378    /// Senior government official
379    SeniorGovernment,
380    /// Senior judicial official
381    SeniorJudicial,
382    /// Senior military official
383    SeniorMilitary,
384    /// Senior political party official
385    SeniorPolitical,
386    /// Senior executive of state-owned enterprise
387    StateEnterprise,
388    /// International organization official
389    InternationalOrganization,
390    /// Family member of PEP
391    FamilyMember,
392    /// Close associate of PEP
393    CloseAssociate,
394}
395
396impl PepCategory {
397    /// Risk weight for AML scoring.
398    pub fn risk_weight(&self) -> f64 {
399        match self {
400            Self::HeadOfState | Self::SeniorGovernment => 3.0,
401            Self::SeniorJudicial | Self::SeniorMilitary => 2.5,
402            Self::SeniorPolitical | Self::StateEnterprise => 2.0,
403            Self::InternationalOrganization => 1.8,
404            Self::FamilyMember => 2.0,
405            Self::CloseAssociate => 1.8,
406        }
407    }
408}
409
410#[cfg(test)]
411mod tests {
412    use super::*;
413
414    #[test]
415    fn test_customer_name() {
416        let name = CustomerName::individual("John", "Doe");
417        assert_eq!(name.legal_name, "John Doe");
418        assert_eq!(name.first_name, Some("John".to_string()));
419
420        let biz = CustomerName::business_with_dba("Acme Corp LLC", "Acme Store");
421        assert_eq!(biz.display_name(), "Acme Store");
422    }
423
424    #[test]
425    fn test_banking_customer() {
426        let customer = BankingCustomer::new_retail(
427            Uuid::new_v4(),
428            "Jane",
429            "Smith",
430            "US",
431            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
432        );
433
434        assert_eq!(customer.customer_type, BankingCustomerType::Retail);
435        assert!(customer.is_active);
436        assert!(!customer.is_mule);
437        assert!(customer.kyc_truthful);
438    }
439
440    #[test]
441    fn test_risk_score_calculation() {
442        let mut customer = BankingCustomer::new_retail(
443            Uuid::new_v4(),
444            "Test",
445            "User",
446            "US",
447            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
448        );
449
450        let base_score = customer.calculate_risk_score();
451
452        customer.is_pep = true;
453        let pep_score = customer.calculate_risk_score();
454        assert!(pep_score > base_score);
455
456        customer.is_mule = true;
457        let mule_score = customer.calculate_risk_score();
458        assert!(mule_score > pep_score);
459    }
460}