1use std::collections::HashSet;
10
11use chrono::NaiveDate;
12use datasynth_core::models::{
13 ChurnReason, CreditRating, Customer, CustomerEngagement, CustomerLifecycleStage,
14 CustomerPaymentBehavior, CustomerPool, CustomerValueSegment, PaymentTerms, RiskTrigger,
15 SegmentedCustomer, SegmentedCustomerPool,
16};
17use datasynth_core::utils::seeded_rng;
18use rand::prelude::*;
19use rand_chacha::ChaCha8Rng;
20use rust_decimal::Decimal;
21use tracing::debug;
22
23use crate::coa_generator::CoAFramework;
24
25#[derive(Debug, Clone)]
27pub struct CustomerGeneratorConfig {
28 pub credit_rating_distribution: Vec<(CreditRating, f64)>,
30 pub payment_behavior_distribution: Vec<(CustomerPaymentBehavior, f64)>,
32 pub payment_terms_distribution: Vec<(PaymentTerms, f64)>,
34 pub intercompany_rate: f64,
36 pub default_country: String,
38 pub default_currency: String,
40 pub credit_limits: Vec<(CreditRating, Decimal, Decimal)>,
42}
43
44impl Default for CustomerGeneratorConfig {
45 fn default() -> Self {
46 Self {
47 credit_rating_distribution: vec![
48 (CreditRating::AAA, 0.05),
49 (CreditRating::AA, 0.10),
50 (CreditRating::A, 0.25),
51 (CreditRating::BBB, 0.30),
52 (CreditRating::BB, 0.15),
53 (CreditRating::B, 0.10),
54 (CreditRating::CCC, 0.04),
55 (CreditRating::D, 0.01),
56 ],
57 payment_behavior_distribution: vec![
58 (CustomerPaymentBehavior::EarlyPayer, 0.15),
59 (CustomerPaymentBehavior::OnTime, 0.45),
60 (CustomerPaymentBehavior::SlightlyLate, 0.25),
61 (CustomerPaymentBehavior::OftenLate, 0.10),
62 (CustomerPaymentBehavior::HighRisk, 0.05),
63 ],
64 payment_terms_distribution: vec![
65 (PaymentTerms::Net30, 0.50),
66 (PaymentTerms::Net60, 0.20),
67 (PaymentTerms::TwoTenNet30, 0.20),
68 (PaymentTerms::Net15, 0.05),
69 (PaymentTerms::Immediate, 0.05),
70 ],
71 intercompany_rate: 0.05,
72 default_country: "US".to_string(),
73 default_currency: "USD".to_string(),
74 credit_limits: vec![
75 (
76 CreditRating::AAA,
77 Decimal::from(1_000_000),
78 Decimal::from(10_000_000),
79 ),
80 (
81 CreditRating::AA,
82 Decimal::from(500_000),
83 Decimal::from(2_000_000),
84 ),
85 (
86 CreditRating::A,
87 Decimal::from(250_000),
88 Decimal::from(1_000_000),
89 ),
90 (
91 CreditRating::BBB,
92 Decimal::from(100_000),
93 Decimal::from(500_000),
94 ),
95 (
96 CreditRating::BB,
97 Decimal::from(50_000),
98 Decimal::from(250_000),
99 ),
100 (
101 CreditRating::B,
102 Decimal::from(25_000),
103 Decimal::from(100_000),
104 ),
105 (
106 CreditRating::CCC,
107 Decimal::from(10_000),
108 Decimal::from(50_000),
109 ),
110 (CreditRating::D, Decimal::from(0), Decimal::from(10_000)),
111 ],
112 }
113 }
114}
115
116#[derive(Debug, Clone)]
118pub struct CustomerSegmentationConfig {
119 pub enabled: bool,
121 pub segment_distribution: SegmentDistribution,
123 pub lifecycle_distribution: LifecycleDistribution,
125 pub referral_config: ReferralConfig,
127 pub hierarchy_config: HierarchyConfig,
129 pub industry_distribution: Vec<(String, f64)>,
131}
132
133impl Default for CustomerSegmentationConfig {
134 fn default() -> Self {
135 Self {
136 enabled: false,
137 segment_distribution: SegmentDistribution::default(),
138 lifecycle_distribution: LifecycleDistribution::default(),
139 referral_config: ReferralConfig::default(),
140 hierarchy_config: HierarchyConfig::default(),
141 industry_distribution: vec![
142 ("Technology".to_string(), 0.20),
143 ("Manufacturing".to_string(), 0.15),
144 ("Retail".to_string(), 0.15),
145 ("Healthcare".to_string(), 0.12),
146 ("Financial".to_string(), 0.12),
147 ("Energy".to_string(), 0.08),
148 ("Transportation".to_string(), 0.08),
149 ("Construction".to_string(), 0.10),
150 ],
151 }
152 }
153}
154
155#[derive(Debug, Clone)]
157pub struct SegmentDistribution {
158 pub enterprise: f64,
160 pub mid_market: f64,
162 pub smb: f64,
164 pub consumer: f64,
166}
167
168impl Default for SegmentDistribution {
169 fn default() -> Self {
170 Self {
171 enterprise: 0.05,
172 mid_market: 0.20,
173 smb: 0.50,
174 consumer: 0.25,
175 }
176 }
177}
178
179impl SegmentDistribution {
180 pub fn validate(&self) -> Result<(), String> {
182 let sum = self.enterprise + self.mid_market + self.smb + self.consumer;
183 if (sum - 1.0).abs() > 0.01 {
184 Err(format!("Segment distribution must sum to 1.0, got {}", sum))
185 } else {
186 Ok(())
187 }
188 }
189
190 pub fn select(&self, roll: f64) -> CustomerValueSegment {
192 let mut cumulative = 0.0;
193
194 cumulative += self.enterprise;
195 if roll < cumulative {
196 return CustomerValueSegment::Enterprise;
197 }
198
199 cumulative += self.mid_market;
200 if roll < cumulative {
201 return CustomerValueSegment::MidMarket;
202 }
203
204 cumulative += self.smb;
205 if roll < cumulative {
206 return CustomerValueSegment::Smb;
207 }
208
209 CustomerValueSegment::Consumer
210 }
211}
212
213#[derive(Debug, Clone)]
215pub struct LifecycleDistribution {
216 pub prospect: f64,
218 pub new: f64,
220 pub growth: f64,
222 pub mature: f64,
224 pub at_risk: f64,
226 pub churned: f64,
228}
229
230impl Default for LifecycleDistribution {
231 fn default() -> Self {
232 Self {
233 prospect: 0.0, new: 0.10,
235 growth: 0.15,
236 mature: 0.60,
237 at_risk: 0.10,
238 churned: 0.05,
239 }
240 }
241}
242
243impl LifecycleDistribution {
244 pub fn validate(&self) -> Result<(), String> {
246 let sum =
247 self.prospect + self.new + self.growth + self.mature + self.at_risk + self.churned;
248 if (sum - 1.0).abs() > 0.01 {
249 Err(format!(
250 "Lifecycle distribution must sum to 1.0, got {}",
251 sum
252 ))
253 } else {
254 Ok(())
255 }
256 }
257}
258
259#[derive(Debug, Clone)]
261pub struct ReferralConfig {
262 pub enabled: bool,
264 pub referral_rate: f64,
266 pub max_referrals_per_customer: usize,
268}
269
270impl Default for ReferralConfig {
271 fn default() -> Self {
272 Self {
273 enabled: true,
274 referral_rate: 0.15,
275 max_referrals_per_customer: 5,
276 }
277 }
278}
279
280#[derive(Debug, Clone)]
282pub struct HierarchyConfig {
283 pub enabled: bool,
285 pub hierarchy_rate: f64,
287 pub max_depth: usize,
289 pub billing_consolidation_rate: f64,
291}
292
293impl Default for HierarchyConfig {
294 fn default() -> Self {
295 Self {
296 enabled: true,
297 hierarchy_rate: 0.30,
298 max_depth: 3,
299 billing_consolidation_rate: 0.50,
300 }
301 }
302}
303
304const CUSTOMER_NAME_TEMPLATES: &[(&str, &[&str])] = &[
306 (
307 "Retail",
308 &[
309 "Consumer Goods Corp.",
310 "Retail Solutions Inc.",
311 "Shop Direct Ltd.",
312 "Market Leaders LLC",
313 "Consumer Brands Group",
314 "Retail Partners Co.",
315 "Shopping Networks Inc.",
316 "Direct Sales Corp.",
317 ],
318 ),
319 (
320 "Manufacturing",
321 &[
322 "Industrial Manufacturing Inc.",
323 "Production Systems Corp.",
324 "Assembly Technologies LLC",
325 "Manufacturing Partners Group",
326 "Factory Solutions Ltd.",
327 "Production Line Inc.",
328 "Industrial Works Corp.",
329 "Manufacturing Excellence Co.",
330 ],
331 ),
332 (
333 "Healthcare",
334 &[
335 "Healthcare Systems Inc.",
336 "Medical Solutions Corp.",
337 "Health Partners LLC",
338 "Medical Equipment Group",
339 "Healthcare Providers Ltd.",
340 "Clinical Services Inc.",
341 "Health Networks Corp.",
342 "Medical Supplies Co.",
343 ],
344 ),
345 (
346 "Technology",
347 &[
348 "Tech Innovations Inc.",
349 "Digital Solutions Corp.",
350 "Software Systems LLC",
351 "Technology Partners Group",
352 "IT Solutions Ltd.",
353 "Tech Enterprises Inc.",
354 "Digital Networks Corp.",
355 "Innovation Labs Co.",
356 ],
357 ),
358 (
359 "Financial",
360 &[
361 "Financial Services Inc.",
362 "Banking Solutions Corp.",
363 "Investment Partners LLC",
364 "Financial Networks Group",
365 "Capital Services Ltd.",
366 "Banking Partners Inc.",
367 "Finance Solutions Corp.",
368 "Investment Group Co.",
369 ],
370 ),
371 (
372 "Energy",
373 &[
374 "Energy Solutions Inc.",
375 "Power Systems Corp.",
376 "Renewable Partners LLC",
377 "Energy Networks Group",
378 "Utility Services Ltd.",
379 "Power Generation Inc.",
380 "Energy Partners Corp.",
381 "Sustainable Energy Co.",
382 ],
383 ),
384 (
385 "Transportation",
386 &[
387 "Transport Solutions Inc.",
388 "Logistics Systems Corp.",
389 "Freight Partners LLC",
390 "Transportation Networks Group",
391 "Shipping Services Ltd.",
392 "Fleet Management Inc.",
393 "Logistics Partners Corp.",
394 "Transport Dynamics Co.",
395 ],
396 ),
397 (
398 "Construction",
399 &[
400 "Construction Solutions Inc.",
401 "Building Systems Corp.",
402 "Development Partners LLC",
403 "Construction Group Ltd.",
404 "Building Services Inc.",
405 "Property Development Corp.",
406 "Construction Partners Co.",
407 "Infrastructure Systems LLC",
408 ],
409 ),
410];
411
412pub struct CustomerGenerator {
414 rng: ChaCha8Rng,
415 seed: u64,
416 config: CustomerGeneratorConfig,
417 customer_counter: usize,
418 segmentation_config: CustomerSegmentationConfig,
420 country_pack: Option<datasynth_core::CountryPack>,
422 coa_framework: CoAFramework,
424 used_names: HashSet<String>,
426}
427
428impl CustomerGenerator {
429 pub fn new(seed: u64) -> Self {
431 Self::with_config(seed, CustomerGeneratorConfig::default())
432 }
433
434 pub fn with_config(seed: u64, config: CustomerGeneratorConfig) -> Self {
436 Self {
437 rng: seeded_rng(seed, 0),
438 seed,
439 config,
440 customer_counter: 0,
441 segmentation_config: CustomerSegmentationConfig::default(),
442 country_pack: None,
443 coa_framework: CoAFramework::UsGaap,
444 used_names: HashSet::new(),
445 }
446 }
447
448 pub fn with_segmentation_config(
450 seed: u64,
451 config: CustomerGeneratorConfig,
452 segmentation_config: CustomerSegmentationConfig,
453 ) -> Self {
454 Self {
455 rng: seeded_rng(seed, 0),
456 seed,
457 config,
458 customer_counter: 0,
459 segmentation_config,
460 country_pack: None,
461 coa_framework: CoAFramework::UsGaap,
462 used_names: HashSet::new(),
463 }
464 }
465
466 pub fn set_coa_framework(&mut self, framework: CoAFramework) {
468 self.coa_framework = framework;
469 }
470
471 pub fn set_segmentation_config(&mut self, segmentation_config: CustomerSegmentationConfig) {
473 self.segmentation_config = segmentation_config;
474 }
475
476 pub fn set_country_pack(&mut self, pack: datasynth_core::CountryPack) {
478 self.country_pack = Some(pack);
479 }
480
481 pub fn generate_customer(
483 &mut self,
484 company_code: &str,
485 _effective_date: NaiveDate,
486 ) -> Customer {
487 self.customer_counter += 1;
488
489 let customer_id = format!("C-{:06}", self.customer_counter);
490 let (_industry, name) = self.select_customer_name_unique();
491
492 let mut customer = Customer::new(
493 &customer_id,
494 &name,
495 datasynth_core::models::CustomerType::Corporate,
496 );
497
498 customer.country = self.config.default_country.clone();
499 customer.currency = self.config.default_currency.clone();
500 customer.credit_rating = self.select_credit_rating();
504 customer.credit_limit = self.generate_credit_limit(&customer.credit_rating);
505
506 customer.payment_behavior = self.select_payment_behavior();
508
509 customer.payment_terms = self.select_payment_terms();
511
512 customer.auxiliary_gl_account = self.generate_auxiliary_gl_account();
514
515 if self.rng.random::<f64>() < self.config.intercompany_rate {
517 customer.is_intercompany = true;
518 customer.intercompany_code = Some(format!("IC-{}", company_code));
519 }
520
521 customer
524 }
525
526 pub fn generate_intercompany_customer(
528 &mut self,
529 company_code: &str,
530 partner_company_code: &str,
531 effective_date: NaiveDate,
532 ) -> Customer {
533 let mut customer = self.generate_customer(company_code, effective_date);
534 customer.is_intercompany = true;
535 customer.intercompany_code = Some(partner_company_code.to_string());
536 customer.name = format!("{} - IC", partner_company_code);
537 customer.credit_rating = CreditRating::AAA; customer.credit_limit = Decimal::from(100_000_000); customer.payment_behavior = CustomerPaymentBehavior::OnTime;
540 customer
541 }
542
543 pub fn generate_customer_with_credit(
545 &mut self,
546 company_code: &str,
547 credit_rating: CreditRating,
548 credit_limit: Decimal,
549 effective_date: NaiveDate,
550 ) -> Customer {
551 let mut customer = self.generate_customer(company_code, effective_date);
552 customer.credit_rating = credit_rating;
553 customer.credit_limit = credit_limit;
554
555 customer.payment_behavior = match credit_rating {
557 CreditRating::AAA | CreditRating::AA => {
558 if self.rng.random::<f64>() < 0.7 {
559 CustomerPaymentBehavior::EarlyPayer
560 } else {
561 CustomerPaymentBehavior::OnTime
562 }
563 }
564 CreditRating::A | CreditRating::BBB => CustomerPaymentBehavior::OnTime,
565 CreditRating::BB | CreditRating::B => CustomerPaymentBehavior::SlightlyLate,
566 CreditRating::CCC | CreditRating::CC => CustomerPaymentBehavior::OftenLate,
567 CreditRating::C | CreditRating::D => CustomerPaymentBehavior::HighRisk,
568 };
569
570 customer
571 }
572
573 pub fn generate_customer_pool(
575 &mut self,
576 count: usize,
577 company_code: &str,
578 effective_date: NaiveDate,
579 ) -> CustomerPool {
580 debug!(count, company_code, %effective_date, "Generating customer pool");
581 let mut pool = CustomerPool::new();
582
583 for _ in 0..count {
584 let customer = self.generate_customer(company_code, effective_date);
585 pool.add_customer(customer);
586 }
587
588 pool
589 }
590
591 pub fn generate_customer_pool_with_ic(
593 &mut self,
594 count: usize,
595 company_code: &str,
596 partner_company_codes: &[String],
597 effective_date: NaiveDate,
598 ) -> CustomerPool {
599 let mut pool = CustomerPool::new();
600
601 let regular_count = count.saturating_sub(partner_company_codes.len());
603 for _ in 0..regular_count {
604 let customer = self.generate_customer(company_code, effective_date);
605 pool.add_customer(customer);
606 }
607
608 for partner in partner_company_codes {
610 let customer =
611 self.generate_intercompany_customer(company_code, partner, effective_date);
612 pool.add_customer(customer);
613 }
614
615 pool
616 }
617
618 pub fn generate_diverse_pool(
620 &mut self,
621 count: usize,
622 company_code: &str,
623 effective_date: NaiveDate,
624 ) -> CustomerPool {
625 let mut pool = CustomerPool::new();
626
627 let rating_counts = [
629 (CreditRating::AAA, (count as f64 * 0.05) as usize),
630 (CreditRating::AA, (count as f64 * 0.10) as usize),
631 (CreditRating::A, (count as f64 * 0.20) as usize),
632 (CreditRating::BBB, (count as f64 * 0.30) as usize),
633 (CreditRating::BB, (count as f64 * 0.15) as usize),
634 (CreditRating::B, (count as f64 * 0.10) as usize),
635 (CreditRating::CCC, (count as f64 * 0.07) as usize),
636 (CreditRating::D, (count as f64 * 0.03) as usize),
637 ];
638
639 for (rating, rating_count) in rating_counts {
640 for _ in 0..rating_count {
641 let credit_limit = self.generate_credit_limit(&rating);
642 let customer = self.generate_customer_with_credit(
643 company_code,
644 rating,
645 credit_limit,
646 effective_date,
647 );
648 pool.add_customer(customer);
649 }
650 }
651
652 while pool.customers.len() < count {
654 let customer = self.generate_customer(company_code, effective_date);
655 pool.add_customer(customer);
656 }
657
658 pool
659 }
660
661 fn select_customer_name(&mut self) -> (&'static str, &'static str) {
663 let industry_idx = self.rng.random_range(0..CUSTOMER_NAME_TEMPLATES.len());
664 let (industry, names) = CUSTOMER_NAME_TEMPLATES[industry_idx];
665 let name_idx = self.rng.random_range(0..names.len());
666 (industry, names[name_idx])
667 }
668
669 fn select_customer_name_unique(&mut self) -> (&'static str, String) {
671 let (industry, name) = self.select_customer_name();
672 let mut name = name.to_string();
673
674 if self.used_names.contains(&name) {
675 let suffixes = [
676 " II",
677 " III",
678 " & Partners",
679 " Group",
680 " Holdings",
681 " International",
682 ];
683 let mut found_unique = false;
684 for suffix in &suffixes {
685 let candidate = format!("{}{}", name, suffix);
686 if !self.used_names.contains(&candidate) {
687 name = candidate;
688 found_unique = true;
689 break;
690 }
691 }
692 if !found_unique {
693 name = format!("{} #{}", name, self.customer_counter);
694 }
695 }
696
697 self.used_names.insert(name.clone());
698 (industry, name)
699 }
700
701 fn generate_auxiliary_gl_account(&self) -> Option<String> {
703 match self.coa_framework {
704 CoAFramework::FrenchPcg => {
705 Some(format!("411{:04}", self.customer_counter))
707 }
708 CoAFramework::GermanSkr04 => {
709 Some(format!(
711 "{}{:04}",
712 datasynth_core::skr::control_accounts::AR_CONTROL,
713 self.customer_counter
714 ))
715 }
716 CoAFramework::UsGaap => None,
717 }
718 }
719
720 fn select_credit_rating(&mut self) -> CreditRating {
722 let roll: f64 = self.rng.random();
723 let mut cumulative = 0.0;
724
725 for (rating, prob) in &self.config.credit_rating_distribution {
726 cumulative += prob;
727 if roll < cumulative {
728 return *rating;
729 }
730 }
731
732 CreditRating::BBB
733 }
734
735 fn generate_credit_limit(&mut self, rating: &CreditRating) -> Decimal {
737 for (r, min, max) in &self.config.credit_limits {
738 if r == rating {
739 let range = (*max - *min).to_string().parse::<f64>().unwrap_or(0.0);
740 let offset = Decimal::from_f64_retain(self.rng.random::<f64>() * range)
741 .unwrap_or(Decimal::ZERO);
742 return *min + offset;
743 }
744 }
745
746 Decimal::from(100_000)
747 }
748
749 fn select_payment_behavior(&mut self) -> CustomerPaymentBehavior {
751 let roll: f64 = self.rng.random();
752 let mut cumulative = 0.0;
753
754 for (behavior, prob) in &self.config.payment_behavior_distribution {
755 cumulative += prob;
756 if roll < cumulative {
757 return *behavior;
758 }
759 }
760
761 CustomerPaymentBehavior::OnTime
762 }
763
764 fn select_payment_terms(&mut self) -> PaymentTerms {
766 let roll: f64 = self.rng.random();
767 let mut cumulative = 0.0;
768
769 for (terms, prob) in &self.config.payment_terms_distribution {
770 cumulative += prob;
771 if roll < cumulative {
772 return *terms;
773 }
774 }
775
776 PaymentTerms::Net30
777 }
778
779 pub fn reset(&mut self) {
781 self.rng = seeded_rng(self.seed, 0);
782 self.customer_counter = 0;
783 self.used_names.clear();
784 }
785
786 pub fn generate_segmented_pool(
790 &mut self,
791 count: usize,
792 company_code: &str,
793 effective_date: NaiveDate,
794 total_annual_revenue: Decimal,
795 ) -> SegmentedCustomerPool {
796 let mut pool = SegmentedCustomerPool::new();
797
798 if !self.segmentation_config.enabled {
799 return pool;
800 }
801
802 let segment_counts = self.calculate_segment_counts(count);
804
805 let mut all_customer_ids: Vec<String> = Vec::new();
807 let mut parent_candidates: Vec<String> = Vec::new();
808
809 for (segment, segment_count) in segment_counts {
810 for _ in 0..segment_count {
811 let customer = self.generate_customer(company_code, effective_date);
812 let customer_id = customer.customer_id.clone();
813
814 let mut segmented =
815 self.create_segmented_customer(&customer, segment, effective_date);
816
817 segmented.industry = Some(self.select_industry());
819
820 segmented.annual_contract_value =
822 self.generate_acv(segment, total_annual_revenue, count);
823
824 if segment == CustomerValueSegment::Enterprise {
826 parent_candidates.push(customer_id.clone());
827 }
828
829 all_customer_ids.push(customer_id);
830 pool.add_customer(segmented);
831 }
832 }
833
834 if self.segmentation_config.referral_config.enabled {
836 self.build_referral_networks(&mut pool, &all_customer_ids);
837 }
838
839 if self.segmentation_config.hierarchy_config.enabled {
841 self.build_corporate_hierarchies(&mut pool, &all_customer_ids, &parent_candidates);
842 }
843
844 self.populate_engagement_metrics(&mut pool, effective_date);
846
847 pool.calculate_statistics();
849
850 pool
851 }
852
853 fn calculate_segment_counts(
855 &mut self,
856 total_count: usize,
857 ) -> Vec<(CustomerValueSegment, usize)> {
858 let dist = &self.segmentation_config.segment_distribution;
859 vec![
860 (
861 CustomerValueSegment::Enterprise,
862 (total_count as f64 * dist.enterprise) as usize,
863 ),
864 (
865 CustomerValueSegment::MidMarket,
866 (total_count as f64 * dist.mid_market) as usize,
867 ),
868 (
869 CustomerValueSegment::Smb,
870 (total_count as f64 * dist.smb) as usize,
871 ),
872 (
873 CustomerValueSegment::Consumer,
874 (total_count as f64 * dist.consumer) as usize,
875 ),
876 ]
877 }
878
879 fn create_segmented_customer(
881 &mut self,
882 customer: &Customer,
883 segment: CustomerValueSegment,
884 effective_date: NaiveDate,
885 ) -> SegmentedCustomer {
886 let lifecycle_stage = self.generate_lifecycle_stage(effective_date);
887
888 SegmentedCustomer::new(
889 &customer.customer_id,
890 &customer.name,
891 segment,
892 effective_date,
893 )
894 .with_lifecycle_stage(lifecycle_stage)
895 }
896
897 fn generate_lifecycle_stage(&mut self, effective_date: NaiveDate) -> CustomerLifecycleStage {
899 let dist = &self.segmentation_config.lifecycle_distribution;
900 let roll: f64 = self.rng.random();
901 let mut cumulative = 0.0;
902
903 cumulative += dist.prospect;
904 if roll < cumulative {
905 return CustomerLifecycleStage::Prospect {
906 conversion_probability: self.rng.random_range(0.1..0.4),
907 source: Some("Marketing".to_string()),
908 first_contact_date: effective_date
909 - chrono::Duration::days(self.rng.random_range(1..90)),
910 };
911 }
912
913 cumulative += dist.new;
914 if roll < cumulative {
915 return CustomerLifecycleStage::New {
916 first_order_date: effective_date
917 - chrono::Duration::days(self.rng.random_range(1..90)),
918 onboarding_complete: self.rng.random::<f64>() > 0.3,
919 };
920 }
921
922 cumulative += dist.growth;
923 if roll < cumulative {
924 return CustomerLifecycleStage::Growth {
925 since: effective_date - chrono::Duration::days(self.rng.random_range(90..365)),
926 growth_rate: self.rng.random_range(0.10..0.50),
927 };
928 }
929
930 cumulative += dist.mature;
931 if roll < cumulative {
932 return CustomerLifecycleStage::Mature {
933 stable_since: effective_date
934 - chrono::Duration::days(self.rng.random_range(365..1825)),
935 avg_annual_spend: Decimal::from(self.rng.random_range(10000..500000)),
936 };
937 }
938
939 cumulative += dist.at_risk;
940 if roll < cumulative {
941 let triggers = self.generate_risk_triggers();
942 return CustomerLifecycleStage::AtRisk {
943 triggers,
944 flagged_date: effective_date - chrono::Duration::days(self.rng.random_range(7..60)),
945 churn_probability: self.rng.random_range(0.3..0.8),
946 };
947 }
948
949 CustomerLifecycleStage::Churned {
951 last_activity: effective_date - chrono::Duration::days(self.rng.random_range(90..365)),
952 win_back_probability: self.rng.random_range(0.05..0.25),
953 reason: Some(self.generate_churn_reason()),
954 }
955 }
956
957 fn generate_risk_triggers(&mut self) -> Vec<RiskTrigger> {
959 let all_triggers = [
960 RiskTrigger::DecliningOrderFrequency,
961 RiskTrigger::DecliningOrderValue,
962 RiskTrigger::PaymentIssues,
963 RiskTrigger::Complaints,
964 RiskTrigger::ReducedEngagement,
965 RiskTrigger::ContractExpiring,
966 ];
967
968 let count = self.rng.random_range(1..=3);
969 let mut triggers = Vec::new();
970
971 for _ in 0..count {
972 let idx = self.rng.random_range(0..all_triggers.len());
973 triggers.push(all_triggers[idx].clone());
974 }
975
976 triggers
977 }
978
979 fn generate_churn_reason(&mut self) -> ChurnReason {
981 let roll: f64 = self.rng.random();
982 if roll < 0.30 {
983 ChurnReason::Competitor
984 } else if roll < 0.50 {
985 ChurnReason::Price
986 } else if roll < 0.65 {
987 ChurnReason::ServiceQuality
988 } else if roll < 0.75 {
989 ChurnReason::BudgetConstraints
990 } else if roll < 0.85 {
991 ChurnReason::ProductFit
992 } else if roll < 0.92 {
993 ChurnReason::Consolidation
994 } else {
995 ChurnReason::Unknown
996 }
997 }
998
999 fn select_industry(&mut self) -> String {
1001 let roll: f64 = self.rng.random();
1002 let mut cumulative = 0.0;
1003
1004 for (industry, prob) in &self.segmentation_config.industry_distribution {
1005 cumulative += prob;
1006 if roll < cumulative {
1007 return industry.clone();
1008 }
1009 }
1010
1011 "Other".to_string()
1012 }
1013
1014 fn generate_acv(
1016 &mut self,
1017 segment: CustomerValueSegment,
1018 total_revenue: Decimal,
1019 total_customers: usize,
1020 ) -> Decimal {
1021 let segment_revenue_share = segment.revenue_share();
1023 let segment_customer_share = segment.customer_share();
1024 let expected_customers_in_segment =
1025 (total_customers as f64 * segment_customer_share) as usize;
1026 let segment_total_revenue = total_revenue
1027 * Decimal::from_f64_retain(segment_revenue_share).unwrap_or(Decimal::ZERO);
1028
1029 let avg_acv = if expected_customers_in_segment > 0 {
1030 segment_total_revenue / Decimal::from(expected_customers_in_segment)
1031 } else {
1032 Decimal::from(10000)
1033 };
1034
1035 let variance = self.rng.random_range(0.5..1.5);
1037 avg_acv * Decimal::from_f64_retain(variance).unwrap_or(Decimal::ONE)
1038 }
1039
1040 fn build_referral_networks(
1042 &mut self,
1043 pool: &mut SegmentedCustomerPool,
1044 customer_ids: &[String],
1045 ) {
1046 let referral_rate = self.segmentation_config.referral_config.referral_rate;
1047 let max_referrals = self
1048 .segmentation_config
1049 .referral_config
1050 .max_referrals_per_customer;
1051
1052 let mut referral_counts: std::collections::HashMap<String, usize> =
1054 std::collections::HashMap::new();
1055
1056 let id_to_idx: std::collections::HashMap<String, usize> = customer_ids
1058 .iter()
1059 .enumerate()
1060 .map(|(idx, id)| (id.clone(), idx))
1061 .collect();
1062
1063 for i in 0..pool.customers.len() {
1064 if self.rng.random::<f64>() < referral_rate {
1065 let potential_referrers: Vec<usize> = customer_ids
1067 .iter()
1068 .enumerate()
1069 .filter(|(j, id)| {
1070 *j != i && referral_counts.get(*id).copied().unwrap_or(0) < max_referrals
1071 })
1072 .map(|(j, _)| j)
1073 .collect();
1074
1075 if !potential_referrers.is_empty() {
1076 let referrer_idx =
1077 potential_referrers[self.rng.random_range(0..potential_referrers.len())];
1078 let referrer_id = customer_ids[referrer_idx].clone();
1079 let customer_id = pool.customers[i].customer_id.clone();
1080
1081 pool.customers[i].network_position.referred_by = Some(referrer_id.clone());
1083
1084 if let Some(&ref_idx) = id_to_idx.get(&referrer_id) {
1086 pool.customers[ref_idx]
1087 .network_position
1088 .referrals_made
1089 .push(customer_id.clone());
1090 }
1091
1092 *referral_counts.entry(referrer_id).or_insert(0) += 1;
1093 }
1094 }
1095 }
1096 }
1097
1098 fn build_corporate_hierarchies(
1100 &mut self,
1101 pool: &mut SegmentedCustomerPool,
1102 customer_ids: &[String],
1103 parent_candidates: &[String],
1104 ) {
1105 let hierarchy_rate = self.segmentation_config.hierarchy_config.hierarchy_rate;
1106 let billing_consolidation_rate = self
1107 .segmentation_config
1108 .hierarchy_config
1109 .billing_consolidation_rate;
1110
1111 let id_to_idx: std::collections::HashMap<String, usize> = customer_ids
1113 .iter()
1114 .enumerate()
1115 .map(|(idx, id)| (id.clone(), idx))
1116 .collect();
1117
1118 for i in 0..pool.customers.len() {
1119 if pool.customers[i].segment == CustomerValueSegment::Enterprise
1121 || pool.customers[i].network_position.parent_customer.is_some()
1122 {
1123 continue;
1124 }
1125
1126 if self.rng.random::<f64>() < hierarchy_rate && !parent_candidates.is_empty() {
1127 let parent_idx = self.rng.random_range(0..parent_candidates.len());
1129 let parent_id = parent_candidates[parent_idx].clone();
1130 let customer_id = pool.customers[i].customer_id.clone();
1131
1132 pool.customers[i].network_position.parent_customer = Some(parent_id.clone());
1134 pool.customers[i].network_position.billing_consolidation =
1135 self.rng.random::<f64>() < billing_consolidation_rate;
1136
1137 if let Some(&parent_idx) = id_to_idx.get(&parent_id) {
1139 pool.customers[parent_idx]
1140 .network_position
1141 .child_customers
1142 .push(customer_id);
1143 }
1144 }
1145 }
1146 }
1147
1148 fn populate_engagement_metrics(
1150 &mut self,
1151 pool: &mut SegmentedCustomerPool,
1152 effective_date: NaiveDate,
1153 ) {
1154 for customer in &mut pool.customers {
1155 let (base_orders, base_revenue) = match customer.lifecycle_stage {
1157 CustomerLifecycleStage::Mature {
1158 avg_annual_spend, ..
1159 } => {
1160 let orders = self.rng.random_range(12..48);
1161 (orders, avg_annual_spend)
1162 }
1163 CustomerLifecycleStage::Growth { growth_rate, .. } => {
1164 let orders = self.rng.random_range(6..24);
1165 let rev = Decimal::from(orders * self.rng.random_range(5000..20000));
1166 (
1167 orders,
1168 rev * Decimal::from_f64_retain(1.0 + growth_rate).unwrap_or(Decimal::ONE),
1169 )
1170 }
1171 CustomerLifecycleStage::New { .. } => {
1172 let orders = self.rng.random_range(1..6);
1173 (
1174 orders,
1175 Decimal::from(orders * self.rng.random_range(2000..10000)),
1176 )
1177 }
1178 CustomerLifecycleStage::AtRisk { .. } => {
1179 let orders = self.rng.random_range(2..12);
1180 (
1181 orders,
1182 Decimal::from(orders * self.rng.random_range(3000..15000)),
1183 )
1184 }
1185 CustomerLifecycleStage::Churned { .. } => (0, Decimal::ZERO),
1186 _ => (0, Decimal::ZERO),
1187 };
1188
1189 customer.engagement = CustomerEngagement {
1190 total_orders: base_orders as u32,
1191 orders_last_12_months: (base_orders as f64 * 0.5) as u32,
1192 lifetime_revenue: base_revenue,
1193 revenue_last_12_months: base_revenue
1194 * Decimal::from_f64_retain(0.5).unwrap_or(Decimal::ZERO),
1195 average_order_value: if base_orders > 0 {
1196 base_revenue / Decimal::from(base_orders)
1197 } else {
1198 Decimal::ZERO
1199 },
1200 days_since_last_order: match &customer.lifecycle_stage {
1201 CustomerLifecycleStage::Churned { last_activity, .. } => {
1202 (effective_date - *last_activity).num_days().max(0) as u32
1203 }
1204 CustomerLifecycleStage::AtRisk { .. } => self.rng.random_range(30..120),
1205 _ => self.rng.random_range(1..30),
1206 },
1207 last_order_date: Some(
1208 effective_date - chrono::Duration::days(self.rng.random_range(1..90)),
1209 ),
1210 first_order_date: Some(
1211 effective_date - chrono::Duration::days(self.rng.random_range(180..1825)),
1212 ),
1213 products_purchased: base_orders as u32 * self.rng.random_range(1..5),
1214 support_tickets: self.rng.random_range(0..10),
1215 nps_score: Some(self.rng.random_range(-20..80) as i8),
1216 };
1217
1218 customer.calculate_churn_risk();
1220
1221 customer.upsell_potential = match customer.segment {
1223 CustomerValueSegment::Enterprise => 0.3 + self.rng.random_range(0.0..0.2),
1224 CustomerValueSegment::MidMarket => 0.4 + self.rng.random_range(0.0..0.3),
1225 CustomerValueSegment::Smb => 0.5 + self.rng.random_range(0.0..0.3),
1226 CustomerValueSegment::Consumer => 0.2 + self.rng.random_range(0.0..0.3),
1227 };
1228 }
1229 }
1230
1231 pub fn generate_pool_with_segmentation(
1233 &mut self,
1234 count: usize,
1235 company_code: &str,
1236 effective_date: NaiveDate,
1237 total_annual_revenue: Decimal,
1238 ) -> (CustomerPool, SegmentedCustomerPool) {
1239 let segmented_pool =
1240 self.generate_segmented_pool(count, company_code, effective_date, total_annual_revenue);
1241
1242 let mut pool = CustomerPool::new();
1244 for _segmented in &segmented_pool.customers {
1245 let customer = self.generate_customer(company_code, effective_date);
1246 pool.add_customer(customer);
1247 }
1248
1249 (pool, segmented_pool)
1250 }
1251}
1252
1253#[cfg(test)]
1254#[allow(clippy::unwrap_used)]
1255mod tests {
1256 use super::*;
1257
1258 #[test]
1259 fn test_customer_generation() {
1260 let mut gen = CustomerGenerator::new(42);
1261 let customer = gen.generate_customer("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1262
1263 assert!(!customer.customer_id.is_empty());
1264 assert!(!customer.name.is_empty());
1265 assert!(customer.credit_limit > Decimal::ZERO);
1266 }
1267
1268 #[test]
1269 fn test_customer_pool_generation() {
1270 let mut gen = CustomerGenerator::new(42);
1271 let pool =
1272 gen.generate_customer_pool(20, "1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1273
1274 assert_eq!(pool.customers.len(), 20);
1275 }
1276
1277 #[test]
1278 fn test_intercompany_customer() {
1279 let mut gen = CustomerGenerator::new(42);
1280 let customer = gen.generate_intercompany_customer(
1281 "1000",
1282 "2000",
1283 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1284 );
1285
1286 assert!(customer.is_intercompany);
1287 assert_eq!(customer.intercompany_code, Some("2000".to_string()));
1288 assert_eq!(customer.credit_rating, CreditRating::AAA);
1289 }
1290
1291 #[test]
1292 fn test_diverse_pool() {
1293 let mut gen = CustomerGenerator::new(42);
1294 let pool =
1295 gen.generate_diverse_pool(100, "1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1296
1297 let aaa_count = pool
1299 .customers
1300 .iter()
1301 .filter(|c| c.credit_rating == CreditRating::AAA)
1302 .count();
1303 let d_count = pool
1304 .customers
1305 .iter()
1306 .filter(|c| c.credit_rating == CreditRating::D)
1307 .count();
1308
1309 assert!(aaa_count > 0);
1310 assert!(d_count > 0);
1311 }
1312
1313 #[test]
1314 fn test_deterministic_generation() {
1315 let mut gen1 = CustomerGenerator::new(42);
1316 let mut gen2 = CustomerGenerator::new(42);
1317
1318 let customer1 =
1319 gen1.generate_customer("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1320 let customer2 =
1321 gen2.generate_customer("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1322
1323 assert_eq!(customer1.customer_id, customer2.customer_id);
1324 assert_eq!(customer1.name, customer2.name);
1325 assert_eq!(customer1.credit_rating, customer2.credit_rating);
1326 }
1327
1328 #[test]
1329 fn test_customer_with_specific_credit() {
1330 let mut gen = CustomerGenerator::new(42);
1331 let customer = gen.generate_customer_with_credit(
1332 "1000",
1333 CreditRating::D,
1334 Decimal::from(5000),
1335 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1336 );
1337
1338 assert_eq!(customer.credit_rating, CreditRating::D);
1339 assert_eq!(customer.credit_limit, Decimal::from(5000));
1340 assert_eq!(customer.payment_behavior, CustomerPaymentBehavior::HighRisk);
1341 }
1342
1343 #[test]
1346 fn test_segmented_pool_generation() {
1347 let segmentation_config = CustomerSegmentationConfig {
1348 enabled: true,
1349 ..Default::default()
1350 };
1351
1352 let mut gen = CustomerGenerator::with_segmentation_config(
1353 42,
1354 CustomerGeneratorConfig::default(),
1355 segmentation_config,
1356 );
1357
1358 let pool = gen.generate_segmented_pool(
1359 100,
1360 "1000",
1361 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1362 Decimal::from(10_000_000),
1363 );
1364
1365 assert_eq!(pool.customers.len(), 100);
1366 assert!(!pool.customers.is_empty());
1367 }
1368
1369 #[test]
1370 fn test_segment_distribution() {
1371 let segmentation_config = CustomerSegmentationConfig {
1372 enabled: true,
1373 segment_distribution: SegmentDistribution {
1374 enterprise: 0.05,
1375 mid_market: 0.20,
1376 smb: 0.50,
1377 consumer: 0.25,
1378 },
1379 ..Default::default()
1380 };
1381
1382 let mut gen = CustomerGenerator::with_segmentation_config(
1383 42,
1384 CustomerGeneratorConfig::default(),
1385 segmentation_config,
1386 );
1387
1388 let pool = gen.generate_segmented_pool(
1389 200,
1390 "1000",
1391 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1392 Decimal::from(10_000_000),
1393 );
1394
1395 let enterprise_count = pool
1397 .customers
1398 .iter()
1399 .filter(|c| c.segment == CustomerValueSegment::Enterprise)
1400 .count();
1401 let smb_count = pool
1402 .customers
1403 .iter()
1404 .filter(|c| c.segment == CustomerValueSegment::Smb)
1405 .count();
1406
1407 assert!((5..=20).contains(&enterprise_count));
1409 assert!((80..=120).contains(&smb_count));
1411 }
1412
1413 #[test]
1414 fn test_referral_network() {
1415 let segmentation_config = CustomerSegmentationConfig {
1416 enabled: true,
1417 referral_config: ReferralConfig {
1418 enabled: true,
1419 referral_rate: 0.30, max_referrals_per_customer: 5,
1421 },
1422 ..Default::default()
1423 };
1424
1425 let mut gen = CustomerGenerator::with_segmentation_config(
1426 42,
1427 CustomerGeneratorConfig::default(),
1428 segmentation_config,
1429 );
1430
1431 let pool = gen.generate_segmented_pool(
1432 50,
1433 "1000",
1434 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1435 Decimal::from(5_000_000),
1436 );
1437
1438 let referred_count = pool
1440 .customers
1441 .iter()
1442 .filter(|c| c.network_position.was_referred())
1443 .count();
1444
1445 assert!(referred_count > 0);
1447 }
1448
1449 #[test]
1450 fn test_corporate_hierarchy() {
1451 let segmentation_config = CustomerSegmentationConfig {
1452 enabled: true,
1453 segment_distribution: SegmentDistribution {
1454 enterprise: 0.10, mid_market: 0.30,
1456 smb: 0.40,
1457 consumer: 0.20,
1458 },
1459 hierarchy_config: HierarchyConfig {
1460 enabled: true,
1461 hierarchy_rate: 0.50, max_depth: 3,
1463 billing_consolidation_rate: 0.50,
1464 },
1465 ..Default::default()
1466 };
1467
1468 let mut gen = CustomerGenerator::with_segmentation_config(
1469 42,
1470 CustomerGeneratorConfig::default(),
1471 segmentation_config,
1472 );
1473
1474 let pool = gen.generate_segmented_pool(
1475 50,
1476 "1000",
1477 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1478 Decimal::from(5_000_000),
1479 );
1480
1481 let in_hierarchy_count = pool
1483 .customers
1484 .iter()
1485 .filter(|c| c.network_position.parent_customer.is_some())
1486 .count();
1487
1488 assert!(in_hierarchy_count > 0);
1490
1491 let parents_with_children = pool
1493 .customers
1494 .iter()
1495 .filter(|c| {
1496 c.segment == CustomerValueSegment::Enterprise
1497 && !c.network_position.child_customers.is_empty()
1498 })
1499 .count();
1500
1501 assert!(parents_with_children > 0);
1502 }
1503
1504 #[test]
1505 fn test_lifecycle_stages() {
1506 let segmentation_config = CustomerSegmentationConfig {
1507 enabled: true,
1508 lifecycle_distribution: LifecycleDistribution {
1509 prospect: 0.0,
1510 new: 0.20,
1511 growth: 0.20,
1512 mature: 0.40,
1513 at_risk: 0.15,
1514 churned: 0.05,
1515 },
1516 ..Default::default()
1517 };
1518
1519 let mut gen = CustomerGenerator::with_segmentation_config(
1520 42,
1521 CustomerGeneratorConfig::default(),
1522 segmentation_config,
1523 );
1524
1525 let pool = gen.generate_segmented_pool(
1526 100,
1527 "1000",
1528 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1529 Decimal::from(10_000_000),
1530 );
1531
1532 let at_risk_count = pool
1534 .customers
1535 .iter()
1536 .filter(|c| matches!(c.lifecycle_stage, CustomerLifecycleStage::AtRisk { .. }))
1537 .count();
1538
1539 assert!((5..=30).contains(&at_risk_count));
1541
1542 let mature_count = pool
1544 .customers
1545 .iter()
1546 .filter(|c| matches!(c.lifecycle_stage, CustomerLifecycleStage::Mature { .. }))
1547 .count();
1548
1549 assert!((25..=55).contains(&mature_count));
1551 }
1552
1553 #[test]
1554 fn test_engagement_metrics() {
1555 let segmentation_config = CustomerSegmentationConfig {
1556 enabled: true,
1557 ..Default::default()
1558 };
1559
1560 let mut gen = CustomerGenerator::with_segmentation_config(
1561 42,
1562 CustomerGeneratorConfig::default(),
1563 segmentation_config,
1564 );
1565
1566 let pool = gen.generate_segmented_pool(
1567 20,
1568 "1000",
1569 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1570 Decimal::from(2_000_000),
1571 );
1572
1573 for customer in &pool.customers {
1575 if !matches!(
1577 customer.lifecycle_stage,
1578 CustomerLifecycleStage::Churned { .. }
1579 ) {
1580 assert!(
1582 customer.engagement.total_orders > 0
1583 || matches!(
1584 customer.lifecycle_stage,
1585 CustomerLifecycleStage::Prospect { .. }
1586 )
1587 );
1588 }
1589
1590 assert!(customer.churn_risk_score >= 0.0 && customer.churn_risk_score <= 1.0);
1592 }
1593 }
1594
1595 #[test]
1596 fn test_segment_distribution_validation() {
1597 let valid = SegmentDistribution::default();
1598 assert!(valid.validate().is_ok());
1599
1600 let invalid = SegmentDistribution {
1601 enterprise: 0.5,
1602 mid_market: 0.5,
1603 smb: 0.5,
1604 consumer: 0.5,
1605 };
1606 assert!(invalid.validate().is_err());
1607 }
1608
1609 #[test]
1610 fn test_segmentation_disabled() {
1611 let segmentation_config = CustomerSegmentationConfig {
1612 enabled: false,
1613 ..Default::default()
1614 };
1615
1616 let mut gen = CustomerGenerator::with_segmentation_config(
1617 42,
1618 CustomerGeneratorConfig::default(),
1619 segmentation_config,
1620 );
1621
1622 let pool = gen.generate_segmented_pool(
1623 20,
1624 "1000",
1625 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1626 Decimal::from(2_000_000),
1627 );
1628
1629 assert!(pool.customers.is_empty());
1631 }
1632
1633 #[test]
1634 fn test_customer_auxiliary_gl_account_french() {
1635 let mut gen = CustomerGenerator::new(42);
1636 gen.set_coa_framework(CoAFramework::FrenchPcg);
1637 let customer = gen.generate_customer("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1638
1639 assert!(customer.auxiliary_gl_account.is_some());
1640 let aux = customer.auxiliary_gl_account.unwrap();
1641 assert!(
1642 aux.starts_with("411"),
1643 "French PCG customer auxiliary should start with 411, got {}",
1644 aux
1645 );
1646 }
1647
1648 #[test]
1649 fn test_customer_auxiliary_gl_account_german() {
1650 let mut gen = CustomerGenerator::new(42);
1651 gen.set_coa_framework(CoAFramework::GermanSkr04);
1652 let customer = gen.generate_customer("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1653
1654 assert!(customer.auxiliary_gl_account.is_some());
1655 let aux = customer.auxiliary_gl_account.unwrap();
1656 assert!(
1657 aux.starts_with("1200"),
1658 "German SKR04 customer auxiliary should start with 1200, got {}",
1659 aux
1660 );
1661 }
1662
1663 #[test]
1664 fn test_customer_auxiliary_gl_account_us_gaap() {
1665 let mut gen = CustomerGenerator::new(42);
1666 let customer = gen.generate_customer("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1667 assert!(customer.auxiliary_gl_account.is_none());
1668 }
1669
1670 #[test]
1671 fn test_customer_name_dedup() {
1672 let mut gen = CustomerGenerator::new(42);
1673 let mut names = HashSet::new();
1674
1675 for _ in 0..200 {
1676 let customer =
1677 gen.generate_customer("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1678 assert!(
1679 names.insert(customer.name.clone()),
1680 "Duplicate customer name found: {}",
1681 customer.name
1682 );
1683 }
1684 }
1685}