datasynth_banking/generators/
account_generator.rs1use chrono::NaiveDate;
4use datasynth_core::models::banking::{AccountFeatures, BankAccountType, BankingCustomerType};
5use datasynth_core::DeterministicUuidFactory;
6use rand::prelude::*;
7use rand_chacha::ChaCha8Rng;
8use rust_decimal::Decimal;
9
10use crate::config::BankingConfig;
11use crate::models::{BankAccount, BankingCustomer, PersonaVariant};
12
13pub struct AccountGenerator {
15 config: BankingConfig,
16 rng: ChaCha8Rng,
17 uuid_factory: DeterministicUuidFactory,
18 account_counter: u64,
19}
20
21impl AccountGenerator {
22 pub fn new(config: BankingConfig, seed: u64) -> Self {
24 Self {
25 config,
26 rng: ChaCha8Rng::seed_from_u64(seed.wrapping_add(1000)),
27 uuid_factory: DeterministicUuidFactory::new(
28 seed,
29 datasynth_core::GeneratorType::ARSubledger,
30 ), account_counter: 0,
32 }
33 }
34
35 pub fn generate_for_customers(
37 &mut self,
38 customers: &mut [BankingCustomer],
39 ) -> Vec<BankAccount> {
40 let mut accounts = Vec::new();
41
42 for customer in customers.iter_mut() {
43 let customer_accounts = self.generate_customer_accounts(customer);
44 for account in &customer_accounts {
45 customer.add_account(account.account_id);
46 }
47 accounts.extend(customer_accounts);
48 }
49
50 accounts
51 }
52
53 pub fn generate_customer_accounts(&mut self, customer: &BankingCustomer) -> Vec<BankAccount> {
55 let mut accounts = Vec::new();
56
57 let account_count = self.determine_account_count(customer);
58
59 accounts.push(self.generate_primary_account(customer));
61
62 for i in 1..account_count {
64 accounts.push(self.generate_secondary_account(customer, i));
65 }
66
67 accounts
68 }
69
70 fn determine_account_count(&mut self, customer: &BankingCustomer) -> u32 {
72 let base_count = match customer.customer_type {
73 BankingCustomerType::Retail => self.config.products.avg_accounts_retail,
74 BankingCustomerType::Business => self.config.products.avg_accounts_business,
75 BankingCustomerType::Trust => 2.0,
76 _ => 1.5,
77 };
78
79 let multiplier = match &customer.persona {
81 Some(PersonaVariant::Retail(p)) => {
82 use datasynth_core::models::banking::RetailPersona;
83 match p {
84 RetailPersona::HighNetWorth => 2.0,
85 RetailPersona::MidCareer => 1.5,
86 RetailPersona::Student => 1.0,
87 _ => 1.2,
88 }
89 }
90 Some(PersonaVariant::Business(p)) => {
91 use datasynth_core::models::banking::BusinessPersona;
92 match p {
93 BusinessPersona::Enterprise => 3.0,
94 BusinessPersona::MidMarket => 2.0,
95 _ => 1.5,
96 }
97 }
98 _ => 1.0,
99 };
100
101 let target = base_count * multiplier;
102 let variation: f64 = self.rng.gen_range(-0.5..0.5);
103 ((target + variation).round() as u32).max(1)
104 }
105
106 fn generate_primary_account(&mut self, customer: &BankingCustomer) -> BankAccount {
108 let account_id = self.uuid_factory.next();
109 let account_number = self.generate_account_number();
110
111 let account_type = match customer.customer_type {
112 BankingCustomerType::Retail => BankAccountType::Checking,
113 BankingCustomerType::Business => BankAccountType::BusinessOperating,
114 BankingCustomerType::Trust => BankAccountType::TrustAccount,
115 _ => BankAccountType::Checking,
116 };
117
118 let mut account = BankAccount::new(
119 account_id,
120 account_number,
121 account_type,
122 customer.customer_id,
123 &self.get_customer_currency(customer),
124 customer.onboarding_date,
125 );
126
127 account.features = self.generate_features(customer, true);
129
130 account.current_balance = self.generate_initial_balance(customer);
132 account.available_balance = account.current_balance;
133
134 if customer.residence_country == "US" {
136 account.routing_number = Some(self.generate_routing_number());
137 }
138
139 account
140 }
141
142 fn generate_secondary_account(
144 &mut self,
145 customer: &BankingCustomer,
146 index: u32,
147 ) -> BankAccount {
148 let account_id = self.uuid_factory.next();
149 let account_number = self.generate_account_number();
150
151 let account_type = self.select_secondary_account_type(customer, index);
152
153 let mut account = BankAccount::new(
154 account_id,
155 account_number,
156 account_type,
157 customer.customer_id,
158 &self.get_customer_currency(customer),
159 self.random_opening_date(customer.onboarding_date),
160 );
161
162 account.features = self.generate_features(customer, false);
164
165 account.current_balance = self.generate_initial_balance(customer)
167 * Decimal::from_f64_retain(0.3).unwrap_or(Decimal::ZERO);
168 account.available_balance = account.current_balance;
169
170 account
171 }
172
173 fn select_secondary_account_type(
175 &mut self,
176 customer: &BankingCustomer,
177 _index: u32,
178 ) -> BankAccountType {
179 match customer.customer_type {
180 BankingCustomerType::Retail => {
181 let types = [
182 (BankAccountType::Savings, 0.5),
183 (BankAccountType::MoneyMarket, 0.2),
184 (BankAccountType::CertificateOfDeposit, 0.1),
185 (BankAccountType::Investment, 0.2),
186 ];
187 self.weighted_select(&types)
188 }
189 BankingCustomerType::Business => {
190 let types = [
191 (BankAccountType::BusinessSavings, 0.4),
192 (BankAccountType::Payroll, 0.3),
193 (BankAccountType::ForeignCurrency, 0.2),
194 (BankAccountType::Escrow, 0.1),
195 ];
196 self.weighted_select(&types)
197 }
198 _ => BankAccountType::Savings,
199 }
200 }
201
202 fn generate_features(
204 &mut self,
205 customer: &BankingCustomer,
206 is_primary: bool,
207 ) -> AccountFeatures {
208 let mut features = match customer.customer_type {
209 BankingCustomerType::Retail if is_primary => {
210 if matches!(
211 customer.persona,
212 Some(PersonaVariant::Retail(
213 datasynth_core::models::banking::RetailPersona::HighNetWorth
214 ))
215 ) {
216 AccountFeatures::retail_premium()
217 } else {
218 AccountFeatures::retail_standard()
219 }
220 }
221 BankingCustomerType::Business => AccountFeatures::business_standard(),
222 _ => AccountFeatures::retail_standard(),
223 };
224
225 if self.rng.gen::<f64>() > self.config.products.debit_card_rate {
227 features.debit_card = false;
228 }
229 if self.rng.gen::<f64>() > self.config.products.international_rate {
230 features.international_transfers = false;
231 features.wire_transfers = false;
232 }
233
234 if !is_primary {
236 features.debit_card = false;
237 features.check_writing = false;
238 }
239
240 features
241 }
242
243 fn generate_initial_balance(&mut self, customer: &BankingCustomer) -> Decimal {
245 let base_balance = match &customer.persona {
246 Some(PersonaVariant::Retail(p)) => {
247 use datasynth_core::models::banking::RetailPersona;
248 match p {
249 RetailPersona::Student => self.rng.gen_range(100.0..2_000.0),
250 RetailPersona::EarlyCareer => self.rng.gen_range(500.0..10_000.0),
251 RetailPersona::MidCareer => self.rng.gen_range(2_000.0..50_000.0),
252 RetailPersona::Retiree => self.rng.gen_range(5_000.0..100_000.0),
253 RetailPersona::HighNetWorth => self.rng.gen_range(50_000.0..1_000_000.0),
254 RetailPersona::GigWorker => self.rng.gen_range(200.0..5_000.0),
255 _ => self.rng.gen_range(500.0..5_000.0),
256 }
257 }
258 Some(PersonaVariant::Business(p)) => {
259 use datasynth_core::models::banking::BusinessPersona;
260 match p {
261 BusinessPersona::SmallBusiness => self.rng.gen_range(5_000.0..100_000.0),
262 BusinessPersona::MidMarket => self.rng.gen_range(50_000.0..1_000_000.0),
263 BusinessPersona::Enterprise => self.rng.gen_range(500_000.0..10_000_000.0),
264 BusinessPersona::CashIntensive => self.rng.gen_range(10_000.0..200_000.0),
265 _ => self.rng.gen_range(10_000.0..200_000.0),
266 }
267 }
268 _ => self.rng.gen_range(1_000.0..10_000.0),
269 };
270
271 Decimal::from_f64_retain(base_balance).unwrap_or(Decimal::ZERO)
272 }
273
274 fn generate_account_number(&mut self) -> String {
276 self.account_counter += 1;
277 format!("****{:04}", self.account_counter % 10000)
278 }
279
280 fn generate_routing_number(&mut self) -> String {
282 let routing_prefixes = [
283 "021", "026", "031", "041", "051", "061", "071", "081", "091",
284 ];
285 let prefix = routing_prefixes.choose(&mut self.rng).unwrap();
286 format!("{}{:06}", prefix, self.rng.gen_range(0..1_000_000))
287 }
288
289 fn get_customer_currency(&self, customer: &BankingCustomer) -> String {
291 match customer.residence_country.as_str() {
292 "US" => "USD",
293 "GB" => "GBP",
294 "CA" => "CAD",
295 "DE" | "FR" | "NL" => "EUR",
296 "JP" => "JPY",
297 "AU" => "AUD",
298 "CH" => "CHF",
299 "SG" => "SGD",
300 _ => "USD",
301 }
302 .to_string()
303 }
304
305 fn random_opening_date(&mut self, onboarding: NaiveDate) -> NaiveDate {
307 let days_after: i64 = self.rng.gen_range(30..365);
308 onboarding + chrono::Duration::days(days_after)
309 }
310
311 fn weighted_select<T: Copy>(&mut self, options: &[(T, f64)]) -> T {
313 let total: f64 = options.iter().map(|(_, w)| w).sum();
314 let roll: f64 = self.rng.gen::<f64>() * total;
315 let mut cumulative = 0.0;
316
317 for (item, weight) in options {
318 cumulative += weight;
319 if roll < cumulative {
320 return *item;
321 }
322 }
323
324 options.last().unwrap().0
325 }
326}
327
328#[cfg(test)]
329mod tests {
330 use super::*;
331 use chrono::NaiveDate;
332 use uuid::Uuid;
333
334 #[test]
335 fn test_account_generation() {
336 let config = BankingConfig::small();
337 let mut customer_gen = crate::generators::CustomerGenerator::new(config.clone(), 12345);
338 let mut customers = customer_gen.generate_all();
339
340 let mut account_gen = AccountGenerator::new(config, 12345);
341 let accounts = account_gen.generate_for_customers(&mut customers);
342
343 assert!(!accounts.is_empty());
344
345 for customer in &customers {
347 assert!(!customer.account_ids.is_empty());
348 }
349 }
350
351 #[test]
352 fn test_account_features() {
353 let config = BankingConfig::default();
354 let mut gen = AccountGenerator::new(config, 12345);
355
356 let customer = BankingCustomer::new_retail(
357 Uuid::new_v4(),
358 "Test",
359 "User",
360 "US",
361 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
362 );
363
364 let features = gen.generate_features(&customer, true);
365 assert!(features.online_banking);
366 }
367}