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