1use 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#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct CustomerName {
15 pub legal_name: String,
17 pub first_name: Option<String>,
19 pub last_name: Option<String>,
21 pub middle_name: Option<String>,
23 pub trade_name: Option<String>,
25}
26
27impl CustomerName {
28 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 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 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 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 pub fn display_name(&self) -> &str {
74 self.trade_name.as_deref().unwrap_or(&self.legal_name)
75 }
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct CustomerRelationship {
81 pub related_customer_id: Uuid,
83 pub relationship_type: RelationshipType,
85 pub start_date: NaiveDate,
87 pub is_active: bool,
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
93#[serde(rename_all = "snake_case")]
94pub enum RelationshipType {
95 Spouse,
97 ParentChild,
99 Sibling,
101 Family,
103 BusinessPartner,
105 Employment,
107 AuthorizedSigner,
109 Beneficiary,
111 Guarantor,
113 Attorney,
115 TrustRelationship,
117 JointAccountHolder,
119}
120
121impl RelationshipType {
122 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
140#[serde(untagged)]
141pub enum PersonaVariant {
142 Retail(RetailPersona),
144 Business(BusinessPersona),
146 Trust(TrustPersona),
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct BankingCustomer {
153 pub customer_id: Uuid,
155 pub customer_type: BankingCustomerType,
157 pub name: CustomerName,
159 pub persona: Option<PersonaVariant>,
161 pub residence_country: String,
163 pub citizenship_country: Option<String>,
165 pub date_of_birth: Option<NaiveDate>,
167 pub tax_id: Option<String>,
169 pub national_id: Option<String>,
171 pub passport_number: Option<String>,
173 pub onboarding_date: NaiveDate,
175 pub kyc_profile: KycProfile,
177 pub risk_tier: RiskTier,
179 pub account_ids: Vec<Uuid>,
181 pub relationships: Vec<CustomerRelationship>,
183 pub beneficial_owners: Vec<BeneficialOwner>,
185 pub email: Option<String>,
187 pub phone: Option<String>,
189 pub address_line1: Option<String>,
191 pub address_line2: Option<String>,
193 pub city: Option<String>,
195 pub state: Option<String>,
197 pub postal_code: Option<String>,
199 pub is_active: bool,
201 pub is_pep: bool,
203 pub pep_category: Option<PepCategory>,
205 pub industry_code: Option<String>,
207 pub industry_description: Option<String>,
209 pub household_id: Option<Uuid>,
211 pub last_kyc_review: Option<NaiveDate>,
213 pub next_kyc_review: Option<NaiveDate>,
215
216 pub is_mule: bool,
219 pub kyc_truthful: bool,
221 pub true_source_of_funds: Option<datasynth_core::models::banking::SourceOfFunds>,
223}
224
225impl BankingCustomer {
226 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 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 pub fn with_persona(mut self, persona: PersonaVariant) -> Self {
319 self.persona = Some(persona);
320 self
321 }
322
323 pub fn with_risk_tier(mut self, tier: RiskTier) -> Self {
325 self.risk_tier = tier;
326 self
327 }
328
329 pub fn add_account(&mut self, account_id: Uuid) {
331 self.account_ids.push(account_id);
332 }
333
334 pub fn add_relationship(&mut self, relationship: CustomerRelationship) {
336 self.relationships.push(relationship);
337 }
338
339 pub fn add_beneficial_owner(&mut self, owner: BeneficialOwner) {
341 self.beneficial_owners.push(owner);
342 }
343
344 pub fn calculate_risk_score(&self) -> u8 {
346 let mut score = self.risk_tier.score() as f64;
347
348 if self.customer_type.requires_enhanced_dd() {
350 score *= 1.2;
351 }
352
353 if self.is_pep {
355 score *= 1.5;
356 }
357
358 if !self.kyc_truthful {
360 score *= 1.3;
361 }
362
363 if self.is_mule {
365 score *= 2.0;
366 }
367
368 score.min(100.0) as u8
369 }
370}
371
372#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
374#[serde(rename_all = "snake_case")]
375pub enum PepCategory {
376 HeadOfState,
378 SeniorGovernment,
380 SeniorJudicial,
382 SeniorMilitary,
384 SeniorPolitical,
386 StateEnterprise,
388 InternationalOrganization,
390 FamilyMember,
392 CloseAssociate,
394}
395
396impl PepCategory {
397 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}