Skip to main content

datasynth_banking/generators/
kyc_generator.rs

1//! KYC profile generator for banking data.
2
3use datasynth_core::models::banking::{
4    CashIntensity, CountryExposure, CountryExposureType, CountryRiskCategory, FrequencyBand,
5    SourceOfFunds, SourceOfWealth, TurnoverBand,
6};
7use rand::prelude::*;
8use rand_chacha::ChaCha8Rng;
9
10use crate::config::BankingConfig;
11use crate::models::{BankingCustomer, ExpectedCategory, KycProfile, PersonaVariant};
12
13/// Generator for KYC profiles.
14pub struct KycGenerator {
15    rng: ChaCha8Rng,
16}
17
18impl KycGenerator {
19    /// Create a new KYC generator.
20    pub fn new(seed: u64) -> Self {
21        Self {
22            rng: ChaCha8Rng::seed_from_u64(seed.wrapping_add(4000)),
23        }
24    }
25
26    /// Generate KYC profile for a customer.
27    pub fn generate_profile(
28        &mut self,
29        customer: &BankingCustomer,
30        _config: &BankingConfig,
31    ) -> KycProfile {
32        match &customer.persona {
33            Some(PersonaVariant::Retail(p)) => self.generate_retail_profile(*p),
34            Some(PersonaVariant::Business(p)) => self.generate_business_profile(*p),
35            Some(PersonaVariant::Trust(p)) => self.generate_trust_profile(*p),
36            None => KycProfile::default(),
37        }
38    }
39
40    /// Generate retail KYC profile.
41    fn generate_retail_profile(
42        &mut self,
43        persona: datasynth_core::models::banking::RetailPersona,
44    ) -> KycProfile {
45        use datasynth_core::models::banking::RetailPersona;
46
47        let (turnover, frequency, source, cash_intensity) = match persona {
48            RetailPersona::Student => (
49                TurnoverBand::VeryLow,
50                FrequencyBand::Low,
51                SourceOfFunds::Other,
52                CashIntensity::Low,
53            ),
54            RetailPersona::EarlyCareer => (
55                TurnoverBand::Low,
56                FrequencyBand::Medium,
57                SourceOfFunds::Employment,
58                CashIntensity::Low,
59            ),
60            RetailPersona::MidCareer => (
61                TurnoverBand::Medium,
62                FrequencyBand::Medium,
63                SourceOfFunds::Employment,
64                CashIntensity::VeryLow,
65            ),
66            RetailPersona::Retiree => (
67                TurnoverBand::Low,
68                FrequencyBand::Low,
69                SourceOfFunds::Pension,
70                CashIntensity::Moderate,
71            ),
72            RetailPersona::HighNetWorth => (
73                TurnoverBand::VeryHigh,
74                FrequencyBand::High,
75                SourceOfFunds::Investments,
76                CashIntensity::VeryLow,
77            ),
78            RetailPersona::GigWorker => (
79                TurnoverBand::Low,
80                FrequencyBand::High,
81                SourceOfFunds::SelfEmployment,
82                CashIntensity::Moderate,
83            ),
84            _ => (
85                TurnoverBand::Low,
86                FrequencyBand::Medium,
87                SourceOfFunds::Employment,
88                CashIntensity::Low,
89            ),
90        };
91
92        let mut profile = KycProfile::new("Personal banking", source)
93            .with_turnover(turnover)
94            .with_frequency(frequency)
95            .with_cash_intensity(cash_intensity);
96
97        // Add expected categories
98        profile.expected_categories = self.generate_retail_categories(persona);
99
100        // Add geographic exposure
101        profile.geographic_exposure = vec![CountryExposure {
102            country_code: "US".to_string(),
103            exposure_type: CountryExposureType::Residence,
104            risk_category: CountryRiskCategory::Low,
105        }];
106
107        // Random completeness
108        profile.completeness_score = self.rng.gen_range(0.90..1.0);
109
110        profile
111    }
112
113    /// Generate business KYC profile.
114    fn generate_business_profile(
115        &mut self,
116        persona: datasynth_core::models::banking::BusinessPersona,
117    ) -> KycProfile {
118        use datasynth_core::models::banking::BusinessPersona;
119
120        let (turnover, cash_intensity) = match persona {
121            BusinessPersona::SmallBusiness => (TurnoverBand::Medium, CashIntensity::Low),
122            BusinessPersona::MidMarket => (TurnoverBand::High, CashIntensity::VeryLow),
123            BusinessPersona::Enterprise => (TurnoverBand::UltraHigh, CashIntensity::VeryLow),
124            BusinessPersona::CashIntensive => (TurnoverBand::High, CashIntensity::VeryHigh),
125            BusinessPersona::ImportExport => (TurnoverBand::VeryHigh, CashIntensity::Low),
126            _ => (TurnoverBand::Medium, CashIntensity::Moderate),
127        };
128
129        let mut profile = KycProfile::new("Business operations", SourceOfFunds::SelfEmployment)
130            .with_turnover(turnover)
131            .with_frequency(FrequencyBand::High)
132            .with_cash_intensity(cash_intensity);
133
134        // Business-specific settings
135        profile.beneficial_owner_complexity = self.rng.gen_range(1..5);
136
137        if matches!(persona, BusinessPersona::ImportExport) {
138            profile.international_rate = 0.4;
139            profile.geographic_exposure = vec![
140                CountryExposure {
141                    country_code: "US".to_string(),
142                    exposure_type: CountryExposureType::BusinessOperations,
143                    risk_category: CountryRiskCategory::Low,
144                },
145                CountryExposure {
146                    country_code: "CN".to_string(),
147                    exposure_type: CountryExposureType::TransactionHistory,
148                    risk_category: CountryRiskCategory::Medium,
149                },
150            ];
151        }
152
153        profile
154    }
155
156    /// Generate trust KYC profile.
157    fn generate_trust_profile(
158        &mut self,
159        _persona: datasynth_core::models::banking::TrustPersona,
160    ) -> KycProfile {
161        let mut profile = KycProfile::high_net_worth();
162        profile.beneficial_owner_complexity = self.rng.gen_range(3..8);
163        profile.source_of_wealth = Some(SourceOfWealth::Inheritance);
164        profile
165    }
166
167    /// Generate expected categories for retail persona.
168    fn generate_retail_categories(
169        &self,
170        persona: datasynth_core::models::banking::RetailPersona,
171    ) -> Vec<ExpectedCategory> {
172        use datasynth_core::models::banking::RetailPersona;
173
174        match persona {
175            RetailPersona::Student => vec![
176                ExpectedCategory::new("Dining", 0.25),
177                ExpectedCategory::new("Entertainment", 0.20),
178                ExpectedCategory::new("Shopping", 0.20),
179                ExpectedCategory::new("Transportation", 0.15),
180            ],
181            RetailPersona::MidCareer => vec![
182                ExpectedCategory::new("Groceries", 0.25),
183                ExpectedCategory::new("Dining", 0.15),
184                ExpectedCategory::new("Utilities", 0.15),
185                ExpectedCategory::new("Shopping", 0.20),
186            ],
187            RetailPersona::HighNetWorth => vec![
188                ExpectedCategory::new("Investment", 0.30),
189                ExpectedCategory::new("Luxury", 0.20),
190                ExpectedCategory::new("Travel", 0.20),
191            ],
192            _ => vec![
193                ExpectedCategory::new("Groceries", 0.25),
194                ExpectedCategory::new("Shopping", 0.20),
195                ExpectedCategory::new("Dining", 0.15),
196            ],
197        }
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204    use chrono::NaiveDate;
205    use uuid::Uuid;
206
207    #[test]
208    fn test_kyc_generation() {
209        let config = BankingConfig::default();
210        let mut gen = KycGenerator::new(12345);
211
212        let customer = BankingCustomer::new_retail(
213            Uuid::new_v4(),
214            "Test",
215            "User",
216            "US",
217            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
218        )
219        .with_persona(PersonaVariant::Retail(
220            datasynth_core::models::banking::RetailPersona::MidCareer,
221        ));
222
223        let profile = gen.generate_profile(&customer, &config);
224        assert!(!profile.declared_purpose.is_empty());
225    }
226}