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