datasynth_banking/generators/
kyc_generator.rs1use 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
13pub struct KycGenerator {
15 rng: ChaCha8Rng,
16}
17
18impl KycGenerator {
19 pub fn new(seed: u64) -> Self {
21 Self {
22 rng: ChaCha8Rng::seed_from_u64(seed.wrapping_add(4000)),
23 }
24 }
25
26 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 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 profile.expected_categories = self.generate_retail_categories(persona);
99
100 profile.geographic_exposure = vec![CountryExposure {
102 country_code: "US".to_string(),
103 exposure_type: CountryExposureType::Residence,
104 risk_category: CountryRiskCategory::Low,
105 }];
106
107 profile.completeness_score = self.rng.gen_range(0.90..1.0);
109
110 profile
111 }
112
113 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 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 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 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}