Skip to main content

datasynth_generators/master_data/
customer_generator.rs

1//! Enhanced customer generator with credit management and payment behavior.
2//!
3//! Also supports customer segmentation with:
4//! - Value segments (Enterprise, Mid-Market, SMB, Consumer)
5//! - Customer lifecycle stages
6//! - Referral networks and corporate hierarchies
7//! - Engagement metrics and churn analysis
8
9use 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/// Configuration for customer generation.
22#[derive(Debug, Clone)]
23pub struct CustomerGeneratorConfig {
24    /// Distribution of credit ratings (rating, probability)
25    pub credit_rating_distribution: Vec<(CreditRating, f64)>,
26    /// Distribution of payment behaviors (behavior, probability)
27    pub payment_behavior_distribution: Vec<(CustomerPaymentBehavior, f64)>,
28    /// Distribution of payment terms (terms, probability)
29    pub payment_terms_distribution: Vec<(PaymentTerms, f64)>,
30    /// Probability of customer being intercompany
31    pub intercompany_rate: f64,
32    /// Default country for customers
33    pub default_country: String,
34    /// Default currency
35    pub default_currency: String,
36    /// Credit limit ranges by rating (min, max)
37    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/// Configuration for customer segmentation.
113#[derive(Debug, Clone)]
114pub struct CustomerSegmentationConfig {
115    /// Enable customer segmentation
116    pub enabled: bool,
117    /// Value segment distribution
118    pub segment_distribution: SegmentDistribution,
119    /// Lifecycle stage distribution
120    pub lifecycle_distribution: LifecycleDistribution,
121    /// Referral network configuration
122    pub referral_config: ReferralConfig,
123    /// Corporate hierarchy configuration
124    pub hierarchy_config: HierarchyConfig,
125    /// Industry distribution
126    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/// Distribution of customer value segments.
152#[derive(Debug, Clone)]
153pub struct SegmentDistribution {
154    /// Enterprise segment (customer share)
155    pub enterprise: f64,
156    /// Mid-market segment (customer share)
157    pub mid_market: f64,
158    /// SMB segment (customer share)
159    pub smb: f64,
160    /// Consumer segment (customer share)
161    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    /// Validate that distribution sums to 1.0.
177    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    /// Select a segment based on the distribution.
187    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/// Distribution of lifecycle stages.
210#[derive(Debug, Clone)]
211pub struct LifecycleDistribution {
212    /// Prospect rate
213    pub prospect: f64,
214    /// New customer rate
215    pub new: f64,
216    /// Growth stage rate
217    pub growth: f64,
218    /// Mature stage rate
219    pub mature: f64,
220    /// At-risk rate
221    pub at_risk: f64,
222    /// Churned rate
223    pub churned: f64,
224}
225
226impl Default for LifecycleDistribution {
227    fn default() -> Self {
228        Self {
229            prospect: 0.0, // Prospects not typically in active pool
230            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    /// Validate that distribution sums to 1.0.
241    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/// Configuration for referral networks.
256#[derive(Debug, Clone)]
257pub struct ReferralConfig {
258    /// Enable referral generation
259    pub enabled: bool,
260    /// Rate of customers acquired via referral
261    pub referral_rate: f64,
262    /// Maximum referrals per customer
263    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/// Configuration for corporate hierarchies.
277#[derive(Debug, Clone)]
278pub struct HierarchyConfig {
279    /// Enable corporate hierarchy generation
280    pub enabled: bool,
281    /// Rate of customers in hierarchies
282    pub hierarchy_rate: f64,
283    /// Maximum hierarchy depth
284    pub max_depth: usize,
285    /// Rate of billing consolidation
286    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
300/// Customer name templates by industry.
301const 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
408/// Generator for customer master data.
409pub struct CustomerGenerator {
410    rng: ChaCha8Rng,
411    seed: u64,
412    config: CustomerGeneratorConfig,
413    customer_counter: usize,
414    /// Segmentation configuration
415    segmentation_config: CustomerSegmentationConfig,
416    /// Optional country pack for locale-aware generation
417    country_pack: Option<datasynth_core::CountryPack>,
418}
419
420impl CustomerGenerator {
421    /// Create a new customer generator.
422    pub fn new(seed: u64) -> Self {
423        Self::with_config(seed, CustomerGeneratorConfig::default())
424    }
425
426    /// Create a new customer generator with custom configuration.
427    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    /// Create a new customer generator with segmentation configuration.
439    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    /// Set segmentation configuration.
455    pub fn set_segmentation_config(&mut self, segmentation_config: CustomerSegmentationConfig) {
456        self.segmentation_config = segmentation_config;
457    }
458
459    /// Set the country pack for locale-aware generation.
460    pub fn set_country_pack(&mut self, pack: datasynth_core::CountryPack) {
461        self.country_pack = Some(pack);
462    }
463
464    /// Generate a single customer.
465    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        // Note: industry and effective_date are not fields on Customer
484
485        // Set credit rating and limit
486        customer.credit_rating = self.select_credit_rating();
487        customer.credit_limit = self.generate_credit_limit(&customer.credit_rating);
488
489        // Set payment behavior
490        customer.payment_behavior = self.select_payment_behavior();
491
492        // Set payment terms
493        customer.payment_terms = self.select_payment_terms();
494
495        // Check if intercompany
496        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        // Note: address, contact_name, contact_email are not fields on Customer
502
503        customer
504    }
505
506    /// Generate an intercompany customer (always intercompany).
507    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; // IC always highest rating
518        customer.credit_limit = Decimal::from(100_000_000); // High limit for IC
519        customer.payment_behavior = CustomerPaymentBehavior::OnTime;
520        customer
521    }
522
523    /// Generate a customer with specific credit profile.
524    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        // Adjust payment behavior based on credit rating
536        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    /// Generate a customer pool with specified count.
554    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    /// Generate a customer pool with intercompany customers.
572    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        // Generate regular customers
582        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        // Generate IC customers for each partner
589        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    /// Generate a diverse customer pool with various credit profiles.
599    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        // Generate customers with varied credit ratings ensuring coverage
608        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        // Fill any remaining slots
633        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    /// Select a customer name from templates.
642    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    /// Select credit rating based on distribution.
650    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    /// Generate credit limit for rating.
665    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    /// Select payment behavior based on distribution.
679    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    /// Select payment terms based on distribution.
694    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    /// Reset the generator.
709    pub fn reset(&mut self) {
710        self.rng = seeded_rng(self.seed, 0);
711        self.customer_counter = 0;
712    }
713
714    // ===== Customer Segmentation Generation =====
715
716    /// Generate a segmented customer pool with value segments, lifecycle stages, and networks.
717    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        // Calculate counts by segment
731        let segment_counts = self.calculate_segment_counts(count);
732
733        // Generate customers by segment
734        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                // Assign industry
746                segmented.industry = Some(self.select_industry());
747
748                // Assign annual contract value based on segment
749                segmented.annual_contract_value =
750                    self.generate_acv(segment, total_annual_revenue, count);
751
752                // Enterprise customers are candidates for parent relationships
753                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        // Build referral networks
763        if self.segmentation_config.referral_config.enabled {
764            self.build_referral_networks(&mut pool, &all_customer_ids);
765        }
766
767        // Build corporate hierarchies
768        if self.segmentation_config.hierarchy_config.enabled {
769            self.build_corporate_hierarchies(&mut pool, &all_customer_ids, &parent_candidates);
770        }
771
772        // Calculate engagement metrics and churn risk
773        self.populate_engagement_metrics(&mut pool, effective_date);
774
775        // Calculate statistics
776        pool.calculate_statistics();
777
778        pool
779    }
780
781    /// Calculate customer counts by segment.
782    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    /// Create a segmented customer from a base customer.
808    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    /// Generate lifecycle stage based on distribution.
826    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        // Churned
878        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    /// Generate risk triggers for at-risk customers.
886    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    /// Generate churn reason.
908    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    /// Select an industry based on distribution.
928    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    /// Generate annual contract value based on segment.
943    fn generate_acv(
944        &mut self,
945        segment: CustomerValueSegment,
946        total_revenue: Decimal,
947        total_customers: usize,
948    ) -> Decimal {
949        // Calculate expected revenue per customer in this segment
950        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        // Add variance (±50%)
964        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    /// Build referral networks among customers.
969    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        // Track referral counts per customer
981        let mut referral_counts: std::collections::HashMap<String, usize> =
982            std::collections::HashMap::new();
983
984        // Create customer ID to index mapping
985        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                // This customer was referred - find a referrer
994                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                    // Update the referred customer
1010                    pool.customers[i].network_position.referred_by = Some(referrer_id.clone());
1011
1012                    // Update the referrer's referral list
1013                    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    /// Build corporate hierarchies among customers.
1027    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        // Create customer ID to index mapping
1040        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            // Skip enterprise customers (they are parents) and already-hierarchied customers
1048            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                // Assign a parent
1056                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                // Update the child
1061                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                // Update the parent's child list
1066                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    /// Populate engagement metrics for customers.
1077    fn populate_engagement_metrics(
1078        &mut self,
1079        pool: &mut SegmentedCustomerPool,
1080        effective_date: NaiveDate,
1081    ) {
1082        for customer in &mut pool.customers {
1083            // Generate engagement based on lifecycle stage and segment
1084            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            // Calculate churn risk
1147            customer.calculate_churn_risk();
1148
1149            // Calculate upsell potential based on segment and engagement
1150            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    /// Generate a combined output of CustomerPool and SegmentedCustomerPool.
1160    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        // Create a regular CustomerPool from the segmented customers
1171        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        // Should have customers with various credit ratings
1226        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    // ===== Customer Segmentation Tests =====
1272
1273    #[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        // Count by segment
1324        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        // Enterprise should be ~5% (10 of 200)
1336        assert!((5..=20).contains(&enterprise_count));
1337        // SMB should be ~50% (100 of 200)
1338        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, // Higher rate for testing
1348                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        // Count customers who were referred
1367        let referred_count = pool
1368            .customers
1369            .iter()
1370            .filter(|c| c.network_position.was_referred())
1371            .count();
1372
1373        // Should have some referred customers
1374        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, // More enterprise for testing
1383                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, // Higher rate for testing
1390                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        // Count customers in hierarchies (have a parent)
1410        let in_hierarchy_count = pool
1411            .customers
1412            .iter()
1413            .filter(|c| c.network_position.parent_customer.is_some())
1414            .count();
1415
1416        // Should have some customers in hierarchies
1417        assert!(in_hierarchy_count > 0);
1418
1419        // Count enterprise customers with children
1420        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        // Count at-risk customers
1461        let at_risk_count = pool
1462            .customers
1463            .iter()
1464            .filter(|c| matches!(c.lifecycle_stage, CustomerLifecycleStage::AtRisk { .. }))
1465            .count();
1466
1467        // Should be roughly 15%
1468        assert!((5..=30).contains(&at_risk_count));
1469
1470        // Count mature customers
1471        let mature_count = pool
1472            .customers
1473            .iter()
1474            .filter(|c| matches!(c.lifecycle_stage, CustomerLifecycleStage::Mature { .. }))
1475            .count();
1476
1477        // Should be roughly 40%
1478        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        // All customers should have engagement data populated
1502        for customer in &pool.customers {
1503            // Churned customers may have 0 orders
1504            if !matches!(
1505                customer.lifecycle_stage,
1506                CustomerLifecycleStage::Churned { .. }
1507            ) {
1508                // Active customers should have some orders
1509                assert!(
1510                    customer.engagement.total_orders > 0
1511                        || matches!(
1512                            customer.lifecycle_stage,
1513                            CustomerLifecycleStage::Prospect { .. }
1514                        )
1515                );
1516            }
1517
1518            // Churn risk should be calculated
1519            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        // Should return empty pool when disabled
1558        assert!(pool.customers.is_empty());
1559    }
1560}