Skip to main content

datasynth_banking/generators/
account_generator.rs

1//! Account generator for banking data.
2
3use 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
13/// Generator for bank accounts.
14pub struct AccountGenerator {
15    config: BankingConfig,
16    rng: ChaCha8Rng,
17    uuid_factory: DeterministicUuidFactory,
18    account_counter: u64,
19}
20
21impl AccountGenerator {
22    /// Create a new account generator.
23    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            ), // Reuse for banking accounts
31            account_counter: 0,
32        }
33    }
34
35    /// Generate accounts for all customers.
36    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    /// Generate accounts for a single customer.
54    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        // Primary checking account
60        accounts.push(self.generate_primary_account(customer));
61
62        // Additional accounts based on persona
63        for i in 1..account_count {
64            accounts.push(self.generate_secondary_account(customer, i));
65        }
66
67        accounts
68    }
69
70    /// Determine number of accounts for customer.
71    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        // Adjust for persona
80        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    /// Generate primary account for customer.
107    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        // Set appropriate features
128        account.features = self.generate_features(customer, true);
129
130        // Set initial balance
131        account.current_balance = self.generate_initial_balance(customer);
132        account.available_balance = account.current_balance;
133
134        // Set routing info for US accounts
135        if customer.residence_country == "US" {
136            account.routing_number = Some(self.generate_routing_number());
137        }
138
139        account
140    }
141
142    /// Generate secondary account for customer.
143    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        // Secondary accounts have reduced features
163        account.features = self.generate_features(customer, false);
164
165        // Set initial balance
166        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    /// Select account type for secondary account.
174    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    /// Generate account features.
203    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        // Adjust based on config
226        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        // Non-primary accounts have fewer features
235        if !is_primary {
236            features.debit_card = false;
237            features.check_writing = false;
238        }
239
240        features
241    }
242
243    /// Generate initial balance.
244    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    /// Generate account number.
275    fn generate_account_number(&mut self) -> String {
276        self.account_counter += 1;
277        format!("****{:04}", self.account_counter % 10000)
278    }
279
280    /// Generate routing number.
281    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    /// Get customer's currency.
290    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    /// Generate random opening date after onboarding.
306    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    /// Weighted random selection.
312    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        // Every customer should have at least one account
346        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}