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 rand::prelude::*;
16use rand_chacha::ChaCha8Rng;
17use rust_decimal::Decimal;
18
19/// Configuration for customer generation.
20#[derive(Debug, Clone)]
21pub struct CustomerGeneratorConfig {
22    /// Distribution of credit ratings (rating, probability)
23    pub credit_rating_distribution: Vec<(CreditRating, f64)>,
24    /// Distribution of payment behaviors (behavior, probability)
25    pub payment_behavior_distribution: Vec<(CustomerPaymentBehavior, f64)>,
26    /// Distribution of payment terms (terms, probability)
27    pub payment_terms_distribution: Vec<(PaymentTerms, f64)>,
28    /// Probability of customer being intercompany
29    pub intercompany_rate: f64,
30    /// Default country for customers
31    pub default_country: String,
32    /// Default currency
33    pub default_currency: String,
34    /// Credit limit ranges by rating (min, max)
35    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/// Configuration for customer segmentation.
111#[derive(Debug, Clone)]
112pub struct CustomerSegmentationConfig {
113    /// Enable customer segmentation
114    pub enabled: bool,
115    /// Value segment distribution
116    pub segment_distribution: SegmentDistribution,
117    /// Lifecycle stage distribution
118    pub lifecycle_distribution: LifecycleDistribution,
119    /// Referral network configuration
120    pub referral_config: ReferralConfig,
121    /// Corporate hierarchy configuration
122    pub hierarchy_config: HierarchyConfig,
123    /// Industry distribution
124    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/// Distribution of customer value segments.
150#[derive(Debug, Clone)]
151pub struct SegmentDistribution {
152    /// Enterprise segment (customer share)
153    pub enterprise: f64,
154    /// Mid-market segment (customer share)
155    pub mid_market: f64,
156    /// SMB segment (customer share)
157    pub smb: f64,
158    /// Consumer segment (customer share)
159    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    /// Validate that distribution sums to 1.0.
175    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    /// Select a segment based on the distribution.
185    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/// Distribution of lifecycle stages.
208#[derive(Debug, Clone)]
209pub struct LifecycleDistribution {
210    /// Prospect rate
211    pub prospect: f64,
212    /// New customer rate
213    pub new: f64,
214    /// Growth stage rate
215    pub growth: f64,
216    /// Mature stage rate
217    pub mature: f64,
218    /// At-risk rate
219    pub at_risk: f64,
220    /// Churned rate
221    pub churned: f64,
222}
223
224impl Default for LifecycleDistribution {
225    fn default() -> Self {
226        Self {
227            prospect: 0.0, // Prospects not typically in active pool
228            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    /// Validate that distribution sums to 1.0.
239    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/// Configuration for referral networks.
254#[derive(Debug, Clone)]
255pub struct ReferralConfig {
256    /// Enable referral generation
257    pub enabled: bool,
258    /// Rate of customers acquired via referral
259    pub referral_rate: f64,
260    /// Maximum referrals per customer
261    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/// Configuration for corporate hierarchies.
275#[derive(Debug, Clone)]
276pub struct HierarchyConfig {
277    /// Enable corporate hierarchy generation
278    pub enabled: bool,
279    /// Rate of customers in hierarchies
280    pub hierarchy_rate: f64,
281    /// Maximum hierarchy depth
282    pub max_depth: usize,
283    /// Rate of billing consolidation
284    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
298/// Customer name templates by industry.
299const 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
406/// Generator for customer master data.
407pub struct CustomerGenerator {
408    rng: ChaCha8Rng,
409    seed: u64,
410    config: CustomerGeneratorConfig,
411    customer_counter: usize,
412    /// Segmentation configuration
413    segmentation_config: CustomerSegmentationConfig,
414}
415
416impl CustomerGenerator {
417    /// Create a new customer generator.
418    pub fn new(seed: u64) -> Self {
419        Self::with_config(seed, CustomerGeneratorConfig::default())
420    }
421
422    /// Create a new customer generator with custom configuration.
423    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    /// Create a new customer generator with segmentation configuration.
434    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    /// Set segmentation configuration.
449    pub fn set_segmentation_config(&mut self, segmentation_config: CustomerSegmentationConfig) {
450        self.segmentation_config = segmentation_config;
451    }
452
453    /// Generate a single customer.
454    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        // Note: industry and effective_date are not fields on Customer
473
474        // Set credit rating and limit
475        customer.credit_rating = self.select_credit_rating();
476        customer.credit_limit = self.generate_credit_limit(&customer.credit_rating);
477
478        // Set payment behavior
479        customer.payment_behavior = self.select_payment_behavior();
480
481        // Set payment terms
482        customer.payment_terms = self.select_payment_terms();
483
484        // Check if intercompany
485        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        // Note: address, contact_name, contact_email are not fields on Customer
491
492        customer
493    }
494
495    /// Generate an intercompany customer (always intercompany).
496    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; // IC always highest rating
507        customer.credit_limit = Decimal::from(100_000_000); // High limit for IC
508        customer.payment_behavior = CustomerPaymentBehavior::OnTime;
509        customer
510    }
511
512    /// Generate a customer with specific credit profile.
513    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        // Adjust payment behavior based on credit rating
525        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    /// Generate a customer pool with specified count.
543    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    /// Generate a customer pool with intercompany customers.
560    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        // Generate regular customers
570        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        // Generate IC customers for each partner
577        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    /// Generate a diverse customer pool with various credit profiles.
587    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        // Generate customers with varied credit ratings ensuring coverage
596        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        // Fill any remaining slots
621        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    /// Select a customer name from templates.
630    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    /// Select credit rating based on distribution.
638    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    /// Generate credit limit for rating.
653    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    /// Select payment behavior based on distribution.
667    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    /// Select payment terms based on distribution.
682    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    /// Generate an address.
697    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    /// Generate a contact name.
730    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    /// Generate a contact email.
745    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    /// Reset the generator.
758    pub fn reset(&mut self) {
759        self.rng = ChaCha8Rng::seed_from_u64(self.seed);
760        self.customer_counter = 0;
761    }
762
763    // ===== Customer Segmentation Generation =====
764
765    /// Generate a segmented customer pool with value segments, lifecycle stages, and networks.
766    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        // Calculate counts by segment
780        let segment_counts = self.calculate_segment_counts(count);
781
782        // Generate customers by segment
783        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                // Assign industry
795                segmented.industry = Some(self.select_industry());
796
797                // Assign annual contract value based on segment
798                segmented.annual_contract_value =
799                    self.generate_acv(segment, total_annual_revenue, count);
800
801                // Enterprise customers are candidates for parent relationships
802                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        // Build referral networks
812        if self.segmentation_config.referral_config.enabled {
813            self.build_referral_networks(&mut pool, &all_customer_ids);
814        }
815
816        // Build corporate hierarchies
817        if self.segmentation_config.hierarchy_config.enabled {
818            self.build_corporate_hierarchies(&mut pool, &all_customer_ids, &parent_candidates);
819        }
820
821        // Calculate engagement metrics and churn risk
822        self.populate_engagement_metrics(&mut pool, effective_date);
823
824        // Calculate statistics
825        pool.calculate_statistics();
826
827        pool
828    }
829
830    /// Calculate customer counts by segment.
831    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    /// Create a segmented customer from a base customer.
857    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    /// Generate lifecycle stage based on distribution.
875    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        // Churned
927        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    /// Generate risk triggers for at-risk customers.
935    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    /// Generate churn reason.
957    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    /// Select an industry based on distribution.
977    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    /// Generate annual contract value based on segment.
992    fn generate_acv(
993        &mut self,
994        segment: CustomerValueSegment,
995        total_revenue: Decimal,
996        total_customers: usize,
997    ) -> Decimal {
998        // Calculate expected revenue per customer in this segment
999        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        // Add variance (±50%)
1013        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    /// Build referral networks among customers.
1018    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        // Track referral counts per customer
1030        let mut referral_counts: std::collections::HashMap<String, usize> =
1031            std::collections::HashMap::new();
1032
1033        // Create customer ID to index mapping
1034        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                // This customer was referred - find a referrer
1043                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                    // Update the referred customer
1059                    pool.customers[i].network_position.referred_by = Some(referrer_id.clone());
1060
1061                    // Update the referrer's referral list
1062                    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    /// Build corporate hierarchies among customers.
1076    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        // Create customer ID to index mapping
1089        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            // Skip enterprise customers (they are parents) and already-hierarchied customers
1097            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                // Assign a parent
1105                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                // Update the child
1110                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                // Update the parent's child list
1115                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    /// Populate engagement metrics for customers.
1126    fn populate_engagement_metrics(
1127        &mut self,
1128        pool: &mut SegmentedCustomerPool,
1129        effective_date: NaiveDate,
1130    ) {
1131        for customer in &mut pool.customers {
1132            // Generate engagement based on lifecycle stage and segment
1133            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            // Calculate churn risk
1196            customer.calculate_churn_risk();
1197
1198            // Calculate upsell potential based on segment and engagement
1199            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    /// Generate a combined output of CustomerPool and SegmentedCustomerPool.
1209    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        // Create a regular CustomerPool from the segmented customers
1220        let mut pool = CustomerPool::new();
1221        for _segmented in &segmented_pool.customers {
1222            let customer = self.generate_customer(company_code, effective_date);
1223            pool.add_customer(customer);
1224        }
1225
1226        (pool, segmented_pool)
1227    }
1228}
1229
1230#[cfg(test)]
1231#[allow(clippy::unwrap_used)]
1232mod tests {
1233    use super::*;
1234
1235    #[test]
1236    fn test_customer_generation() {
1237        let mut gen = CustomerGenerator::new(42);
1238        let customer = gen.generate_customer("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1239
1240        assert!(!customer.customer_id.is_empty());
1241        assert!(!customer.name.is_empty());
1242        assert!(customer.credit_limit > Decimal::ZERO);
1243    }
1244
1245    #[test]
1246    fn test_customer_pool_generation() {
1247        let mut gen = CustomerGenerator::new(42);
1248        let pool =
1249            gen.generate_customer_pool(20, "1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1250
1251        assert_eq!(pool.customers.len(), 20);
1252    }
1253
1254    #[test]
1255    fn test_intercompany_customer() {
1256        let mut gen = CustomerGenerator::new(42);
1257        let customer = gen.generate_intercompany_customer(
1258            "1000",
1259            "2000",
1260            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1261        );
1262
1263        assert!(customer.is_intercompany);
1264        assert_eq!(customer.intercompany_code, Some("2000".to_string()));
1265        assert_eq!(customer.credit_rating, CreditRating::AAA);
1266    }
1267
1268    #[test]
1269    fn test_diverse_pool() {
1270        let mut gen = CustomerGenerator::new(42);
1271        let pool =
1272            gen.generate_diverse_pool(100, "1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1273
1274        // Should have customers with various credit ratings
1275        let aaa_count = pool
1276            .customers
1277            .iter()
1278            .filter(|c| c.credit_rating == CreditRating::AAA)
1279            .count();
1280        let d_count = pool
1281            .customers
1282            .iter()
1283            .filter(|c| c.credit_rating == CreditRating::D)
1284            .count();
1285
1286        assert!(aaa_count > 0);
1287        assert!(d_count > 0);
1288    }
1289
1290    #[test]
1291    fn test_deterministic_generation() {
1292        let mut gen1 = CustomerGenerator::new(42);
1293        let mut gen2 = CustomerGenerator::new(42);
1294
1295        let customer1 =
1296            gen1.generate_customer("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1297        let customer2 =
1298            gen2.generate_customer("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1299
1300        assert_eq!(customer1.customer_id, customer2.customer_id);
1301        assert_eq!(customer1.name, customer2.name);
1302        assert_eq!(customer1.credit_rating, customer2.credit_rating);
1303    }
1304
1305    #[test]
1306    fn test_customer_with_specific_credit() {
1307        let mut gen = CustomerGenerator::new(42);
1308        let customer = gen.generate_customer_with_credit(
1309            "1000",
1310            CreditRating::D,
1311            Decimal::from(5000),
1312            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1313        );
1314
1315        assert_eq!(customer.credit_rating, CreditRating::D);
1316        assert_eq!(customer.credit_limit, Decimal::from(5000));
1317        assert_eq!(customer.payment_behavior, CustomerPaymentBehavior::HighRisk);
1318    }
1319
1320    // ===== Customer Segmentation Tests =====
1321
1322    #[test]
1323    fn test_segmented_pool_generation() {
1324        let segmentation_config = CustomerSegmentationConfig {
1325            enabled: true,
1326            ..Default::default()
1327        };
1328
1329        let mut gen = CustomerGenerator::with_segmentation_config(
1330            42,
1331            CustomerGeneratorConfig::default(),
1332            segmentation_config,
1333        );
1334
1335        let pool = gen.generate_segmented_pool(
1336            100,
1337            "1000",
1338            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1339            Decimal::from(10_000_000),
1340        );
1341
1342        assert_eq!(pool.customers.len(), 100);
1343        assert!(!pool.customers.is_empty());
1344    }
1345
1346    #[test]
1347    fn test_segment_distribution() {
1348        let segmentation_config = CustomerSegmentationConfig {
1349            enabled: true,
1350            segment_distribution: SegmentDistribution {
1351                enterprise: 0.05,
1352                mid_market: 0.20,
1353                smb: 0.50,
1354                consumer: 0.25,
1355            },
1356            ..Default::default()
1357        };
1358
1359        let mut gen = CustomerGenerator::with_segmentation_config(
1360            42,
1361            CustomerGeneratorConfig::default(),
1362            segmentation_config,
1363        );
1364
1365        let pool = gen.generate_segmented_pool(
1366            200,
1367            "1000",
1368            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1369            Decimal::from(10_000_000),
1370        );
1371
1372        // Count by segment
1373        let enterprise_count = pool
1374            .customers
1375            .iter()
1376            .filter(|c| c.segment == CustomerValueSegment::Enterprise)
1377            .count();
1378        let smb_count = pool
1379            .customers
1380            .iter()
1381            .filter(|c| c.segment == CustomerValueSegment::Smb)
1382            .count();
1383
1384        // Enterprise should be ~5% (10 of 200)
1385        assert!((5..=20).contains(&enterprise_count));
1386        // SMB should be ~50% (100 of 200)
1387        assert!((80..=120).contains(&smb_count));
1388    }
1389
1390    #[test]
1391    fn test_referral_network() {
1392        let segmentation_config = CustomerSegmentationConfig {
1393            enabled: true,
1394            referral_config: ReferralConfig {
1395                enabled: true,
1396                referral_rate: 0.30, // Higher rate for testing
1397                max_referrals_per_customer: 5,
1398            },
1399            ..Default::default()
1400        };
1401
1402        let mut gen = CustomerGenerator::with_segmentation_config(
1403            42,
1404            CustomerGeneratorConfig::default(),
1405            segmentation_config,
1406        );
1407
1408        let pool = gen.generate_segmented_pool(
1409            50,
1410            "1000",
1411            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1412            Decimal::from(5_000_000),
1413        );
1414
1415        // Count customers who were referred
1416        let referred_count = pool
1417            .customers
1418            .iter()
1419            .filter(|c| c.network_position.was_referred())
1420            .count();
1421
1422        // Should have some referred customers
1423        assert!(referred_count > 0);
1424    }
1425
1426    #[test]
1427    fn test_corporate_hierarchy() {
1428        let segmentation_config = CustomerSegmentationConfig {
1429            enabled: true,
1430            segment_distribution: SegmentDistribution {
1431                enterprise: 0.10, // More enterprise for testing
1432                mid_market: 0.30,
1433                smb: 0.40,
1434                consumer: 0.20,
1435            },
1436            hierarchy_config: HierarchyConfig {
1437                enabled: true,
1438                hierarchy_rate: 0.50, // Higher rate for testing
1439                max_depth: 3,
1440                billing_consolidation_rate: 0.50,
1441            },
1442            ..Default::default()
1443        };
1444
1445        let mut gen = CustomerGenerator::with_segmentation_config(
1446            42,
1447            CustomerGeneratorConfig::default(),
1448            segmentation_config,
1449        );
1450
1451        let pool = gen.generate_segmented_pool(
1452            50,
1453            "1000",
1454            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1455            Decimal::from(5_000_000),
1456        );
1457
1458        // Count customers in hierarchies (have a parent)
1459        let in_hierarchy_count = pool
1460            .customers
1461            .iter()
1462            .filter(|c| c.network_position.parent_customer.is_some())
1463            .count();
1464
1465        // Should have some customers in hierarchies
1466        assert!(in_hierarchy_count > 0);
1467
1468        // Count enterprise customers with children
1469        let parents_with_children = pool
1470            .customers
1471            .iter()
1472            .filter(|c| {
1473                c.segment == CustomerValueSegment::Enterprise
1474                    && !c.network_position.child_customers.is_empty()
1475            })
1476            .count();
1477
1478        assert!(parents_with_children > 0);
1479    }
1480
1481    #[test]
1482    fn test_lifecycle_stages() {
1483        let segmentation_config = CustomerSegmentationConfig {
1484            enabled: true,
1485            lifecycle_distribution: LifecycleDistribution {
1486                prospect: 0.0,
1487                new: 0.20,
1488                growth: 0.20,
1489                mature: 0.40,
1490                at_risk: 0.15,
1491                churned: 0.05,
1492            },
1493            ..Default::default()
1494        };
1495
1496        let mut gen = CustomerGenerator::with_segmentation_config(
1497            42,
1498            CustomerGeneratorConfig::default(),
1499            segmentation_config,
1500        );
1501
1502        let pool = gen.generate_segmented_pool(
1503            100,
1504            "1000",
1505            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1506            Decimal::from(10_000_000),
1507        );
1508
1509        // Count at-risk customers
1510        let at_risk_count = pool
1511            .customers
1512            .iter()
1513            .filter(|c| matches!(c.lifecycle_stage, CustomerLifecycleStage::AtRisk { .. }))
1514            .count();
1515
1516        // Should be roughly 15%
1517        assert!((5..=30).contains(&at_risk_count));
1518
1519        // Count mature customers
1520        let mature_count = pool
1521            .customers
1522            .iter()
1523            .filter(|c| matches!(c.lifecycle_stage, CustomerLifecycleStage::Mature { .. }))
1524            .count();
1525
1526        // Should be roughly 40%
1527        assert!((25..=55).contains(&mature_count));
1528    }
1529
1530    #[test]
1531    fn test_engagement_metrics() {
1532        let segmentation_config = CustomerSegmentationConfig {
1533            enabled: true,
1534            ..Default::default()
1535        };
1536
1537        let mut gen = CustomerGenerator::with_segmentation_config(
1538            42,
1539            CustomerGeneratorConfig::default(),
1540            segmentation_config,
1541        );
1542
1543        let pool = gen.generate_segmented_pool(
1544            20,
1545            "1000",
1546            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1547            Decimal::from(2_000_000),
1548        );
1549
1550        // All customers should have engagement data populated
1551        for customer in &pool.customers {
1552            // Churned customers may have 0 orders
1553            if !matches!(
1554                customer.lifecycle_stage,
1555                CustomerLifecycleStage::Churned { .. }
1556            ) {
1557                // Active customers should have some orders
1558                assert!(
1559                    customer.engagement.total_orders > 0
1560                        || matches!(
1561                            customer.lifecycle_stage,
1562                            CustomerLifecycleStage::Prospect { .. }
1563                        )
1564                );
1565            }
1566
1567            // Churn risk should be calculated
1568            assert!(customer.churn_risk_score >= 0.0 && customer.churn_risk_score <= 1.0);
1569        }
1570    }
1571
1572    #[test]
1573    fn test_segment_distribution_validation() {
1574        let valid = SegmentDistribution::default();
1575        assert!(valid.validate().is_ok());
1576
1577        let invalid = SegmentDistribution {
1578            enterprise: 0.5,
1579            mid_market: 0.5,
1580            smb: 0.5,
1581            consumer: 0.5,
1582        };
1583        assert!(invalid.validate().is_err());
1584    }
1585
1586    #[test]
1587    fn test_segmentation_disabled() {
1588        let segmentation_config = CustomerSegmentationConfig {
1589            enabled: false,
1590            ..Default::default()
1591        };
1592
1593        let mut gen = CustomerGenerator::with_segmentation_config(
1594            42,
1595            CustomerGeneratorConfig::default(),
1596            segmentation_config,
1597        );
1598
1599        let pool = gen.generate_segmented_pool(
1600            20,
1601            "1000",
1602            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1603            Decimal::from(2_000_000),
1604        );
1605
1606        // Should return empty pool when disabled
1607        assert!(pool.customers.is_empty());
1608    }
1609}