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