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    /// Customer lifecycle status
200    pub status: CustomerStatus,
201    /// Whether customer is active
202    pub is_active: bool,
203    /// Whether customer is a PEP (Politically Exposed Person)
204    pub is_pep: bool,
205    /// PEP category if applicable
206    pub pep_category: Option<PepCategory>,
207    /// Industry/occupation (NAICS code for businesses)
208    pub industry_code: Option<String>,
209    /// Industry description
210    pub industry_description: Option<String>,
211    /// Household ID for linked retail customers
212    pub household_id: Option<Uuid>,
213    /// Date of last KYC review
214    pub last_kyc_review: Option<NaiveDate>,
215    /// Next scheduled KYC review
216    pub next_kyc_review: Option<NaiveDate>,
217    /// Cross-reference to core enterprise customer ID (from master data)
218    #[serde(skip_serializing_if = "Option::is_none")]
219    pub enterprise_customer_id: Option<String>,
220
221    // Ground truth labels (for ML)
222    /// Whether this is a mule account (ground truth)
223    pub is_mule: bool,
224    /// Whether KYC information is truthful
225    pub kyc_truthful: bool,
226    /// True source of funds if different from declared
227    pub true_source_of_funds: Option<datasynth_core::models::banking::SourceOfFunds>,
228}
229
230impl BankingCustomer {
231    /// Create a new retail customer.
232    pub fn new_retail(
233        customer_id: Uuid,
234        first_name: &str,
235        last_name: &str,
236        residence_country: &str,
237        onboarding_date: NaiveDate,
238    ) -> Self {
239        Self {
240            customer_id,
241            customer_type: BankingCustomerType::Retail,
242            name: CustomerName::individual(first_name, last_name),
243            persona: None,
244            residence_country: residence_country.to_string(),
245            citizenship_country: Some(residence_country.to_string()),
246            date_of_birth: None,
247            tax_id: None,
248            national_id: None,
249            passport_number: None,
250            onboarding_date,
251            kyc_profile: KycProfile::default(),
252            risk_tier: RiskTier::default(),
253            account_ids: Vec::new(),
254            relationships: Vec::new(),
255            beneficial_owners: Vec::new(),
256            email: None,
257            phone: None,
258            address_line1: None,
259            address_line2: None,
260            city: None,
261            state: None,
262            postal_code: None,
263            status: CustomerStatus::Active,
264            is_active: true,
265            is_pep: false,
266            pep_category: None,
267            industry_code: None,
268            industry_description: None,
269            household_id: None,
270            last_kyc_review: Some(onboarding_date),
271            next_kyc_review: None,
272            enterprise_customer_id: None,
273            is_mule: false,
274            kyc_truthful: true,
275            true_source_of_funds: None,
276        }
277    }
278
279    /// Create a new business customer.
280    pub fn new_business(
281        customer_id: Uuid,
282        legal_name: &str,
283        residence_country: &str,
284        onboarding_date: NaiveDate,
285    ) -> Self {
286        Self {
287            customer_id,
288            customer_type: BankingCustomerType::Business,
289            name: CustomerName::business(legal_name),
290            persona: None,
291            residence_country: residence_country.to_string(),
292            citizenship_country: None,
293            date_of_birth: None,
294            tax_id: None,
295            national_id: None,
296            passport_number: None,
297            onboarding_date,
298            kyc_profile: KycProfile::default(),
299            risk_tier: RiskTier::default(),
300            account_ids: Vec::new(),
301            relationships: Vec::new(),
302            beneficial_owners: Vec::new(),
303            email: None,
304            phone: None,
305            address_line1: None,
306            address_line2: None,
307            city: None,
308            state: None,
309            postal_code: None,
310            status: CustomerStatus::Active,
311            is_active: true,
312            is_pep: false,
313            pep_category: None,
314            industry_code: None,
315            industry_description: None,
316            household_id: None,
317            last_kyc_review: Some(onboarding_date),
318            next_kyc_review: None,
319            enterprise_customer_id: None,
320            is_mule: false,
321            kyc_truthful: true,
322            true_source_of_funds: None,
323        }
324    }
325
326    /// Set the persona.
327    pub fn with_persona(mut self, persona: PersonaVariant) -> Self {
328        self.persona = Some(persona);
329        self
330    }
331
332    /// Set the risk tier.
333    pub fn with_risk_tier(mut self, tier: RiskTier) -> Self {
334        self.risk_tier = tier;
335        self
336    }
337
338    /// Add an account.
339    pub fn add_account(&mut self, account_id: Uuid) {
340        self.account_ids.push(account_id);
341    }
342
343    /// Add a relationship.
344    pub fn add_relationship(&mut self, relationship: CustomerRelationship) {
345        self.relationships.push(relationship);
346    }
347
348    /// Add a beneficial owner.
349    pub fn add_beneficial_owner(&mut self, owner: BeneficialOwner) {
350        self.beneficial_owners.push(owner);
351    }
352
353    /// Calculate composite risk score.
354    pub fn calculate_risk_score(&self) -> u8 {
355        let mut score = self.risk_tier.score() as f64;
356
357        // Adjust for customer type
358        if self.customer_type.requires_enhanced_dd() {
359            score *= 1.2;
360        }
361
362        // Adjust for PEP status
363        if self.is_pep {
364            score *= 1.5;
365        }
366
367        // Adjust for KYC truthfulness
368        if !self.kyc_truthful {
369            score *= 1.3;
370        }
371
372        // Adjust for mule status
373        if self.is_mule {
374            score *= 2.0;
375        }
376
377        score.min(100.0) as u8
378    }
379}
380
381/// Customer lifecycle status.
382#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
383#[serde(rename_all = "snake_case")]
384pub enum CustomerStatus {
385    /// Customer is active and in good standing
386    #[default]
387    Active,
388    /// Customer has no recent activity
389    Dormant,
390    /// Customer account is temporarily suspended
391    Suspended,
392    /// Customer account has been closed
393    Closed,
394    /// Customer is under compliance review
395    UnderReview,
396}
397
398/// PEP (Politically Exposed Person) category.
399#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
400#[serde(rename_all = "snake_case")]
401pub enum PepCategory {
402    /// Head of state / government
403    HeadOfState,
404    /// Senior government official
405    SeniorGovernment,
406    /// Senior judicial official
407    SeniorJudicial,
408    /// Senior military official
409    SeniorMilitary,
410    /// Senior political party official
411    SeniorPolitical,
412    /// Senior executive of state-owned enterprise
413    StateEnterprise,
414    /// International organization official
415    InternationalOrganization,
416    /// Family member of PEP
417    FamilyMember,
418    /// Close associate of PEP
419    CloseAssociate,
420}
421
422impl PepCategory {
423    /// Risk weight for AML scoring.
424    pub fn risk_weight(&self) -> f64 {
425        match self {
426            Self::HeadOfState | Self::SeniorGovernment => 3.0,
427            Self::SeniorJudicial | Self::SeniorMilitary => 2.5,
428            Self::SeniorPolitical | Self::StateEnterprise => 2.0,
429            Self::InternationalOrganization => 1.8,
430            Self::FamilyMember => 2.0,
431            Self::CloseAssociate => 1.8,
432        }
433    }
434}
435
436#[cfg(test)]
437#[allow(clippy::unwrap_used)]
438mod tests {
439    use super::*;
440
441    #[test]
442    fn test_customer_name() {
443        let name = CustomerName::individual("John", "Doe");
444        assert_eq!(name.legal_name, "John Doe");
445        assert_eq!(name.first_name, Some("John".to_string()));
446
447        let biz = CustomerName::business_with_dba("Acme Corp LLC", "Acme Store");
448        assert_eq!(biz.display_name(), "Acme Store");
449    }
450
451    #[test]
452    fn test_banking_customer() {
453        let customer = BankingCustomer::new_retail(
454            Uuid::new_v4(),
455            "Jane",
456            "Smith",
457            "US",
458            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
459        );
460
461        assert_eq!(customer.customer_type, BankingCustomerType::Retail);
462        assert!(customer.is_active);
463        assert!(!customer.is_mule);
464        assert!(customer.kyc_truthful);
465    }
466
467    #[test]
468    fn test_risk_score_calculation() {
469        let mut customer = BankingCustomer::new_retail(
470            Uuid::new_v4(),
471            "Test",
472            "User",
473            "US",
474            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
475        );
476
477        let base_score = customer.calculate_risk_score();
478
479        customer.is_pep = true;
480        let pep_score = customer.calculate_risk_score();
481        assert!(pep_score > base_score);
482
483        customer.is_mule = true;
484        let mule_score = customer.calculate_risk_score();
485        assert!(mule_score > pep_score);
486    }
487}