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 status: CustomerStatus,
201 pub is_active: bool,
203 pub is_pep: bool,
205 pub pep_category: Option<PepCategory>,
207 pub industry_code: Option<String>,
209 pub industry_description: Option<String>,
211 pub household_id: Option<Uuid>,
213 pub last_kyc_review: Option<NaiveDate>,
215 pub next_kyc_review: Option<NaiveDate>,
217 #[serde(skip_serializing_if = "Option::is_none")]
219 pub enterprise_customer_id: Option<String>,
220
221 pub is_mule: bool,
224 pub kyc_truthful: bool,
226 pub true_source_of_funds: Option<datasynth_core::models::banking::SourceOfFunds>,
228}
229
230impl BankingCustomer {
231 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 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 pub fn with_persona(mut self, persona: PersonaVariant) -> Self {
328 self.persona = Some(persona);
329 self
330 }
331
332 pub fn with_risk_tier(mut self, tier: RiskTier) -> Self {
334 self.risk_tier = tier;
335 self
336 }
337
338 pub fn add_account(&mut self, account_id: Uuid) {
340 self.account_ids.push(account_id);
341 }
342
343 pub fn add_relationship(&mut self, relationship: CustomerRelationship) {
345 self.relationships.push(relationship);
346 }
347
348 pub fn add_beneficial_owner(&mut self, owner: BeneficialOwner) {
350 self.beneficial_owners.push(owner);
351 }
352
353 pub fn calculate_risk_score(&self) -> u8 {
355 let mut score = self.risk_tier.score() as f64;
356
357 if self.customer_type.requires_enhanced_dd() {
359 score *= 1.2;
360 }
361
362 if self.is_pep {
364 score *= 1.5;
365 }
366
367 if !self.kyc_truthful {
369 score *= 1.3;
370 }
371
372 if self.is_mule {
374 score *= 2.0;
375 }
376
377 score.min(100.0) as u8
378 }
379}
380
381#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
383#[serde(rename_all = "snake_case")]
384pub enum CustomerStatus {
385 #[default]
387 Active,
388 Dormant,
390 Suspended,
392 Closed,
394 UnderReview,
396}
397
398#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
400#[serde(rename_all = "snake_case")]
401pub enum PepCategory {
402 HeadOfState,
404 SeniorGovernment,
406 SeniorJudicial,
408 SeniorMilitary,
410 SeniorPolitical,
412 StateEnterprise,
414 InternationalOrganization,
416 FamilyMember,
418 CloseAssociate,
420}
421
422impl PepCategory {
423 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}