1use chrono::NaiveDate;
4use datasynth_core::models::{
5 CreditRating, Customer, CustomerPaymentBehavior, CustomerPool, PaymentTerms,
6};
7use rand::prelude::*;
8use rand_chacha::ChaCha8Rng;
9use rust_decimal::Decimal;
10
11#[derive(Debug, Clone)]
13pub struct CustomerGeneratorConfig {
14 pub credit_rating_distribution: Vec<(CreditRating, f64)>,
16 pub payment_behavior_distribution: Vec<(CustomerPaymentBehavior, f64)>,
18 pub payment_terms_distribution: Vec<(PaymentTerms, f64)>,
20 pub intercompany_rate: f64,
22 pub default_country: String,
24 pub default_currency: String,
26 pub credit_limits: Vec<(CreditRating, Decimal, Decimal)>,
28}
29
30impl Default for CustomerGeneratorConfig {
31 fn default() -> Self {
32 Self {
33 credit_rating_distribution: vec![
34 (CreditRating::AAA, 0.05),
35 (CreditRating::AA, 0.10),
36 (CreditRating::A, 0.25),
37 (CreditRating::BBB, 0.30),
38 (CreditRating::BB, 0.15),
39 (CreditRating::B, 0.10),
40 (CreditRating::CCC, 0.04),
41 (CreditRating::D, 0.01),
42 ],
43 payment_behavior_distribution: vec![
44 (CustomerPaymentBehavior::EarlyPayer, 0.15),
45 (CustomerPaymentBehavior::OnTime, 0.45),
46 (CustomerPaymentBehavior::SlightlyLate, 0.25),
47 (CustomerPaymentBehavior::OftenLate, 0.10),
48 (CustomerPaymentBehavior::HighRisk, 0.05),
49 ],
50 payment_terms_distribution: vec![
51 (PaymentTerms::Net30, 0.50),
52 (PaymentTerms::Net60, 0.20),
53 (PaymentTerms::TwoTenNet30, 0.20),
54 (PaymentTerms::Net15, 0.05),
55 (PaymentTerms::Immediate, 0.05),
56 ],
57 intercompany_rate: 0.05,
58 default_country: "US".to_string(),
59 default_currency: "USD".to_string(),
60 credit_limits: vec![
61 (
62 CreditRating::AAA,
63 Decimal::from(1_000_000),
64 Decimal::from(10_000_000),
65 ),
66 (
67 CreditRating::AA,
68 Decimal::from(500_000),
69 Decimal::from(2_000_000),
70 ),
71 (
72 CreditRating::A,
73 Decimal::from(250_000),
74 Decimal::from(1_000_000),
75 ),
76 (
77 CreditRating::BBB,
78 Decimal::from(100_000),
79 Decimal::from(500_000),
80 ),
81 (
82 CreditRating::BB,
83 Decimal::from(50_000),
84 Decimal::from(250_000),
85 ),
86 (
87 CreditRating::B,
88 Decimal::from(25_000),
89 Decimal::from(100_000),
90 ),
91 (
92 CreditRating::CCC,
93 Decimal::from(10_000),
94 Decimal::from(50_000),
95 ),
96 (CreditRating::D, Decimal::from(0), Decimal::from(10_000)),
97 ],
98 }
99 }
100}
101
102const CUSTOMER_NAME_TEMPLATES: &[(&str, &[&str])] = &[
104 (
105 "Retail",
106 &[
107 "Consumer Goods Corp.",
108 "Retail Solutions Inc.",
109 "Shop Direct Ltd.",
110 "Market Leaders LLC",
111 "Consumer Brands Group",
112 "Retail Partners Co.",
113 "Shopping Networks Inc.",
114 "Direct Sales Corp.",
115 ],
116 ),
117 (
118 "Manufacturing",
119 &[
120 "Industrial Manufacturing Inc.",
121 "Production Systems Corp.",
122 "Assembly Technologies LLC",
123 "Manufacturing Partners Group",
124 "Factory Solutions Ltd.",
125 "Production Line Inc.",
126 "Industrial Works Corp.",
127 "Manufacturing Excellence Co.",
128 ],
129 ),
130 (
131 "Healthcare",
132 &[
133 "Healthcare Systems Inc.",
134 "Medical Solutions Corp.",
135 "Health Partners LLC",
136 "Medical Equipment Group",
137 "Healthcare Providers Ltd.",
138 "Clinical Services Inc.",
139 "Health Networks Corp.",
140 "Medical Supplies Co.",
141 ],
142 ),
143 (
144 "Technology",
145 &[
146 "Tech Innovations Inc.",
147 "Digital Solutions Corp.",
148 "Software Systems LLC",
149 "Technology Partners Group",
150 "IT Solutions Ltd.",
151 "Tech Enterprises Inc.",
152 "Digital Networks Corp.",
153 "Innovation Labs Co.",
154 ],
155 ),
156 (
157 "Financial",
158 &[
159 "Financial Services Inc.",
160 "Banking Solutions Corp.",
161 "Investment Partners LLC",
162 "Financial Networks Group",
163 "Capital Services Ltd.",
164 "Banking Partners Inc.",
165 "Finance Solutions Corp.",
166 "Investment Group Co.",
167 ],
168 ),
169 (
170 "Energy",
171 &[
172 "Energy Solutions Inc.",
173 "Power Systems Corp.",
174 "Renewable Partners LLC",
175 "Energy Networks Group",
176 "Utility Services Ltd.",
177 "Power Generation Inc.",
178 "Energy Partners Corp.",
179 "Sustainable Energy Co.",
180 ],
181 ),
182 (
183 "Transportation",
184 &[
185 "Transport Solutions Inc.",
186 "Logistics Systems Corp.",
187 "Freight Partners LLC",
188 "Transportation Networks Group",
189 "Shipping Services Ltd.",
190 "Fleet Management Inc.",
191 "Logistics Partners Corp.",
192 "Transport Dynamics Co.",
193 ],
194 ),
195 (
196 "Construction",
197 &[
198 "Construction Solutions Inc.",
199 "Building Systems Corp.",
200 "Development Partners LLC",
201 "Construction Group Ltd.",
202 "Building Services Inc.",
203 "Property Development Corp.",
204 "Construction Partners Co.",
205 "Infrastructure Systems LLC",
206 ],
207 ),
208];
209
210pub struct CustomerGenerator {
212 rng: ChaCha8Rng,
213 seed: u64,
214 config: CustomerGeneratorConfig,
215 customer_counter: usize,
216}
217
218impl CustomerGenerator {
219 pub fn new(seed: u64) -> Self {
221 Self::with_config(seed, CustomerGeneratorConfig::default())
222 }
223
224 pub fn with_config(seed: u64, config: CustomerGeneratorConfig) -> Self {
226 Self {
227 rng: ChaCha8Rng::seed_from_u64(seed),
228 seed,
229 config,
230 customer_counter: 0,
231 }
232 }
233
234 pub fn generate_customer(
236 &mut self,
237 company_code: &str,
238 _effective_date: NaiveDate,
239 ) -> Customer {
240 self.customer_counter += 1;
241
242 let customer_id = format!("C-{:06}", self.customer_counter);
243 let (_industry, name) = self.select_customer_name();
244
245 let mut customer = Customer::new(
246 &customer_id,
247 name,
248 datasynth_core::models::CustomerType::Corporate,
249 );
250
251 customer.country = self.config.default_country.clone();
252 customer.currency = self.config.default_currency.clone();
253 customer.credit_rating = self.select_credit_rating();
257 customer.credit_limit = self.generate_credit_limit(&customer.credit_rating);
258
259 customer.payment_behavior = self.select_payment_behavior();
261
262 customer.payment_terms = self.select_payment_terms();
264
265 if self.rng.gen::<f64>() < self.config.intercompany_rate {
267 customer.is_intercompany = true;
268 customer.intercompany_code = Some(format!("IC-{}", company_code));
269 }
270
271 customer
274 }
275
276 pub fn generate_intercompany_customer(
278 &mut self,
279 company_code: &str,
280 partner_company_code: &str,
281 effective_date: NaiveDate,
282 ) -> Customer {
283 let mut customer = self.generate_customer(company_code, effective_date);
284 customer.is_intercompany = true;
285 customer.intercompany_code = Some(partner_company_code.to_string());
286 customer.name = format!("{} - IC", partner_company_code);
287 customer.credit_rating = CreditRating::AAA; customer.credit_limit = Decimal::from(100_000_000); customer.payment_behavior = CustomerPaymentBehavior::OnTime;
290 customer
291 }
292
293 pub fn generate_customer_with_credit(
295 &mut self,
296 company_code: &str,
297 credit_rating: CreditRating,
298 credit_limit: Decimal,
299 effective_date: NaiveDate,
300 ) -> Customer {
301 let mut customer = self.generate_customer(company_code, effective_date);
302 customer.credit_rating = credit_rating;
303 customer.credit_limit = credit_limit;
304
305 customer.payment_behavior = match credit_rating {
307 CreditRating::AAA | CreditRating::AA => {
308 if self.rng.gen::<f64>() < 0.7 {
309 CustomerPaymentBehavior::EarlyPayer
310 } else {
311 CustomerPaymentBehavior::OnTime
312 }
313 }
314 CreditRating::A | CreditRating::BBB => CustomerPaymentBehavior::OnTime,
315 CreditRating::BB | CreditRating::B => CustomerPaymentBehavior::SlightlyLate,
316 CreditRating::CCC | CreditRating::CC => CustomerPaymentBehavior::OftenLate,
317 CreditRating::C | CreditRating::D => CustomerPaymentBehavior::HighRisk,
318 };
319
320 customer
321 }
322
323 pub fn generate_customer_pool(
325 &mut self,
326 count: usize,
327 company_code: &str,
328 effective_date: NaiveDate,
329 ) -> CustomerPool {
330 let mut pool = CustomerPool::new();
331
332 for _ in 0..count {
333 let customer = self.generate_customer(company_code, effective_date);
334 pool.add_customer(customer);
335 }
336
337 pool
338 }
339
340 pub fn generate_customer_pool_with_ic(
342 &mut self,
343 count: usize,
344 company_code: &str,
345 partner_company_codes: &[String],
346 effective_date: NaiveDate,
347 ) -> CustomerPool {
348 let mut pool = CustomerPool::new();
349
350 let regular_count = count.saturating_sub(partner_company_codes.len());
352 for _ in 0..regular_count {
353 let customer = self.generate_customer(company_code, effective_date);
354 pool.add_customer(customer);
355 }
356
357 for partner in partner_company_codes {
359 let customer =
360 self.generate_intercompany_customer(company_code, partner, effective_date);
361 pool.add_customer(customer);
362 }
363
364 pool
365 }
366
367 pub fn generate_diverse_pool(
369 &mut self,
370 count: usize,
371 company_code: &str,
372 effective_date: NaiveDate,
373 ) -> CustomerPool {
374 let mut pool = CustomerPool::new();
375
376 let rating_counts = [
378 (CreditRating::AAA, (count as f64 * 0.05) as usize),
379 (CreditRating::AA, (count as f64 * 0.10) as usize),
380 (CreditRating::A, (count as f64 * 0.20) as usize),
381 (CreditRating::BBB, (count as f64 * 0.30) as usize),
382 (CreditRating::BB, (count as f64 * 0.15) as usize),
383 (CreditRating::B, (count as f64 * 0.10) as usize),
384 (CreditRating::CCC, (count as f64 * 0.07) as usize),
385 (CreditRating::D, (count as f64 * 0.03) as usize),
386 ];
387
388 for (rating, rating_count) in rating_counts {
389 for _ in 0..rating_count {
390 let credit_limit = self.generate_credit_limit(&rating);
391 let customer = self.generate_customer_with_credit(
392 company_code,
393 rating,
394 credit_limit,
395 effective_date,
396 );
397 pool.add_customer(customer);
398 }
399 }
400
401 while pool.customers.len() < count {
403 let customer = self.generate_customer(company_code, effective_date);
404 pool.add_customer(customer);
405 }
406
407 pool
408 }
409
410 fn select_customer_name(&mut self) -> (&'static str, &'static str) {
412 let industry_idx = self.rng.gen_range(0..CUSTOMER_NAME_TEMPLATES.len());
413 let (industry, names) = CUSTOMER_NAME_TEMPLATES[industry_idx];
414 let name_idx = self.rng.gen_range(0..names.len());
415 (industry, names[name_idx])
416 }
417
418 fn select_credit_rating(&mut self) -> CreditRating {
420 let roll: f64 = self.rng.gen();
421 let mut cumulative = 0.0;
422
423 for (rating, prob) in &self.config.credit_rating_distribution {
424 cumulative += prob;
425 if roll < cumulative {
426 return *rating;
427 }
428 }
429
430 CreditRating::BBB
431 }
432
433 fn generate_credit_limit(&mut self, rating: &CreditRating) -> Decimal {
435 for (r, min, max) in &self.config.credit_limits {
436 if r == rating {
437 let range = (*max - *min).to_string().parse::<f64>().unwrap_or(0.0);
438 let offset = Decimal::from_f64_retain(self.rng.gen::<f64>() * range)
439 .unwrap_or(Decimal::ZERO);
440 return *min + offset;
441 }
442 }
443
444 Decimal::from(100_000)
445 }
446
447 fn select_payment_behavior(&mut self) -> CustomerPaymentBehavior {
449 let roll: f64 = self.rng.gen();
450 let mut cumulative = 0.0;
451
452 for (behavior, prob) in &self.config.payment_behavior_distribution {
453 cumulative += prob;
454 if roll < cumulative {
455 return *behavior;
456 }
457 }
458
459 CustomerPaymentBehavior::OnTime
460 }
461
462 fn select_payment_terms(&mut self) -> PaymentTerms {
464 let roll: f64 = self.rng.gen();
465 let mut cumulative = 0.0;
466
467 for (terms, prob) in &self.config.payment_terms_distribution {
468 cumulative += prob;
469 if roll < cumulative {
470 return *terms;
471 }
472 }
473
474 PaymentTerms::Net30
475 }
476
477 fn generate_address(&mut self) -> String {
479 let street_num = self.rng.gen_range(1..9999);
480 let streets = [
481 "Corporate Dr",
482 "Business Center",
483 "Commerce Way",
484 "Executive Plaza",
485 "Industry Park",
486 "Trade Center",
487 ];
488 let cities = [
489 "New York",
490 "Los Angeles",
491 "Chicago",
492 "Houston",
493 "Phoenix",
494 "Philadelphia",
495 "San Antonio",
496 "San Diego",
497 ];
498 let states = ["NY", "CA", "IL", "TX", "AZ", "PA", "TX", "CA"];
499
500 let idx = self.rng.gen_range(0..cities.len());
501 let street_idx = self.rng.gen_range(0..streets.len());
502 let zip = self.rng.gen_range(10000..99999);
503
504 format!(
505 "{} {}, {}, {} {}",
506 street_num, streets[street_idx], cities[idx], states[idx], zip
507 )
508 }
509
510 fn generate_contact_name(&mut self) -> String {
512 let first_names = [
513 "John", "Jane", "Michael", "Sarah", "David", "Emily", "Robert", "Lisa",
514 ];
515 let last_names = [
516 "Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis",
517 ];
518
519 let first = first_names[self.rng.gen_range(0..first_names.len())];
520 let last = last_names[self.rng.gen_range(0..last_names.len())];
521
522 format!("{} {}", first, last)
523 }
524
525 fn generate_contact_email(&mut self, company_name: &str) -> String {
527 let domain = company_name
528 .to_lowercase()
529 .replace([' ', '.', ','], "")
530 .chars()
531 .filter(|c| c.is_alphanumeric())
532 .take(15)
533 .collect::<String>();
534
535 format!("contact@{}.com", domain)
536 }
537
538 pub fn reset(&mut self) {
540 self.rng = ChaCha8Rng::seed_from_u64(self.seed);
541 self.customer_counter = 0;
542 }
543}
544
545#[cfg(test)]
546mod tests {
547 use super::*;
548
549 #[test]
550 fn test_customer_generation() {
551 let mut gen = CustomerGenerator::new(42);
552 let customer = gen.generate_customer("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
553
554 assert!(!customer.customer_id.is_empty());
555 assert!(!customer.name.is_empty());
556 assert!(customer.credit_limit > Decimal::ZERO);
557 }
558
559 #[test]
560 fn test_customer_pool_generation() {
561 let mut gen = CustomerGenerator::new(42);
562 let pool =
563 gen.generate_customer_pool(20, "1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
564
565 assert_eq!(pool.customers.len(), 20);
566 }
567
568 #[test]
569 fn test_intercompany_customer() {
570 let mut gen = CustomerGenerator::new(42);
571 let customer = gen.generate_intercompany_customer(
572 "1000",
573 "2000",
574 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
575 );
576
577 assert!(customer.is_intercompany);
578 assert_eq!(customer.intercompany_code, Some("2000".to_string()));
579 assert_eq!(customer.credit_rating, CreditRating::AAA);
580 }
581
582 #[test]
583 fn test_diverse_pool() {
584 let mut gen = CustomerGenerator::new(42);
585 let pool =
586 gen.generate_diverse_pool(100, "1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
587
588 let aaa_count = pool
590 .customers
591 .iter()
592 .filter(|c| c.credit_rating == CreditRating::AAA)
593 .count();
594 let d_count = pool
595 .customers
596 .iter()
597 .filter(|c| c.credit_rating == CreditRating::D)
598 .count();
599
600 assert!(aaa_count > 0);
601 assert!(d_count > 0);
602 }
603
604 #[test]
605 fn test_deterministic_generation() {
606 let mut gen1 = CustomerGenerator::new(42);
607 let mut gen2 = CustomerGenerator::new(42);
608
609 let customer1 =
610 gen1.generate_customer("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
611 let customer2 =
612 gen2.generate_customer("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
613
614 assert_eq!(customer1.customer_id, customer2.customer_id);
615 assert_eq!(customer1.name, customer2.name);
616 assert_eq!(customer1.credit_rating, customer2.credit_rating);
617 }
618
619 #[test]
620 fn test_customer_with_specific_credit() {
621 let mut gen = CustomerGenerator::new(42);
622 let customer = gen.generate_customer_with_credit(
623 "1000",
624 CreditRating::D,
625 Decimal::from(5000),
626 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
627 );
628
629 assert_eq!(customer.credit_rating, CreditRating::D);
630 assert_eq!(customer.credit_limit, Decimal::from(5000));
631 assert_eq!(customer.payment_behavior, CustomerPaymentBehavior::HighRisk);
632 }
633}