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 template_provider: Option<datasynth_core::templates::SharedTemplateProvider>,
425}
426
427impl CustomerGenerator {
428 pub fn new(seed: u64) -> Self {
430 Self::with_config(seed, CustomerGeneratorConfig::default())
431 }
432
433 pub fn with_config(seed: u64, config: CustomerGeneratorConfig) -> Self {
435 Self {
436 rng: seeded_rng(seed, 0),
437 seed,
438 config,
439 customer_counter: 0,
440 segmentation_config: CustomerSegmentationConfig::default(),
441 country_pack: None,
442 coa_framework: CoAFramework::UsGaap,
443 used_names: HashSet::new(),
444 template_provider: None,
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 template_provider: None,
464 }
465 }
466
467 pub fn set_coa_framework(&mut self, framework: CoAFramework) {
469 self.coa_framework = framework;
470 }
471
472 pub fn set_template_provider(
478 &mut self,
479 provider: datasynth_core::templates::SharedTemplateProvider,
480 ) {
481 self.template_provider = Some(provider);
482 }
483
484 pub fn set_segmentation_config(&mut self, segmentation_config: CustomerSegmentationConfig) {
486 self.segmentation_config = segmentation_config;
487 }
488
489 pub fn set_country_pack(&mut self, pack: datasynth_core::CountryPack) {
491 self.country_pack = Some(pack);
492 }
493
494 pub fn set_counter_offset(&mut self, offset: usize) {
501 self.customer_counter = offset;
502 }
503
504 pub fn generate_customer(
506 &mut self,
507 company_code: &str,
508 _effective_date: NaiveDate,
509 ) -> Customer {
510 self.customer_counter += 1;
511
512 let customer_id = format!("C-{:06}", self.customer_counter);
513 let (_industry, name) = self.select_customer_name_unique();
514
515 let mut customer = Customer::new(
516 &customer_id,
517 &name,
518 datasynth_core::models::CustomerType::Corporate,
519 );
520
521 customer.country = self.config.default_country.clone();
522 customer.currency = self.config.default_currency.clone();
523 customer.credit_rating = self.select_credit_rating();
527 customer.credit_limit = self.generate_credit_limit(&customer.credit_rating);
528
529 customer.payment_behavior = self.select_payment_behavior();
531
532 customer.payment_terms = self.select_payment_terms();
534
535 customer.auxiliary_gl_account = self.generate_auxiliary_gl_account();
537
538 if self.rng.random::<f64>() < self.config.intercompany_rate {
540 customer.is_intercompany = true;
541 customer.intercompany_code = Some(format!("IC-{company_code}"));
542 }
543
544 customer
547 }
548
549 pub fn generate_intercompany_customer(
551 &mut self,
552 company_code: &str,
553 partner_company_code: &str,
554 effective_date: NaiveDate,
555 ) -> Customer {
556 let mut customer = self.generate_customer(company_code, effective_date);
557 customer.is_intercompany = true;
558 customer.intercompany_code = Some(partner_company_code.to_string());
559 customer.name = format!("{partner_company_code} - IC");
560 customer.credit_rating = CreditRating::AAA; customer.credit_limit = Decimal::from(100_000_000); customer.payment_behavior = CustomerPaymentBehavior::OnTime;
563 customer
564 }
565
566 pub fn generate_customer_with_credit(
568 &mut self,
569 company_code: &str,
570 credit_rating: CreditRating,
571 credit_limit: Decimal,
572 effective_date: NaiveDate,
573 ) -> Customer {
574 let mut customer = self.generate_customer(company_code, effective_date);
575 customer.credit_rating = credit_rating;
576 customer.credit_limit = credit_limit;
577
578 customer.payment_behavior = match credit_rating {
580 CreditRating::AAA | CreditRating::AA => {
581 if self.rng.random::<f64>() < 0.7 {
582 CustomerPaymentBehavior::EarlyPayer
583 } else {
584 CustomerPaymentBehavior::OnTime
585 }
586 }
587 CreditRating::A | CreditRating::BBB => CustomerPaymentBehavior::OnTime,
588 CreditRating::BB | CreditRating::B => CustomerPaymentBehavior::SlightlyLate,
589 CreditRating::CCC | CreditRating::CC => CustomerPaymentBehavior::OftenLate,
590 CreditRating::C | CreditRating::D => CustomerPaymentBehavior::HighRisk,
591 };
592
593 customer
594 }
595
596 pub fn generate_customer_pool(
598 &mut self,
599 count: usize,
600 company_code: &str,
601 effective_date: NaiveDate,
602 ) -> CustomerPool {
603 debug!(count, company_code, %effective_date, "Generating customer pool");
604 let mut pool = CustomerPool::new();
605
606 for _ in 0..count {
607 let customer = self.generate_customer(company_code, effective_date);
608 pool.add_customer(customer);
609 }
610
611 pool
612 }
613
614 pub fn generate_customer_pool_with_ic(
616 &mut self,
617 count: usize,
618 company_code: &str,
619 partner_company_codes: &[String],
620 effective_date: NaiveDate,
621 ) -> CustomerPool {
622 let mut pool = CustomerPool::new();
623
624 let regular_count = count.saturating_sub(partner_company_codes.len());
626 for _ in 0..regular_count {
627 let customer = self.generate_customer(company_code, effective_date);
628 pool.add_customer(customer);
629 }
630
631 for partner in partner_company_codes {
633 let customer =
634 self.generate_intercompany_customer(company_code, partner, effective_date);
635 pool.add_customer(customer);
636 }
637
638 pool
639 }
640
641 pub fn generate_diverse_pool(
643 &mut self,
644 count: usize,
645 company_code: &str,
646 effective_date: NaiveDate,
647 ) -> CustomerPool {
648 let mut pool = CustomerPool::new();
649
650 let rating_counts = [
652 (CreditRating::AAA, (count as f64 * 0.05) as usize),
653 (CreditRating::AA, (count as f64 * 0.10) as usize),
654 (CreditRating::A, (count as f64 * 0.20) as usize),
655 (CreditRating::BBB, (count as f64 * 0.30) as usize),
656 (CreditRating::BB, (count as f64 * 0.15) as usize),
657 (CreditRating::B, (count as f64 * 0.10) as usize),
658 (CreditRating::CCC, (count as f64 * 0.07) as usize),
659 (CreditRating::D, (count as f64 * 0.03) as usize),
660 ];
661
662 for (rating, rating_count) in rating_counts {
663 for _ in 0..rating_count {
664 let credit_limit = self.generate_credit_limit(&rating);
665 let customer = self.generate_customer_with_credit(
666 company_code,
667 rating,
668 credit_limit,
669 effective_date,
670 );
671 pool.add_customer(customer);
672 }
673 }
674
675 while pool.customers.len() < count {
677 let customer = self.generate_customer(company_code, effective_date);
678 pool.add_customer(customer);
679 }
680
681 pool
682 }
683
684 fn select_customer_name(&mut self) -> (&'static str, String) {
692 let industry_idx = self.rng.random_range(0..CUSTOMER_NAME_TEMPLATES.len());
693 let (industry, names) = CUSTOMER_NAME_TEMPLATES[industry_idx];
694 let name_idx = self.rng.random_range(0..names.len());
695 let embedded_name = names[name_idx].to_string();
696
697 if let Some(ref provider) = self.template_provider {
698 let candidate = provider.get_customer_name(industry, &mut self.rng);
706 let is_from_file = !names.iter().any(|n| *n == candidate);
707 if is_from_file {
708 return (industry, candidate);
709 }
710 }
711
712 (industry, embedded_name)
713 }
714
715 fn select_customer_name_unique(&mut self) -> (&'static str, String) {
717 let (industry, name_str) = self.select_customer_name();
718 let mut name = name_str;
719
720 if self.used_names.contains(&name) {
721 let suffixes = [
722 " II",
723 " III",
724 " & Partners",
725 " Group",
726 " Holdings",
727 " International",
728 ];
729 let mut found_unique = false;
730 for suffix in &suffixes {
731 let candidate = format!("{name}{suffix}");
732 if !self.used_names.contains(&candidate) {
733 name = candidate;
734 found_unique = true;
735 break;
736 }
737 }
738 if !found_unique {
739 name = format!("{} #{}", name, self.customer_counter);
740 }
741 }
742
743 self.used_names.insert(name.clone());
744 (industry, name)
745 }
746
747 fn generate_auxiliary_gl_account(&self) -> Option<String> {
749 match self.coa_framework {
750 CoAFramework::FrenchPcg => {
751 Some(format!("411{:04}", self.customer_counter))
753 }
754 CoAFramework::GermanSkr04 => {
755 Some(format!(
757 "{}{:04}",
758 datasynth_core::skr::control_accounts::AR_CONTROL,
759 self.customer_counter
760 ))
761 }
762 CoAFramework::UsGaap => None,
763 }
764 }
765
766 fn select_credit_rating(&mut self) -> CreditRating {
768 let roll: f64 = self.rng.random();
769 let mut cumulative = 0.0;
770
771 for (rating, prob) in &self.config.credit_rating_distribution {
772 cumulative += prob;
773 if roll < cumulative {
774 return *rating;
775 }
776 }
777
778 CreditRating::BBB
779 }
780
781 fn generate_credit_limit(&mut self, rating: &CreditRating) -> Decimal {
783 for (r, min, max) in &self.config.credit_limits {
784 if r == rating {
785 let range = (*max - *min).to_string().parse::<f64>().unwrap_or(0.0);
786 let offset = Decimal::from_f64_retain(self.rng.random::<f64>() * range)
787 .unwrap_or(Decimal::ZERO);
788 return *min + offset;
789 }
790 }
791
792 Decimal::from(100_000)
793 }
794
795 fn select_payment_behavior(&mut self) -> CustomerPaymentBehavior {
797 let roll: f64 = self.rng.random();
798 let mut cumulative = 0.0;
799
800 for (behavior, prob) in &self.config.payment_behavior_distribution {
801 cumulative += prob;
802 if roll < cumulative {
803 return *behavior;
804 }
805 }
806
807 CustomerPaymentBehavior::OnTime
808 }
809
810 fn select_payment_terms(&mut self) -> PaymentTerms {
812 let roll: f64 = self.rng.random();
813 let mut cumulative = 0.0;
814
815 for (terms, prob) in &self.config.payment_terms_distribution {
816 cumulative += prob;
817 if roll < cumulative {
818 return *terms;
819 }
820 }
821
822 PaymentTerms::Net30
823 }
824
825 pub fn reset(&mut self) {
827 self.rng = seeded_rng(self.seed, 0);
828 self.customer_counter = 0;
829 self.used_names.clear();
830 }
831
832 pub fn generate_segmented_pool(
836 &mut self,
837 count: usize,
838 company_code: &str,
839 effective_date: NaiveDate,
840 total_annual_revenue: Decimal,
841 ) -> SegmentedCustomerPool {
842 let mut pool = SegmentedCustomerPool::new();
843
844 if !self.segmentation_config.enabled {
845 return pool;
846 }
847
848 let segment_counts = self.calculate_segment_counts(count);
850
851 let mut all_customer_ids: Vec<String> = Vec::new();
853 let mut parent_candidates: Vec<String> = Vec::new();
854
855 for (segment, segment_count) in segment_counts {
856 for _ in 0..segment_count {
857 let customer = self.generate_customer(company_code, effective_date);
858 let customer_id = customer.customer_id.clone();
859
860 let mut segmented =
861 self.create_segmented_customer(&customer, segment, effective_date);
862
863 segmented.industry = Some(self.select_industry());
865
866 segmented.annual_contract_value =
868 self.generate_acv(segment, total_annual_revenue, count);
869
870 if segment == CustomerValueSegment::Enterprise {
872 parent_candidates.push(customer_id.clone());
873 }
874
875 all_customer_ids.push(customer_id);
876 pool.add_customer(segmented);
877 }
878 }
879
880 if self.segmentation_config.referral_config.enabled {
882 self.build_referral_networks(&mut pool, &all_customer_ids);
883 }
884
885 if self.segmentation_config.hierarchy_config.enabled {
887 self.build_corporate_hierarchies(&mut pool, &all_customer_ids, &parent_candidates);
888 }
889
890 self.populate_engagement_metrics(&mut pool, effective_date);
892
893 pool.calculate_statistics();
895
896 pool
897 }
898
899 fn calculate_segment_counts(
901 &mut self,
902 total_count: usize,
903 ) -> Vec<(CustomerValueSegment, usize)> {
904 let dist = &self.segmentation_config.segment_distribution;
905 vec![
906 (
907 CustomerValueSegment::Enterprise,
908 (total_count as f64 * dist.enterprise) as usize,
909 ),
910 (
911 CustomerValueSegment::MidMarket,
912 (total_count as f64 * dist.mid_market) as usize,
913 ),
914 (
915 CustomerValueSegment::Smb,
916 (total_count as f64 * dist.smb) as usize,
917 ),
918 (
919 CustomerValueSegment::Consumer,
920 (total_count as f64 * dist.consumer) as usize,
921 ),
922 ]
923 }
924
925 fn create_segmented_customer(
927 &mut self,
928 customer: &Customer,
929 segment: CustomerValueSegment,
930 effective_date: NaiveDate,
931 ) -> SegmentedCustomer {
932 let lifecycle_stage = self.generate_lifecycle_stage(effective_date);
933
934 SegmentedCustomer::new(
935 &customer.customer_id,
936 &customer.name,
937 segment,
938 effective_date,
939 )
940 .with_lifecycle_stage(lifecycle_stage)
941 }
942
943 fn generate_lifecycle_stage(&mut self, effective_date: NaiveDate) -> CustomerLifecycleStage {
945 let dist = &self.segmentation_config.lifecycle_distribution;
946 let roll: f64 = self.rng.random();
947 let mut cumulative = 0.0;
948
949 cumulative += dist.prospect;
950 if roll < cumulative {
951 return CustomerLifecycleStage::Prospect {
952 conversion_probability: self.rng.random_range(0.1..0.4),
953 source: Some("Marketing".to_string()),
954 first_contact_date: effective_date
955 - chrono::Duration::days(self.rng.random_range(1..90)),
956 };
957 }
958
959 cumulative += dist.new;
960 if roll < cumulative {
961 return CustomerLifecycleStage::New {
962 first_order_date: effective_date
963 - chrono::Duration::days(self.rng.random_range(1..90)),
964 onboarding_complete: self.rng.random::<f64>() > 0.3,
965 };
966 }
967
968 cumulative += dist.growth;
969 if roll < cumulative {
970 return CustomerLifecycleStage::Growth {
971 since: effective_date - chrono::Duration::days(self.rng.random_range(90..365)),
972 growth_rate: self.rng.random_range(0.10..0.50),
973 };
974 }
975
976 cumulative += dist.mature;
977 if roll < cumulative {
978 return CustomerLifecycleStage::Mature {
979 stable_since: effective_date
980 - chrono::Duration::days(self.rng.random_range(365..1825)),
981 avg_annual_spend: Decimal::from(self.rng.random_range(10000..500000)),
982 };
983 }
984
985 cumulative += dist.at_risk;
986 if roll < cumulative {
987 let triggers = self.generate_risk_triggers();
988 return CustomerLifecycleStage::AtRisk {
989 triggers,
990 flagged_date: effective_date - chrono::Duration::days(self.rng.random_range(7..60)),
991 churn_probability: self.rng.random_range(0.3..0.8),
992 };
993 }
994
995 CustomerLifecycleStage::Churned {
997 last_activity: effective_date - chrono::Duration::days(self.rng.random_range(90..365)),
998 win_back_probability: self.rng.random_range(0.05..0.25),
999 reason: Some(self.generate_churn_reason()),
1000 }
1001 }
1002
1003 fn generate_risk_triggers(&mut self) -> Vec<RiskTrigger> {
1005 let all_triggers = [
1006 RiskTrigger::DecliningOrderFrequency,
1007 RiskTrigger::DecliningOrderValue,
1008 RiskTrigger::PaymentIssues,
1009 RiskTrigger::Complaints,
1010 RiskTrigger::ReducedEngagement,
1011 RiskTrigger::ContractExpiring,
1012 ];
1013
1014 let count = self.rng.random_range(1..=3);
1015 let mut triggers = Vec::new();
1016
1017 for _ in 0..count {
1018 let idx = self.rng.random_range(0..all_triggers.len());
1019 triggers.push(all_triggers[idx].clone());
1020 }
1021
1022 triggers
1023 }
1024
1025 fn generate_churn_reason(&mut self) -> ChurnReason {
1027 let roll: f64 = self.rng.random();
1028 if roll < 0.30 {
1029 ChurnReason::Competitor
1030 } else if roll < 0.50 {
1031 ChurnReason::Price
1032 } else if roll < 0.65 {
1033 ChurnReason::ServiceQuality
1034 } else if roll < 0.75 {
1035 ChurnReason::BudgetConstraints
1036 } else if roll < 0.85 {
1037 ChurnReason::ProductFit
1038 } else if roll < 0.92 {
1039 ChurnReason::Consolidation
1040 } else {
1041 ChurnReason::Unknown
1042 }
1043 }
1044
1045 fn select_industry(&mut self) -> String {
1047 let roll: f64 = self.rng.random();
1048 let mut cumulative = 0.0;
1049
1050 for (industry, prob) in &self.segmentation_config.industry_distribution {
1051 cumulative += prob;
1052 if roll < cumulative {
1053 return industry.clone();
1054 }
1055 }
1056
1057 "Other".to_string()
1058 }
1059
1060 fn generate_acv(
1062 &mut self,
1063 segment: CustomerValueSegment,
1064 total_revenue: Decimal,
1065 total_customers: usize,
1066 ) -> Decimal {
1067 let segment_revenue_share = segment.revenue_share();
1069 let segment_customer_share = segment.customer_share();
1070 let expected_customers_in_segment =
1071 (total_customers as f64 * segment_customer_share) as usize;
1072 let segment_total_revenue = total_revenue
1073 * Decimal::from_f64_retain(segment_revenue_share).unwrap_or(Decimal::ZERO);
1074
1075 let avg_acv = if expected_customers_in_segment > 0 {
1076 segment_total_revenue / Decimal::from(expected_customers_in_segment)
1077 } else {
1078 Decimal::from(10000)
1079 };
1080
1081 let variance = self.rng.random_range(0.5..1.5);
1083 avg_acv * Decimal::from_f64_retain(variance).unwrap_or(Decimal::ONE)
1084 }
1085
1086 fn build_referral_networks(
1088 &mut self,
1089 pool: &mut SegmentedCustomerPool,
1090 customer_ids: &[String],
1091 ) {
1092 let referral_rate = self.segmentation_config.referral_config.referral_rate;
1093 let max_referrals = self
1094 .segmentation_config
1095 .referral_config
1096 .max_referrals_per_customer;
1097
1098 let mut referral_counts: std::collections::HashMap<String, usize> =
1100 std::collections::HashMap::new();
1101
1102 let id_to_idx: std::collections::HashMap<String, usize> = customer_ids
1104 .iter()
1105 .enumerate()
1106 .map(|(idx, id)| (id.clone(), idx))
1107 .collect();
1108
1109 for i in 0..pool.customers.len() {
1110 if self.rng.random::<f64>() < referral_rate {
1111 let potential_referrers: Vec<usize> = customer_ids
1113 .iter()
1114 .enumerate()
1115 .filter(|(j, id)| {
1116 *j != i && referral_counts.get(*id).copied().unwrap_or(0) < max_referrals
1117 })
1118 .map(|(j, _)| j)
1119 .collect();
1120
1121 if !potential_referrers.is_empty() {
1122 let referrer_idx =
1123 potential_referrers[self.rng.random_range(0..potential_referrers.len())];
1124 let referrer_id = customer_ids[referrer_idx].clone();
1125 let customer_id = pool.customers[i].customer_id.clone();
1126
1127 pool.customers[i].network_position.referred_by = Some(referrer_id.clone());
1129
1130 if let Some(&ref_idx) = id_to_idx.get(&referrer_id) {
1132 pool.customers[ref_idx]
1133 .network_position
1134 .referrals_made
1135 .push(customer_id.clone());
1136 }
1137
1138 *referral_counts.entry(referrer_id).or_insert(0) += 1;
1139 }
1140 }
1141 }
1142 }
1143
1144 fn build_corporate_hierarchies(
1146 &mut self,
1147 pool: &mut SegmentedCustomerPool,
1148 customer_ids: &[String],
1149 parent_candidates: &[String],
1150 ) {
1151 let hierarchy_rate = self.segmentation_config.hierarchy_config.hierarchy_rate;
1152 let billing_consolidation_rate = self
1153 .segmentation_config
1154 .hierarchy_config
1155 .billing_consolidation_rate;
1156
1157 let id_to_idx: std::collections::HashMap<String, usize> = customer_ids
1159 .iter()
1160 .enumerate()
1161 .map(|(idx, id)| (id.clone(), idx))
1162 .collect();
1163
1164 for i in 0..pool.customers.len() {
1165 if pool.customers[i].segment == CustomerValueSegment::Enterprise
1167 || pool.customers[i].network_position.parent_customer.is_some()
1168 {
1169 continue;
1170 }
1171
1172 if self.rng.random::<f64>() < hierarchy_rate && !parent_candidates.is_empty() {
1173 let parent_idx = self.rng.random_range(0..parent_candidates.len());
1175 let parent_id = parent_candidates[parent_idx].clone();
1176 let customer_id = pool.customers[i].customer_id.clone();
1177
1178 pool.customers[i].network_position.parent_customer = Some(parent_id.clone());
1180 pool.customers[i].network_position.billing_consolidation =
1181 self.rng.random::<f64>() < billing_consolidation_rate;
1182
1183 if let Some(&parent_idx) = id_to_idx.get(&parent_id) {
1185 pool.customers[parent_idx]
1186 .network_position
1187 .child_customers
1188 .push(customer_id);
1189 }
1190 }
1191 }
1192 }
1193
1194 fn populate_engagement_metrics(
1196 &mut self,
1197 pool: &mut SegmentedCustomerPool,
1198 effective_date: NaiveDate,
1199 ) {
1200 for customer in &mut pool.customers {
1201 let (base_orders, base_revenue) = match customer.lifecycle_stage {
1203 CustomerLifecycleStage::Mature {
1204 avg_annual_spend, ..
1205 } => {
1206 let orders = self.rng.random_range(12..48);
1207 (orders, avg_annual_spend)
1208 }
1209 CustomerLifecycleStage::Growth { growth_rate, .. } => {
1210 let orders = self.rng.random_range(6..24);
1211 let rev = Decimal::from(orders * self.rng.random_range(5000..20000));
1212 (
1213 orders,
1214 rev * Decimal::from_f64_retain(1.0 + growth_rate).unwrap_or(Decimal::ONE),
1215 )
1216 }
1217 CustomerLifecycleStage::New { .. } => {
1218 let orders = self.rng.random_range(1..6);
1219 (
1220 orders,
1221 Decimal::from(orders * self.rng.random_range(2000..10000)),
1222 )
1223 }
1224 CustomerLifecycleStage::AtRisk { .. } => {
1225 let orders = self.rng.random_range(2..12);
1226 (
1227 orders,
1228 Decimal::from(orders * self.rng.random_range(3000..15000)),
1229 )
1230 }
1231 CustomerLifecycleStage::Churned { .. } => (0, Decimal::ZERO),
1232 _ => (0, Decimal::ZERO),
1233 };
1234
1235 customer.engagement = CustomerEngagement {
1236 total_orders: base_orders as u32,
1237 orders_last_12_months: (base_orders as f64 * 0.5) as u32,
1238 lifetime_revenue: base_revenue,
1239 revenue_last_12_months: base_revenue
1240 * Decimal::from_f64_retain(0.5).unwrap_or(Decimal::ZERO),
1241 average_order_value: if base_orders > 0 {
1242 base_revenue / Decimal::from(base_orders)
1243 } else {
1244 Decimal::ZERO
1245 },
1246 days_since_last_order: match &customer.lifecycle_stage {
1247 CustomerLifecycleStage::Churned { last_activity, .. } => {
1248 (effective_date - *last_activity).num_days().max(0) as u32
1249 }
1250 CustomerLifecycleStage::AtRisk { .. } => self.rng.random_range(30..120),
1251 _ => self.rng.random_range(1..30),
1252 },
1253 last_order_date: Some(
1254 effective_date - chrono::Duration::days(self.rng.random_range(1..90)),
1255 ),
1256 first_order_date: Some(
1257 effective_date - chrono::Duration::days(self.rng.random_range(180..1825)),
1258 ),
1259 products_purchased: base_orders as u32 * self.rng.random_range(1..5),
1260 support_tickets: self.rng.random_range(0..10),
1261 nps_score: Some(self.rng.random_range(-20..80) as i8),
1262 };
1263
1264 customer.calculate_churn_risk();
1266
1267 customer.upsell_potential = match customer.segment {
1269 CustomerValueSegment::Enterprise => 0.3 + self.rng.random_range(0.0..0.2),
1270 CustomerValueSegment::MidMarket => 0.4 + self.rng.random_range(0.0..0.3),
1271 CustomerValueSegment::Smb => 0.5 + self.rng.random_range(0.0..0.3),
1272 CustomerValueSegment::Consumer => 0.2 + self.rng.random_range(0.0..0.3),
1273 };
1274 }
1275 }
1276
1277 pub fn generate_pool_with_segmentation(
1279 &mut self,
1280 count: usize,
1281 company_code: &str,
1282 effective_date: NaiveDate,
1283 total_annual_revenue: Decimal,
1284 ) -> (CustomerPool, SegmentedCustomerPool) {
1285 let segmented_pool =
1286 self.generate_segmented_pool(count, company_code, effective_date, total_annual_revenue);
1287
1288 let mut pool = CustomerPool::new();
1290 for _segmented in &segmented_pool.customers {
1291 let customer = self.generate_customer(company_code, effective_date);
1292 pool.add_customer(customer);
1293 }
1294
1295 (pool, segmented_pool)
1296 }
1297}
1298
1299#[cfg(test)]
1300#[allow(clippy::unwrap_used)]
1301mod tests {
1302 use super::*;
1303
1304 #[test]
1305 fn test_customer_generation() {
1306 let mut gen = CustomerGenerator::new(42);
1307 let customer = gen.generate_customer("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1308
1309 assert!(!customer.customer_id.is_empty());
1310 assert!(!customer.name.is_empty());
1311 assert!(customer.credit_limit > Decimal::ZERO);
1312 }
1313
1314 #[test]
1315 fn test_customer_pool_generation() {
1316 let mut gen = CustomerGenerator::new(42);
1317 let pool =
1318 gen.generate_customer_pool(20, "1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1319
1320 assert_eq!(pool.customers.len(), 20);
1321 }
1322
1323 #[test]
1324 fn test_intercompany_customer() {
1325 let mut gen = CustomerGenerator::new(42);
1326 let customer = gen.generate_intercompany_customer(
1327 "1000",
1328 "2000",
1329 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1330 );
1331
1332 assert!(customer.is_intercompany);
1333 assert_eq!(customer.intercompany_code, Some("2000".to_string()));
1334 assert_eq!(customer.credit_rating, CreditRating::AAA);
1335 }
1336
1337 #[test]
1338 fn test_diverse_pool() {
1339 let mut gen = CustomerGenerator::new(42);
1340 let pool =
1341 gen.generate_diverse_pool(100, "1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1342
1343 let aaa_count = pool
1345 .customers
1346 .iter()
1347 .filter(|c| c.credit_rating == CreditRating::AAA)
1348 .count();
1349 let d_count = pool
1350 .customers
1351 .iter()
1352 .filter(|c| c.credit_rating == CreditRating::D)
1353 .count();
1354
1355 assert!(aaa_count > 0);
1356 assert!(d_count > 0);
1357 }
1358
1359 #[test]
1360 fn test_deterministic_generation() {
1361 let mut gen1 = CustomerGenerator::new(42);
1362 let mut gen2 = CustomerGenerator::new(42);
1363
1364 let customer1 =
1365 gen1.generate_customer("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1366 let customer2 =
1367 gen2.generate_customer("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1368
1369 assert_eq!(customer1.customer_id, customer2.customer_id);
1370 assert_eq!(customer1.name, customer2.name);
1371 assert_eq!(customer1.credit_rating, customer2.credit_rating);
1372 }
1373
1374 #[test]
1375 fn test_customer_with_specific_credit() {
1376 let mut gen = CustomerGenerator::new(42);
1377 let customer = gen.generate_customer_with_credit(
1378 "1000",
1379 CreditRating::D,
1380 Decimal::from(5000),
1381 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1382 );
1383
1384 assert_eq!(customer.credit_rating, CreditRating::D);
1385 assert_eq!(customer.credit_limit, Decimal::from(5000));
1386 assert_eq!(customer.payment_behavior, CustomerPaymentBehavior::HighRisk);
1387 }
1388
1389 #[test]
1392 fn test_segmented_pool_generation() {
1393 let segmentation_config = CustomerSegmentationConfig {
1394 enabled: true,
1395 ..Default::default()
1396 };
1397
1398 let mut gen = CustomerGenerator::with_segmentation_config(
1399 42,
1400 CustomerGeneratorConfig::default(),
1401 segmentation_config,
1402 );
1403
1404 let pool = gen.generate_segmented_pool(
1405 100,
1406 "1000",
1407 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1408 Decimal::from(10_000_000),
1409 );
1410
1411 assert_eq!(pool.customers.len(), 100);
1412 assert!(!pool.customers.is_empty());
1413 }
1414
1415 #[test]
1416 fn test_segment_distribution() {
1417 let segmentation_config = CustomerSegmentationConfig {
1418 enabled: true,
1419 segment_distribution: SegmentDistribution {
1420 enterprise: 0.05,
1421 mid_market: 0.20,
1422 smb: 0.50,
1423 consumer: 0.25,
1424 },
1425 ..Default::default()
1426 };
1427
1428 let mut gen = CustomerGenerator::with_segmentation_config(
1429 42,
1430 CustomerGeneratorConfig::default(),
1431 segmentation_config,
1432 );
1433
1434 let pool = gen.generate_segmented_pool(
1435 200,
1436 "1000",
1437 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1438 Decimal::from(10_000_000),
1439 );
1440
1441 let enterprise_count = pool
1443 .customers
1444 .iter()
1445 .filter(|c| c.segment == CustomerValueSegment::Enterprise)
1446 .count();
1447 let smb_count = pool
1448 .customers
1449 .iter()
1450 .filter(|c| c.segment == CustomerValueSegment::Smb)
1451 .count();
1452
1453 assert!((5..=20).contains(&enterprise_count));
1455 assert!((80..=120).contains(&smb_count));
1457 }
1458
1459 #[test]
1460 fn test_referral_network() {
1461 let segmentation_config = CustomerSegmentationConfig {
1462 enabled: true,
1463 referral_config: ReferralConfig {
1464 enabled: true,
1465 referral_rate: 0.30, max_referrals_per_customer: 5,
1467 },
1468 ..Default::default()
1469 };
1470
1471 let mut gen = CustomerGenerator::with_segmentation_config(
1472 42,
1473 CustomerGeneratorConfig::default(),
1474 segmentation_config,
1475 );
1476
1477 let pool = gen.generate_segmented_pool(
1478 50,
1479 "1000",
1480 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1481 Decimal::from(5_000_000),
1482 );
1483
1484 let referred_count = pool
1486 .customers
1487 .iter()
1488 .filter(|c| c.network_position.was_referred())
1489 .count();
1490
1491 assert!(referred_count > 0);
1493 }
1494
1495 #[test]
1496 fn test_corporate_hierarchy() {
1497 let segmentation_config = CustomerSegmentationConfig {
1498 enabled: true,
1499 segment_distribution: SegmentDistribution {
1500 enterprise: 0.10, mid_market: 0.30,
1502 smb: 0.40,
1503 consumer: 0.20,
1504 },
1505 hierarchy_config: HierarchyConfig {
1506 enabled: true,
1507 hierarchy_rate: 0.50, max_depth: 3,
1509 billing_consolidation_rate: 0.50,
1510 },
1511 ..Default::default()
1512 };
1513
1514 let mut gen = CustomerGenerator::with_segmentation_config(
1515 42,
1516 CustomerGeneratorConfig::default(),
1517 segmentation_config,
1518 );
1519
1520 let pool = gen.generate_segmented_pool(
1521 50,
1522 "1000",
1523 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1524 Decimal::from(5_000_000),
1525 );
1526
1527 let in_hierarchy_count = pool
1529 .customers
1530 .iter()
1531 .filter(|c| c.network_position.parent_customer.is_some())
1532 .count();
1533
1534 assert!(in_hierarchy_count > 0);
1536
1537 let parents_with_children = pool
1539 .customers
1540 .iter()
1541 .filter(|c| {
1542 c.segment == CustomerValueSegment::Enterprise
1543 && !c.network_position.child_customers.is_empty()
1544 })
1545 .count();
1546
1547 assert!(parents_with_children > 0);
1548 }
1549
1550 #[test]
1551 fn test_lifecycle_stages() {
1552 let segmentation_config = CustomerSegmentationConfig {
1553 enabled: true,
1554 lifecycle_distribution: LifecycleDistribution {
1555 prospect: 0.0,
1556 new: 0.20,
1557 growth: 0.20,
1558 mature: 0.40,
1559 at_risk: 0.15,
1560 churned: 0.05,
1561 },
1562 ..Default::default()
1563 };
1564
1565 let mut gen = CustomerGenerator::with_segmentation_config(
1566 42,
1567 CustomerGeneratorConfig::default(),
1568 segmentation_config,
1569 );
1570
1571 let pool = gen.generate_segmented_pool(
1572 100,
1573 "1000",
1574 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1575 Decimal::from(10_000_000),
1576 );
1577
1578 let at_risk_count = pool
1580 .customers
1581 .iter()
1582 .filter(|c| matches!(c.lifecycle_stage, CustomerLifecycleStage::AtRisk { .. }))
1583 .count();
1584
1585 assert!((5..=30).contains(&at_risk_count));
1587
1588 let mature_count = pool
1590 .customers
1591 .iter()
1592 .filter(|c| matches!(c.lifecycle_stage, CustomerLifecycleStage::Mature { .. }))
1593 .count();
1594
1595 assert!((25..=55).contains(&mature_count));
1597 }
1598
1599 #[test]
1600 fn test_engagement_metrics() {
1601 let segmentation_config = CustomerSegmentationConfig {
1602 enabled: true,
1603 ..Default::default()
1604 };
1605
1606 let mut gen = CustomerGenerator::with_segmentation_config(
1607 42,
1608 CustomerGeneratorConfig::default(),
1609 segmentation_config,
1610 );
1611
1612 let pool = gen.generate_segmented_pool(
1613 20,
1614 "1000",
1615 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1616 Decimal::from(2_000_000),
1617 );
1618
1619 for customer in &pool.customers {
1621 if !matches!(
1623 customer.lifecycle_stage,
1624 CustomerLifecycleStage::Churned { .. }
1625 ) {
1626 assert!(
1628 customer.engagement.total_orders > 0
1629 || matches!(
1630 customer.lifecycle_stage,
1631 CustomerLifecycleStage::Prospect { .. }
1632 )
1633 );
1634 }
1635
1636 assert!(customer.churn_risk_score >= 0.0 && customer.churn_risk_score <= 1.0);
1638 }
1639 }
1640
1641 #[test]
1642 fn test_segment_distribution_validation() {
1643 let valid = SegmentDistribution::default();
1644 assert!(valid.validate().is_ok());
1645
1646 let invalid = SegmentDistribution {
1647 enterprise: 0.5,
1648 mid_market: 0.5,
1649 smb: 0.5,
1650 consumer: 0.5,
1651 };
1652 assert!(invalid.validate().is_err());
1653 }
1654
1655 #[test]
1656 fn test_segmentation_disabled() {
1657 let segmentation_config = CustomerSegmentationConfig {
1658 enabled: false,
1659 ..Default::default()
1660 };
1661
1662 let mut gen = CustomerGenerator::with_segmentation_config(
1663 42,
1664 CustomerGeneratorConfig::default(),
1665 segmentation_config,
1666 );
1667
1668 let pool = gen.generate_segmented_pool(
1669 20,
1670 "1000",
1671 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1672 Decimal::from(2_000_000),
1673 );
1674
1675 assert!(pool.customers.is_empty());
1677 }
1678
1679 #[test]
1680 fn test_customer_auxiliary_gl_account_french() {
1681 let mut gen = CustomerGenerator::new(42);
1682 gen.set_coa_framework(CoAFramework::FrenchPcg);
1683 let customer = gen.generate_customer("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1684
1685 assert!(customer.auxiliary_gl_account.is_some());
1686 let aux = customer.auxiliary_gl_account.unwrap();
1687 assert!(
1688 aux.starts_with("411"),
1689 "French PCG customer auxiliary should start with 411, got {}",
1690 aux
1691 );
1692 }
1693
1694 #[test]
1695 fn test_customer_auxiliary_gl_account_german() {
1696 let mut gen = CustomerGenerator::new(42);
1697 gen.set_coa_framework(CoAFramework::GermanSkr04);
1698 let customer = gen.generate_customer("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1699
1700 assert!(customer.auxiliary_gl_account.is_some());
1701 let aux = customer.auxiliary_gl_account.unwrap();
1702 assert!(
1703 aux.starts_with("1200"),
1704 "German SKR04 customer auxiliary should start with 1200, got {}",
1705 aux
1706 );
1707 }
1708
1709 #[test]
1710 fn test_customer_auxiliary_gl_account_us_gaap() {
1711 let mut gen = CustomerGenerator::new(42);
1712 let customer = gen.generate_customer("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1713 assert!(customer.auxiliary_gl_account.is_none());
1714 }
1715
1716 #[test]
1717 fn test_customer_name_dedup() {
1718 let mut gen = CustomerGenerator::new(42);
1719 let mut names = HashSet::new();
1720
1721 for _ in 0..200 {
1722 let customer =
1723 gen.generate_customer("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1724 assert!(
1725 names.insert(customer.name.clone()),
1726 "Duplicate customer name found: {}",
1727 customer.name
1728 );
1729 }
1730 }
1731}