use chrono::NaiveDate;
use datasynth_core::models::banking::{
BankingCustomerType, BusinessPersona, RetailPersona, RiskTier, TrustPersona,
};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use super::{BeneficialOwner, KycProfile};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CustomerName {
pub legal_name: String,
pub first_name: Option<String>,
pub last_name: Option<String>,
pub middle_name: Option<String>,
pub trade_name: Option<String>,
}
impl CustomerName {
pub fn individual(first: &str, last: &str) -> Self {
Self {
legal_name: format!("{first} {last}"),
first_name: Some(first.to_string()),
last_name: Some(last.to_string()),
middle_name: None,
trade_name: None,
}
}
pub fn individual_full(first: &str, middle: &str, last: &str) -> Self {
Self {
legal_name: format!("{first} {middle} {last}"),
first_name: Some(first.to_string()),
last_name: Some(last.to_string()),
middle_name: Some(middle.to_string()),
trade_name: None,
}
}
pub fn business(legal_name: &str) -> Self {
Self {
legal_name: legal_name.to_string(),
first_name: None,
last_name: None,
middle_name: None,
trade_name: None,
}
}
pub fn business_with_dba(legal_name: &str, trade_name: &str) -> Self {
Self {
legal_name: legal_name.to_string(),
first_name: None,
last_name: None,
middle_name: None,
trade_name: Some(trade_name.to_string()),
}
}
pub fn display_name(&self) -> &str {
self.trade_name.as_deref().unwrap_or(&self.legal_name)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CustomerRelationship {
pub related_customer_id: Uuid,
pub relationship_type: RelationshipType,
pub start_date: NaiveDate,
pub is_active: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RelationshipType {
Spouse,
ParentChild,
Sibling,
Family,
BusinessPartner,
Employment,
AuthorizedSigner,
Beneficiary,
Guarantor,
Attorney,
TrustRelationship,
JointAccountHolder,
}
impl RelationshipType {
pub fn risk_weight(&self) -> f64 {
match self {
Self::Spouse | Self::ParentChild | Self::Sibling => 1.0,
Self::Family => 1.1,
Self::BusinessPartner => 1.3,
Self::Employment => 0.8,
Self::AuthorizedSigner | Self::JointAccountHolder => 1.2,
Self::Beneficiary => 1.4,
Self::Guarantor => 1.1,
Self::Attorney => 1.5,
Self::TrustRelationship => 1.6,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(untagged)]
pub enum PersonaVariant {
Retail(RetailPersona),
Business(BusinessPersona),
Trust(TrustPersona),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BankingCustomer {
pub customer_id: Uuid,
pub customer_type: BankingCustomerType,
pub name: CustomerName,
pub persona: Option<PersonaVariant>,
pub residence_country: String,
pub citizenship_country: Option<String>,
pub date_of_birth: Option<NaiveDate>,
pub tax_id: Option<String>,
pub national_id: Option<String>,
pub passport_number: Option<String>,
pub onboarding_date: NaiveDate,
pub kyc_profile: KycProfile,
pub risk_tier: RiskTier,
pub account_ids: Vec<Uuid>,
pub relationships: Vec<CustomerRelationship>,
pub beneficial_owners: Vec<BeneficialOwner>,
pub email: Option<String>,
pub phone: Option<String>,
pub address_line1: Option<String>,
pub address_line2: Option<String>,
pub city: Option<String>,
pub state: Option<String>,
pub postal_code: Option<String>,
pub status: CustomerStatus,
pub is_active: bool,
pub is_pep: bool,
pub pep_category: Option<PepCategory>,
pub industry_code: Option<String>,
pub industry_description: Option<String>,
pub household_id: Option<Uuid>,
pub last_kyc_review: Option<NaiveDate>,
pub next_kyc_review: Option<NaiveDate>,
#[serde(skip_serializing_if = "Option::is_none")]
pub enterprise_customer_id: Option<String>,
pub is_mule: bool,
pub kyc_truthful: bool,
pub true_source_of_funds: Option<datasynth_core::models::banking::SourceOfFunds>,
}
impl BankingCustomer {
pub fn new_retail(
customer_id: Uuid,
first_name: &str,
last_name: &str,
residence_country: &str,
onboarding_date: NaiveDate,
) -> Self {
Self {
customer_id,
customer_type: BankingCustomerType::Retail,
name: CustomerName::individual(first_name, last_name),
persona: None,
residence_country: residence_country.to_string(),
citizenship_country: Some(residence_country.to_string()),
date_of_birth: None,
tax_id: None,
national_id: None,
passport_number: None,
onboarding_date,
kyc_profile: KycProfile::default(),
risk_tier: RiskTier::default(),
account_ids: Vec::new(),
relationships: Vec::new(),
beneficial_owners: Vec::new(),
email: None,
phone: None,
address_line1: None,
address_line2: None,
city: None,
state: None,
postal_code: None,
status: CustomerStatus::Active,
is_active: true,
is_pep: false,
pep_category: None,
industry_code: None,
industry_description: None,
household_id: None,
last_kyc_review: Some(onboarding_date),
next_kyc_review: None,
enterprise_customer_id: None,
is_mule: false,
kyc_truthful: true,
true_source_of_funds: None,
}
}
pub fn new_business(
customer_id: Uuid,
legal_name: &str,
residence_country: &str,
onboarding_date: NaiveDate,
) -> Self {
Self {
customer_id,
customer_type: BankingCustomerType::Business,
name: CustomerName::business(legal_name),
persona: None,
residence_country: residence_country.to_string(),
citizenship_country: None,
date_of_birth: None,
tax_id: None,
national_id: None,
passport_number: None,
onboarding_date,
kyc_profile: KycProfile::default(),
risk_tier: RiskTier::default(),
account_ids: Vec::new(),
relationships: Vec::new(),
beneficial_owners: Vec::new(),
email: None,
phone: None,
address_line1: None,
address_line2: None,
city: None,
state: None,
postal_code: None,
status: CustomerStatus::Active,
is_active: true,
is_pep: false,
pep_category: None,
industry_code: None,
industry_description: None,
household_id: None,
last_kyc_review: Some(onboarding_date),
next_kyc_review: None,
enterprise_customer_id: None,
is_mule: false,
kyc_truthful: true,
true_source_of_funds: None,
}
}
pub fn with_persona(mut self, persona: PersonaVariant) -> Self {
self.persona = Some(persona);
self
}
pub fn with_risk_tier(mut self, tier: RiskTier) -> Self {
self.risk_tier = tier;
self
}
pub fn add_account(&mut self, account_id: Uuid) {
self.account_ids.push(account_id);
}
pub fn add_relationship(&mut self, relationship: CustomerRelationship) {
self.relationships.push(relationship);
}
pub fn add_beneficial_owner(&mut self, owner: BeneficialOwner) {
self.beneficial_owners.push(owner);
}
pub fn calculate_risk_score(&self) -> u8 {
let mut score = self.risk_tier.score() as f64;
if self.customer_type.requires_enhanced_dd() {
score *= 1.2;
}
if self.is_pep {
score *= 1.5;
}
if !self.kyc_truthful {
score *= 1.3;
}
if self.is_mule {
score *= 2.0;
}
score.min(100.0) as u8
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum CustomerStatus {
#[default]
Active,
Dormant,
Suspended,
Closed,
UnderReview,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PepCategory {
HeadOfState,
SeniorGovernment,
SeniorJudicial,
SeniorMilitary,
SeniorPolitical,
StateEnterprise,
InternationalOrganization,
FamilyMember,
CloseAssociate,
}
impl PepCategory {
pub fn risk_weight(&self) -> f64 {
match self {
Self::HeadOfState | Self::SeniorGovernment => 3.0,
Self::SeniorJudicial | Self::SeniorMilitary => 2.5,
Self::SeniorPolitical | Self::StateEnterprise => 2.0,
Self::InternationalOrganization => 1.8,
Self::FamilyMember => 2.0,
Self::CloseAssociate => 1.8,
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_customer_name() {
let name = CustomerName::individual("John", "Doe");
assert_eq!(name.legal_name, "John Doe");
assert_eq!(name.first_name, Some("John".to_string()));
let biz = CustomerName::business_with_dba("Acme Corp LLC", "Acme Store");
assert_eq!(biz.display_name(), "Acme Store");
}
#[test]
fn test_banking_customer() {
let customer = BankingCustomer::new_retail(
Uuid::new_v4(),
"Jane",
"Smith",
"US",
NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
);
assert_eq!(customer.customer_type, BankingCustomerType::Retail);
assert!(customer.is_active);
assert!(!customer.is_mule);
assert!(customer.kyc_truthful);
}
#[test]
fn test_risk_score_calculation() {
let mut customer = BankingCustomer::new_retail(
Uuid::new_v4(),
"Test",
"User",
"US",
NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
);
let base_score = customer.calculate_risk_score();
customer.is_pep = true;
let pep_score = customer.calculate_risk_score();
assert!(pep_score > base_score);
customer.is_mule = true;
let mule_score = customer.calculate_risk_score();
assert!(mule_score > pep_score);
}
}