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)]
1300mod tests {
1301 use super::*;
1302
1303 #[test]
1304 fn test_customer_generation() {
1305 let mut gen = CustomerGenerator::new(42);
1306 let customer = gen.generate_customer("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1307
1308 assert!(!customer.customer_id.is_empty());
1309 assert!(!customer.name.is_empty());
1310 assert!(customer.credit_limit > Decimal::ZERO);
1311 }
1312
1313 #[test]
1314 fn test_customer_pool_generation() {
1315 let mut gen = CustomerGenerator::new(42);
1316 let pool =
1317 gen.generate_customer_pool(20, "1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1318
1319 assert_eq!(pool.customers.len(), 20);
1320 }
1321
1322 #[test]
1323 fn test_intercompany_customer() {
1324 let mut gen = CustomerGenerator::new(42);
1325 let customer = gen.generate_intercompany_customer(
1326 "1000",
1327 "2000",
1328 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1329 );
1330
1331 assert!(customer.is_intercompany);
1332 assert_eq!(customer.intercompany_code, Some("2000".to_string()));
1333 assert_eq!(customer.credit_rating, CreditRating::AAA);
1334 }
1335
1336 #[test]
1337 fn test_diverse_pool() {
1338 let mut gen = CustomerGenerator::new(42);
1339 let pool =
1340 gen.generate_diverse_pool(100, "1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1341
1342 let aaa_count = pool
1344 .customers
1345 .iter()
1346 .filter(|c| c.credit_rating == CreditRating::AAA)
1347 .count();
1348 let d_count = pool
1349 .customers
1350 .iter()
1351 .filter(|c| c.credit_rating == CreditRating::D)
1352 .count();
1353
1354 assert!(aaa_count > 0);
1355 assert!(d_count > 0);
1356 }
1357
1358 #[test]
1359 fn test_deterministic_generation() {
1360 let mut gen1 = CustomerGenerator::new(42);
1361 let mut gen2 = CustomerGenerator::new(42);
1362
1363 let customer1 =
1364 gen1.generate_customer("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1365 let customer2 =
1366 gen2.generate_customer("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1367
1368 assert_eq!(customer1.customer_id, customer2.customer_id);
1369 assert_eq!(customer1.name, customer2.name);
1370 assert_eq!(customer1.credit_rating, customer2.credit_rating);
1371 }
1372
1373 #[test]
1374 fn test_customer_with_specific_credit() {
1375 let mut gen = CustomerGenerator::new(42);
1376 let customer = gen.generate_customer_with_credit(
1377 "1000",
1378 CreditRating::D,
1379 Decimal::from(5000),
1380 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1381 );
1382
1383 assert_eq!(customer.credit_rating, CreditRating::D);
1384 assert_eq!(customer.credit_limit, Decimal::from(5000));
1385 assert_eq!(customer.payment_behavior, CustomerPaymentBehavior::HighRisk);
1386 }
1387
1388 #[test]
1391 fn test_segmented_pool_generation() {
1392 let segmentation_config = CustomerSegmentationConfig {
1393 enabled: true,
1394 ..Default::default()
1395 };
1396
1397 let mut gen = CustomerGenerator::with_segmentation_config(
1398 42,
1399 CustomerGeneratorConfig::default(),
1400 segmentation_config,
1401 );
1402
1403 let pool = gen.generate_segmented_pool(
1404 100,
1405 "1000",
1406 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1407 Decimal::from(10_000_000),
1408 );
1409
1410 assert_eq!(pool.customers.len(), 100);
1411 assert!(!pool.customers.is_empty());
1412 }
1413
1414 #[test]
1415 fn test_segment_distribution() {
1416 let segmentation_config = CustomerSegmentationConfig {
1417 enabled: true,
1418 segment_distribution: SegmentDistribution {
1419 enterprise: 0.05,
1420 mid_market: 0.20,
1421 smb: 0.50,
1422 consumer: 0.25,
1423 },
1424 ..Default::default()
1425 };
1426
1427 let mut gen = CustomerGenerator::with_segmentation_config(
1428 42,
1429 CustomerGeneratorConfig::default(),
1430 segmentation_config,
1431 );
1432
1433 let pool = gen.generate_segmented_pool(
1434 200,
1435 "1000",
1436 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1437 Decimal::from(10_000_000),
1438 );
1439
1440 let enterprise_count = pool
1442 .customers
1443 .iter()
1444 .filter(|c| c.segment == CustomerValueSegment::Enterprise)
1445 .count();
1446 let smb_count = pool
1447 .customers
1448 .iter()
1449 .filter(|c| c.segment == CustomerValueSegment::Smb)
1450 .count();
1451
1452 assert!((5..=20).contains(&enterprise_count));
1454 assert!((80..=120).contains(&smb_count));
1456 }
1457
1458 #[test]
1459 fn test_referral_network() {
1460 let segmentation_config = CustomerSegmentationConfig {
1461 enabled: true,
1462 referral_config: ReferralConfig {
1463 enabled: true,
1464 referral_rate: 0.30, max_referrals_per_customer: 5,
1466 },
1467 ..Default::default()
1468 };
1469
1470 let mut gen = CustomerGenerator::with_segmentation_config(
1471 42,
1472 CustomerGeneratorConfig::default(),
1473 segmentation_config,
1474 );
1475
1476 let pool = gen.generate_segmented_pool(
1477 50,
1478 "1000",
1479 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1480 Decimal::from(5_000_000),
1481 );
1482
1483 let referred_count = pool
1485 .customers
1486 .iter()
1487 .filter(|c| c.network_position.was_referred())
1488 .count();
1489
1490 assert!(referred_count > 0);
1492 }
1493
1494 #[test]
1495 fn test_corporate_hierarchy() {
1496 let segmentation_config = CustomerSegmentationConfig {
1497 enabled: true,
1498 segment_distribution: SegmentDistribution {
1499 enterprise: 0.10, mid_market: 0.30,
1501 smb: 0.40,
1502 consumer: 0.20,
1503 },
1504 hierarchy_config: HierarchyConfig {
1505 enabled: true,
1506 hierarchy_rate: 0.50, max_depth: 3,
1508 billing_consolidation_rate: 0.50,
1509 },
1510 ..Default::default()
1511 };
1512
1513 let mut gen = CustomerGenerator::with_segmentation_config(
1514 42,
1515 CustomerGeneratorConfig::default(),
1516 segmentation_config,
1517 );
1518
1519 let pool = gen.generate_segmented_pool(
1520 50,
1521 "1000",
1522 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1523 Decimal::from(5_000_000),
1524 );
1525
1526 let in_hierarchy_count = pool
1528 .customers
1529 .iter()
1530 .filter(|c| c.network_position.parent_customer.is_some())
1531 .count();
1532
1533 assert!(in_hierarchy_count > 0);
1535
1536 let parents_with_children = pool
1538 .customers
1539 .iter()
1540 .filter(|c| {
1541 c.segment == CustomerValueSegment::Enterprise
1542 && !c.network_position.child_customers.is_empty()
1543 })
1544 .count();
1545
1546 assert!(parents_with_children > 0);
1547 }
1548
1549 #[test]
1550 fn test_lifecycle_stages() {
1551 let segmentation_config = CustomerSegmentationConfig {
1552 enabled: true,
1553 lifecycle_distribution: LifecycleDistribution {
1554 prospect: 0.0,
1555 new: 0.20,
1556 growth: 0.20,
1557 mature: 0.40,
1558 at_risk: 0.15,
1559 churned: 0.05,
1560 },
1561 ..Default::default()
1562 };
1563
1564 let mut gen = CustomerGenerator::with_segmentation_config(
1565 42,
1566 CustomerGeneratorConfig::default(),
1567 segmentation_config,
1568 );
1569
1570 let pool = gen.generate_segmented_pool(
1571 100,
1572 "1000",
1573 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1574 Decimal::from(10_000_000),
1575 );
1576
1577 let at_risk_count = pool
1579 .customers
1580 .iter()
1581 .filter(|c| matches!(c.lifecycle_stage, CustomerLifecycleStage::AtRisk { .. }))
1582 .count();
1583
1584 assert!((5..=30).contains(&at_risk_count));
1586
1587 let mature_count = pool
1589 .customers
1590 .iter()
1591 .filter(|c| matches!(c.lifecycle_stage, CustomerLifecycleStage::Mature { .. }))
1592 .count();
1593
1594 assert!((25..=55).contains(&mature_count));
1596 }
1597
1598 #[test]
1599 fn test_engagement_metrics() {
1600 let segmentation_config = CustomerSegmentationConfig {
1601 enabled: true,
1602 ..Default::default()
1603 };
1604
1605 let mut gen = CustomerGenerator::with_segmentation_config(
1606 42,
1607 CustomerGeneratorConfig::default(),
1608 segmentation_config,
1609 );
1610
1611 let pool = gen.generate_segmented_pool(
1612 20,
1613 "1000",
1614 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1615 Decimal::from(2_000_000),
1616 );
1617
1618 for customer in &pool.customers {
1620 if !matches!(
1622 customer.lifecycle_stage,
1623 CustomerLifecycleStage::Churned { .. }
1624 ) {
1625 assert!(
1627 customer.engagement.total_orders > 0
1628 || matches!(
1629 customer.lifecycle_stage,
1630 CustomerLifecycleStage::Prospect { .. }
1631 )
1632 );
1633 }
1634
1635 assert!(customer.churn_risk_score >= 0.0 && customer.churn_risk_score <= 1.0);
1637 }
1638 }
1639
1640 #[test]
1641 fn test_segment_distribution_validation() {
1642 let valid = SegmentDistribution::default();
1643 assert!(valid.validate().is_ok());
1644
1645 let invalid = SegmentDistribution {
1646 enterprise: 0.5,
1647 mid_market: 0.5,
1648 smb: 0.5,
1649 consumer: 0.5,
1650 };
1651 assert!(invalid.validate().is_err());
1652 }
1653
1654 #[test]
1655 fn test_segmentation_disabled() {
1656 let segmentation_config = CustomerSegmentationConfig {
1657 enabled: false,
1658 ..Default::default()
1659 };
1660
1661 let mut gen = CustomerGenerator::with_segmentation_config(
1662 42,
1663 CustomerGeneratorConfig::default(),
1664 segmentation_config,
1665 );
1666
1667 let pool = gen.generate_segmented_pool(
1668 20,
1669 "1000",
1670 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1671 Decimal::from(2_000_000),
1672 );
1673
1674 assert!(pool.customers.is_empty());
1676 }
1677
1678 #[test]
1679 fn test_customer_auxiliary_gl_account_french() {
1680 let mut gen = CustomerGenerator::new(42);
1681 gen.set_coa_framework(CoAFramework::FrenchPcg);
1682 let customer = gen.generate_customer("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1683
1684 assert!(customer.auxiliary_gl_account.is_some());
1685 let aux = customer.auxiliary_gl_account.unwrap();
1686 assert!(
1687 aux.starts_with("411"),
1688 "French PCG customer auxiliary should start with 411, got {}",
1689 aux
1690 );
1691 }
1692
1693 #[test]
1694 fn test_customer_auxiliary_gl_account_german() {
1695 let mut gen = CustomerGenerator::new(42);
1696 gen.set_coa_framework(CoAFramework::GermanSkr04);
1697 let customer = gen.generate_customer("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1698
1699 assert!(customer.auxiliary_gl_account.is_some());
1700 let aux = customer.auxiliary_gl_account.unwrap();
1701 assert!(
1702 aux.starts_with("1200"),
1703 "German SKR04 customer auxiliary should start with 1200, got {}",
1704 aux
1705 );
1706 }
1707
1708 #[test]
1709 fn test_customer_auxiliary_gl_account_us_gaap() {
1710 let mut gen = CustomerGenerator::new(42);
1711 let customer = gen.generate_customer("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1712 assert!(customer.auxiliary_gl_account.is_none());
1713 }
1714
1715 #[test]
1716 fn test_customer_name_dedup() {
1717 let mut gen = CustomerGenerator::new(42);
1718 let mut names = HashSet::new();
1719
1720 for _ in 0..200 {
1721 let customer =
1722 gen.generate_customer("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1723 assert!(
1724 names.insert(customer.name.clone()),
1725 "Duplicate customer name found: {}",
1726 customer.name
1727 );
1728 }
1729 }
1730}