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