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)]
1231mod tests {
1232 use super::*;
1233
1234 #[test]
1235 fn test_customer_generation() {
1236 let mut gen = CustomerGenerator::new(42);
1237 let customer = gen.generate_customer("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1238
1239 assert!(!customer.customer_id.is_empty());
1240 assert!(!customer.name.is_empty());
1241 assert!(customer.credit_limit > Decimal::ZERO);
1242 }
1243
1244 #[test]
1245 fn test_customer_pool_generation() {
1246 let mut gen = CustomerGenerator::new(42);
1247 let pool =
1248 gen.generate_customer_pool(20, "1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1249
1250 assert_eq!(pool.customers.len(), 20);
1251 }
1252
1253 #[test]
1254 fn test_intercompany_customer() {
1255 let mut gen = CustomerGenerator::new(42);
1256 let customer = gen.generate_intercompany_customer(
1257 "1000",
1258 "2000",
1259 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1260 );
1261
1262 assert!(customer.is_intercompany);
1263 assert_eq!(customer.intercompany_code, Some("2000".to_string()));
1264 assert_eq!(customer.credit_rating, CreditRating::AAA);
1265 }
1266
1267 #[test]
1268 fn test_diverse_pool() {
1269 let mut gen = CustomerGenerator::new(42);
1270 let pool =
1271 gen.generate_diverse_pool(100, "1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1272
1273 let aaa_count = pool
1275 .customers
1276 .iter()
1277 .filter(|c| c.credit_rating == CreditRating::AAA)
1278 .count();
1279 let d_count = pool
1280 .customers
1281 .iter()
1282 .filter(|c| c.credit_rating == CreditRating::D)
1283 .count();
1284
1285 assert!(aaa_count > 0);
1286 assert!(d_count > 0);
1287 }
1288
1289 #[test]
1290 fn test_deterministic_generation() {
1291 let mut gen1 = CustomerGenerator::new(42);
1292 let mut gen2 = CustomerGenerator::new(42);
1293
1294 let customer1 =
1295 gen1.generate_customer("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1296 let customer2 =
1297 gen2.generate_customer("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1298
1299 assert_eq!(customer1.customer_id, customer2.customer_id);
1300 assert_eq!(customer1.name, customer2.name);
1301 assert_eq!(customer1.credit_rating, customer2.credit_rating);
1302 }
1303
1304 #[test]
1305 fn test_customer_with_specific_credit() {
1306 let mut gen = CustomerGenerator::new(42);
1307 let customer = gen.generate_customer_with_credit(
1308 "1000",
1309 CreditRating::D,
1310 Decimal::from(5000),
1311 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1312 );
1313
1314 assert_eq!(customer.credit_rating, CreditRating::D);
1315 assert_eq!(customer.credit_limit, Decimal::from(5000));
1316 assert_eq!(customer.payment_behavior, CustomerPaymentBehavior::HighRisk);
1317 }
1318
1319 #[test]
1322 fn test_segmented_pool_generation() {
1323 let segmentation_config = CustomerSegmentationConfig {
1324 enabled: true,
1325 ..Default::default()
1326 };
1327
1328 let mut gen = CustomerGenerator::with_segmentation_config(
1329 42,
1330 CustomerGeneratorConfig::default(),
1331 segmentation_config,
1332 );
1333
1334 let pool = gen.generate_segmented_pool(
1335 100,
1336 "1000",
1337 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1338 Decimal::from(10_000_000),
1339 );
1340
1341 assert_eq!(pool.customers.len(), 100);
1342 assert!(!pool.customers.is_empty());
1343 }
1344
1345 #[test]
1346 fn test_segment_distribution() {
1347 let segmentation_config = CustomerSegmentationConfig {
1348 enabled: true,
1349 segment_distribution: SegmentDistribution {
1350 enterprise: 0.05,
1351 mid_market: 0.20,
1352 smb: 0.50,
1353 consumer: 0.25,
1354 },
1355 ..Default::default()
1356 };
1357
1358 let mut gen = CustomerGenerator::with_segmentation_config(
1359 42,
1360 CustomerGeneratorConfig::default(),
1361 segmentation_config,
1362 );
1363
1364 let pool = gen.generate_segmented_pool(
1365 200,
1366 "1000",
1367 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1368 Decimal::from(10_000_000),
1369 );
1370
1371 let enterprise_count = pool
1373 .customers
1374 .iter()
1375 .filter(|c| c.segment == CustomerValueSegment::Enterprise)
1376 .count();
1377 let smb_count = pool
1378 .customers
1379 .iter()
1380 .filter(|c| c.segment == CustomerValueSegment::Smb)
1381 .count();
1382
1383 assert!((5..=20).contains(&enterprise_count));
1385 assert!((80..=120).contains(&smb_count));
1387 }
1388
1389 #[test]
1390 fn test_referral_network() {
1391 let segmentation_config = CustomerSegmentationConfig {
1392 enabled: true,
1393 referral_config: ReferralConfig {
1394 enabled: true,
1395 referral_rate: 0.30, max_referrals_per_customer: 5,
1397 },
1398 ..Default::default()
1399 };
1400
1401 let mut gen = CustomerGenerator::with_segmentation_config(
1402 42,
1403 CustomerGeneratorConfig::default(),
1404 segmentation_config,
1405 );
1406
1407 let pool = gen.generate_segmented_pool(
1408 50,
1409 "1000",
1410 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1411 Decimal::from(5_000_000),
1412 );
1413
1414 let referred_count = pool
1416 .customers
1417 .iter()
1418 .filter(|c| c.network_position.was_referred())
1419 .count();
1420
1421 assert!(referred_count > 0);
1423 }
1424
1425 #[test]
1426 fn test_corporate_hierarchy() {
1427 let segmentation_config = CustomerSegmentationConfig {
1428 enabled: true,
1429 segment_distribution: SegmentDistribution {
1430 enterprise: 0.10, mid_market: 0.30,
1432 smb: 0.40,
1433 consumer: 0.20,
1434 },
1435 hierarchy_config: HierarchyConfig {
1436 enabled: true,
1437 hierarchy_rate: 0.50, max_depth: 3,
1439 billing_consolidation_rate: 0.50,
1440 },
1441 ..Default::default()
1442 };
1443
1444 let mut gen = CustomerGenerator::with_segmentation_config(
1445 42,
1446 CustomerGeneratorConfig::default(),
1447 segmentation_config,
1448 );
1449
1450 let pool = gen.generate_segmented_pool(
1451 50,
1452 "1000",
1453 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1454 Decimal::from(5_000_000),
1455 );
1456
1457 let in_hierarchy_count = pool
1459 .customers
1460 .iter()
1461 .filter(|c| c.network_position.parent_customer.is_some())
1462 .count();
1463
1464 assert!(in_hierarchy_count > 0);
1466
1467 let parents_with_children = pool
1469 .customers
1470 .iter()
1471 .filter(|c| {
1472 c.segment == CustomerValueSegment::Enterprise
1473 && !c.network_position.child_customers.is_empty()
1474 })
1475 .count();
1476
1477 assert!(parents_with_children > 0);
1478 }
1479
1480 #[test]
1481 fn test_lifecycle_stages() {
1482 let segmentation_config = CustomerSegmentationConfig {
1483 enabled: true,
1484 lifecycle_distribution: LifecycleDistribution {
1485 prospect: 0.0,
1486 new: 0.20,
1487 growth: 0.20,
1488 mature: 0.40,
1489 at_risk: 0.15,
1490 churned: 0.05,
1491 },
1492 ..Default::default()
1493 };
1494
1495 let mut gen = CustomerGenerator::with_segmentation_config(
1496 42,
1497 CustomerGeneratorConfig::default(),
1498 segmentation_config,
1499 );
1500
1501 let pool = gen.generate_segmented_pool(
1502 100,
1503 "1000",
1504 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1505 Decimal::from(10_000_000),
1506 );
1507
1508 let at_risk_count = pool
1510 .customers
1511 .iter()
1512 .filter(|c| matches!(c.lifecycle_stage, CustomerLifecycleStage::AtRisk { .. }))
1513 .count();
1514
1515 assert!((5..=30).contains(&at_risk_count));
1517
1518 let mature_count = pool
1520 .customers
1521 .iter()
1522 .filter(|c| matches!(c.lifecycle_stage, CustomerLifecycleStage::Mature { .. }))
1523 .count();
1524
1525 assert!((25..=55).contains(&mature_count));
1527 }
1528
1529 #[test]
1530 fn test_engagement_metrics() {
1531 let segmentation_config = CustomerSegmentationConfig {
1532 enabled: true,
1533 ..Default::default()
1534 };
1535
1536 let mut gen = CustomerGenerator::with_segmentation_config(
1537 42,
1538 CustomerGeneratorConfig::default(),
1539 segmentation_config,
1540 );
1541
1542 let pool = gen.generate_segmented_pool(
1543 20,
1544 "1000",
1545 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1546 Decimal::from(2_000_000),
1547 );
1548
1549 for customer in &pool.customers {
1551 if !matches!(
1553 customer.lifecycle_stage,
1554 CustomerLifecycleStage::Churned { .. }
1555 ) {
1556 assert!(
1558 customer.engagement.total_orders > 0
1559 || matches!(
1560 customer.lifecycle_stage,
1561 CustomerLifecycleStage::Prospect { .. }
1562 )
1563 );
1564 }
1565
1566 assert!(customer.churn_risk_score >= 0.0 && customer.churn_risk_score <= 1.0);
1568 }
1569 }
1570
1571 #[test]
1572 fn test_segment_distribution_validation() {
1573 let valid = SegmentDistribution::default();
1574 assert!(valid.validate().is_ok());
1575
1576 let invalid = SegmentDistribution {
1577 enterprise: 0.5,
1578 mid_market: 0.5,
1579 smb: 0.5,
1580 consumer: 0.5,
1581 };
1582 assert!(invalid.validate().is_err());
1583 }
1584
1585 #[test]
1586 fn test_segmentation_disabled() {
1587 let segmentation_config = CustomerSegmentationConfig {
1588 enabled: false,
1589 ..Default::default()
1590 };
1591
1592 let mut gen = CustomerGenerator::with_segmentation_config(
1593 42,
1594 CustomerGeneratorConfig::default(),
1595 segmentation_config,
1596 );
1597
1598 let pool = gen.generate_segmented_pool(
1599 20,
1600 "1000",
1601 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1602 Decimal::from(2_000_000),
1603 );
1604
1605 assert!(pool.customers.is_empty());
1607 }
1608}