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 std::collections::HashSet;
10
11use chrono::NaiveDate;
12use datasynth_core::models::{
13    ChurnReason, CreditRating, Customer, CustomerEngagement, CustomerLifecycleStage,
14    CustomerPaymentBehavior, CustomerPool, CustomerValueSegment, PaymentTerms, RiskTrigger,
15    SegmentedCustomer, SegmentedCustomerPool,
16};
17use datasynth_core::utils::seeded_rng;
18use rand::prelude::*;
19use rand_chacha::ChaCha8Rng;
20use rust_decimal::Decimal;
21use tracing::debug;
22
23use crate::coa_generator::CoAFramework;
24
25/// Configuration for customer generation.
26#[derive(Debug, Clone)]
27pub struct CustomerGeneratorConfig {
28    /// Distribution of credit ratings (rating, probability)
29    pub credit_rating_distribution: Vec<(CreditRating, f64)>,
30    /// Distribution of payment behaviors (behavior, probability)
31    pub payment_behavior_distribution: Vec<(CustomerPaymentBehavior, f64)>,
32    /// Distribution of payment terms (terms, probability)
33    pub payment_terms_distribution: Vec<(PaymentTerms, f64)>,
34    /// Probability of customer being intercompany
35    pub intercompany_rate: f64,
36    /// Default country for customers
37    pub default_country: String,
38    /// Default currency
39    pub default_currency: String,
40    /// Credit limit ranges by rating (min, max)
41    pub credit_limits: Vec<(CreditRating, Decimal, Decimal)>,
42}
43
44impl Default for CustomerGeneratorConfig {
45    fn default() -> Self {
46        Self {
47            credit_rating_distribution: vec![
48                (CreditRating::AAA, 0.05),
49                (CreditRating::AA, 0.10),
50                (CreditRating::A, 0.25),
51                (CreditRating::BBB, 0.30),
52                (CreditRating::BB, 0.15),
53                (CreditRating::B, 0.10),
54                (CreditRating::CCC, 0.04),
55                (CreditRating::D, 0.01),
56            ],
57            payment_behavior_distribution: vec![
58                (CustomerPaymentBehavior::EarlyPayer, 0.15),
59                (CustomerPaymentBehavior::OnTime, 0.45),
60                (CustomerPaymentBehavior::SlightlyLate, 0.25),
61                (CustomerPaymentBehavior::OftenLate, 0.10),
62                (CustomerPaymentBehavior::HighRisk, 0.05),
63            ],
64            payment_terms_distribution: vec![
65                (PaymentTerms::Net30, 0.50),
66                (PaymentTerms::Net60, 0.20),
67                (PaymentTerms::TwoTenNet30, 0.20),
68                (PaymentTerms::Net15, 0.05),
69                (PaymentTerms::Immediate, 0.05),
70            ],
71            intercompany_rate: 0.05,
72            default_country: "US".to_string(),
73            default_currency: "USD".to_string(),
74            credit_limits: vec![
75                (
76                    CreditRating::AAA,
77                    Decimal::from(1_000_000),
78                    Decimal::from(10_000_000),
79                ),
80                (
81                    CreditRating::AA,
82                    Decimal::from(500_000),
83                    Decimal::from(2_000_000),
84                ),
85                (
86                    CreditRating::A,
87                    Decimal::from(250_000),
88                    Decimal::from(1_000_000),
89                ),
90                (
91                    CreditRating::BBB,
92                    Decimal::from(100_000),
93                    Decimal::from(500_000),
94                ),
95                (
96                    CreditRating::BB,
97                    Decimal::from(50_000),
98                    Decimal::from(250_000),
99                ),
100                (
101                    CreditRating::B,
102                    Decimal::from(25_000),
103                    Decimal::from(100_000),
104                ),
105                (
106                    CreditRating::CCC,
107                    Decimal::from(10_000),
108                    Decimal::from(50_000),
109                ),
110                (CreditRating::D, Decimal::from(0), Decimal::from(10_000)),
111            ],
112        }
113    }
114}
115
116/// Configuration for customer segmentation.
117#[derive(Debug, Clone)]
118pub struct CustomerSegmentationConfig {
119    /// Enable customer segmentation
120    pub enabled: bool,
121    /// Value segment distribution
122    pub segment_distribution: SegmentDistribution,
123    /// Lifecycle stage distribution
124    pub lifecycle_distribution: LifecycleDistribution,
125    /// Referral network configuration
126    pub referral_config: ReferralConfig,
127    /// Corporate hierarchy configuration
128    pub hierarchy_config: HierarchyConfig,
129    /// Industry distribution
130    pub industry_distribution: Vec<(String, f64)>,
131}
132
133impl Default for CustomerSegmentationConfig {
134    fn default() -> Self {
135        Self {
136            enabled: false,
137            segment_distribution: SegmentDistribution::default(),
138            lifecycle_distribution: LifecycleDistribution::default(),
139            referral_config: ReferralConfig::default(),
140            hierarchy_config: HierarchyConfig::default(),
141            industry_distribution: vec![
142                ("Technology".to_string(), 0.20),
143                ("Manufacturing".to_string(), 0.15),
144                ("Retail".to_string(), 0.15),
145                ("Healthcare".to_string(), 0.12),
146                ("Financial".to_string(), 0.12),
147                ("Energy".to_string(), 0.08),
148                ("Transportation".to_string(), 0.08),
149                ("Construction".to_string(), 0.10),
150            ],
151        }
152    }
153}
154
155/// Distribution of customer value segments.
156#[derive(Debug, Clone)]
157pub struct SegmentDistribution {
158    /// Enterprise segment (customer share)
159    pub enterprise: f64,
160    /// Mid-market segment (customer share)
161    pub mid_market: f64,
162    /// SMB segment (customer share)
163    pub smb: f64,
164    /// Consumer segment (customer share)
165    pub consumer: f64,
166}
167
168impl Default for SegmentDistribution {
169    fn default() -> Self {
170        Self {
171            enterprise: 0.05,
172            mid_market: 0.20,
173            smb: 0.50,
174            consumer: 0.25,
175        }
176    }
177}
178
179impl SegmentDistribution {
180    /// Validate that distribution sums to 1.0.
181    pub fn validate(&self) -> Result<(), String> {
182        let sum = self.enterprise + self.mid_market + self.smb + self.consumer;
183        if (sum - 1.0).abs() > 0.01 {
184            Err(format!("Segment distribution must sum to 1.0, got {}", sum))
185        } else {
186            Ok(())
187        }
188    }
189
190    /// Select a segment based on the distribution.
191    pub fn select(&self, roll: f64) -> CustomerValueSegment {
192        let mut cumulative = 0.0;
193
194        cumulative += self.enterprise;
195        if roll < cumulative {
196            return CustomerValueSegment::Enterprise;
197        }
198
199        cumulative += self.mid_market;
200        if roll < cumulative {
201            return CustomerValueSegment::MidMarket;
202        }
203
204        cumulative += self.smb;
205        if roll < cumulative {
206            return CustomerValueSegment::Smb;
207        }
208
209        CustomerValueSegment::Consumer
210    }
211}
212
213/// Distribution of lifecycle stages.
214#[derive(Debug, Clone)]
215pub struct LifecycleDistribution {
216    /// Prospect rate
217    pub prospect: f64,
218    /// New customer rate
219    pub new: f64,
220    /// Growth stage rate
221    pub growth: f64,
222    /// Mature stage rate
223    pub mature: f64,
224    /// At-risk rate
225    pub at_risk: f64,
226    /// Churned rate
227    pub churned: f64,
228}
229
230impl Default for LifecycleDistribution {
231    fn default() -> Self {
232        Self {
233            prospect: 0.0, // Prospects not typically in active pool
234            new: 0.10,
235            growth: 0.15,
236            mature: 0.60,
237            at_risk: 0.10,
238            churned: 0.05,
239        }
240    }
241}
242
243impl LifecycleDistribution {
244    /// Validate that distribution sums to 1.0.
245    pub fn validate(&self) -> Result<(), String> {
246        let sum =
247            self.prospect + self.new + self.growth + self.mature + self.at_risk + self.churned;
248        if (sum - 1.0).abs() > 0.01 {
249            Err(format!(
250                "Lifecycle distribution must sum to 1.0, got {}",
251                sum
252            ))
253        } else {
254            Ok(())
255        }
256    }
257}
258
259/// Configuration for referral networks.
260#[derive(Debug, Clone)]
261pub struct ReferralConfig {
262    /// Enable referral generation
263    pub enabled: bool,
264    /// Rate of customers acquired via referral
265    pub referral_rate: f64,
266    /// Maximum referrals per customer
267    pub max_referrals_per_customer: usize,
268}
269
270impl Default for ReferralConfig {
271    fn default() -> Self {
272        Self {
273            enabled: true,
274            referral_rate: 0.15,
275            max_referrals_per_customer: 5,
276        }
277    }
278}
279
280/// Configuration for corporate hierarchies.
281#[derive(Debug, Clone)]
282pub struct HierarchyConfig {
283    /// Enable corporate hierarchy generation
284    pub enabled: bool,
285    /// Rate of customers in hierarchies
286    pub hierarchy_rate: f64,
287    /// Maximum hierarchy depth
288    pub max_depth: usize,
289    /// Rate of billing consolidation
290    pub billing_consolidation_rate: f64,
291}
292
293impl Default for HierarchyConfig {
294    fn default() -> Self {
295        Self {
296            enabled: true,
297            hierarchy_rate: 0.30,
298            max_depth: 3,
299            billing_consolidation_rate: 0.50,
300        }
301    }
302}
303
304/// Customer name templates by industry.
305const CUSTOMER_NAME_TEMPLATES: &[(&str, &[&str])] = &[
306    (
307        "Retail",
308        &[
309            "Consumer Goods Corp.",
310            "Retail Solutions Inc.",
311            "Shop Direct Ltd.",
312            "Market Leaders LLC",
313            "Consumer Brands Group",
314            "Retail Partners Co.",
315            "Shopping Networks Inc.",
316            "Direct Sales Corp.",
317        ],
318    ),
319    (
320        "Manufacturing",
321        &[
322            "Industrial Manufacturing Inc.",
323            "Production Systems Corp.",
324            "Assembly Technologies LLC",
325            "Manufacturing Partners Group",
326            "Factory Solutions Ltd.",
327            "Production Line Inc.",
328            "Industrial Works Corp.",
329            "Manufacturing Excellence Co.",
330        ],
331    ),
332    (
333        "Healthcare",
334        &[
335            "Healthcare Systems Inc.",
336            "Medical Solutions Corp.",
337            "Health Partners LLC",
338            "Medical Equipment Group",
339            "Healthcare Providers Ltd.",
340            "Clinical Services Inc.",
341            "Health Networks Corp.",
342            "Medical Supplies Co.",
343        ],
344    ),
345    (
346        "Technology",
347        &[
348            "Tech Innovations Inc.",
349            "Digital Solutions Corp.",
350            "Software Systems LLC",
351            "Technology Partners Group",
352            "IT Solutions Ltd.",
353            "Tech Enterprises Inc.",
354            "Digital Networks Corp.",
355            "Innovation Labs Co.",
356        ],
357    ),
358    (
359        "Financial",
360        &[
361            "Financial Services Inc.",
362            "Banking Solutions Corp.",
363            "Investment Partners LLC",
364            "Financial Networks Group",
365            "Capital Services Ltd.",
366            "Banking Partners Inc.",
367            "Finance Solutions Corp.",
368            "Investment Group Co.",
369        ],
370    ),
371    (
372        "Energy",
373        &[
374            "Energy Solutions Inc.",
375            "Power Systems Corp.",
376            "Renewable Partners LLC",
377            "Energy Networks Group",
378            "Utility Services Ltd.",
379            "Power Generation Inc.",
380            "Energy Partners Corp.",
381            "Sustainable Energy Co.",
382        ],
383    ),
384    (
385        "Transportation",
386        &[
387            "Transport Solutions Inc.",
388            "Logistics Systems Corp.",
389            "Freight Partners LLC",
390            "Transportation Networks Group",
391            "Shipping Services Ltd.",
392            "Fleet Management Inc.",
393            "Logistics Partners Corp.",
394            "Transport Dynamics Co.",
395        ],
396    ),
397    (
398        "Construction",
399        &[
400            "Construction Solutions Inc.",
401            "Building Systems Corp.",
402            "Development Partners LLC",
403            "Construction Group Ltd.",
404            "Building Services Inc.",
405            "Property Development Corp.",
406            "Construction Partners Co.",
407            "Infrastructure Systems LLC",
408        ],
409    ),
410];
411
412/// Generator for customer master data.
413pub struct CustomerGenerator {
414    rng: ChaCha8Rng,
415    seed: u64,
416    config: CustomerGeneratorConfig,
417    customer_counter: usize,
418    /// Segmentation configuration
419    segmentation_config: CustomerSegmentationConfig,
420    /// Optional country pack for locale-aware generation
421    country_pack: Option<datasynth_core::CountryPack>,
422    /// Accounting framework for auxiliary GL account generation
423    coa_framework: CoAFramework,
424    /// Tracks used customer names for deduplication
425    used_names: HashSet<String>,
426}
427
428impl CustomerGenerator {
429    /// Create a new customer generator.
430    pub fn new(seed: u64) -> Self {
431        Self::with_config(seed, CustomerGeneratorConfig::default())
432    }
433
434    /// Create a new customer generator with custom configuration.
435    pub fn with_config(seed: u64, config: CustomerGeneratorConfig) -> Self {
436        Self {
437            rng: seeded_rng(seed, 0),
438            seed,
439            config,
440            customer_counter: 0,
441            segmentation_config: CustomerSegmentationConfig::default(),
442            country_pack: None,
443            coa_framework: CoAFramework::UsGaap,
444            used_names: HashSet::new(),
445        }
446    }
447
448    /// Create a new customer generator with segmentation configuration.
449    pub fn with_segmentation_config(
450        seed: u64,
451        config: CustomerGeneratorConfig,
452        segmentation_config: CustomerSegmentationConfig,
453    ) -> Self {
454        Self {
455            rng: seeded_rng(seed, 0),
456            seed,
457            config,
458            customer_counter: 0,
459            segmentation_config,
460            country_pack: None,
461            coa_framework: CoAFramework::UsGaap,
462            used_names: HashSet::new(),
463        }
464    }
465
466    /// Set the accounting framework for auxiliary GL account generation.
467    pub fn set_coa_framework(&mut self, framework: CoAFramework) {
468        self.coa_framework = framework;
469    }
470
471    /// Set segmentation configuration.
472    pub fn set_segmentation_config(&mut self, segmentation_config: CustomerSegmentationConfig) {
473        self.segmentation_config = segmentation_config;
474    }
475
476    /// Set the country pack for locale-aware generation.
477    pub fn set_country_pack(&mut self, pack: datasynth_core::CountryPack) {
478        self.country_pack = Some(pack);
479    }
480
481    /// Generate a single customer.
482    pub fn generate_customer(
483        &mut self,
484        company_code: &str,
485        _effective_date: NaiveDate,
486    ) -> Customer {
487        self.customer_counter += 1;
488
489        let customer_id = format!("C-{:06}", self.customer_counter);
490        let (_industry, name) = self.select_customer_name_unique();
491
492        let mut customer = Customer::new(
493            &customer_id,
494            &name,
495            datasynth_core::models::CustomerType::Corporate,
496        );
497
498        customer.country = self.config.default_country.clone();
499        customer.currency = self.config.default_currency.clone();
500        // Note: industry and effective_date are not fields on Customer
501
502        // Set credit rating and limit
503        customer.credit_rating = self.select_credit_rating();
504        customer.credit_limit = self.generate_credit_limit(&customer.credit_rating);
505
506        // Set payment behavior
507        customer.payment_behavior = self.select_payment_behavior();
508
509        // Set payment terms
510        customer.payment_terms = self.select_payment_terms();
511
512        // Set auxiliary GL account based on accounting framework
513        customer.auxiliary_gl_account = self.generate_auxiliary_gl_account();
514
515        // Check if intercompany
516        if self.rng.random::<f64>() < self.config.intercompany_rate {
517            customer.is_intercompany = true;
518            customer.intercompany_code = Some(format!("IC-{}", company_code));
519        }
520
521        // Note: address, contact_name, contact_email are not fields on Customer
522
523        customer
524    }
525
526    /// Generate an intercompany customer (always intercompany).
527    pub fn generate_intercompany_customer(
528        &mut self,
529        company_code: &str,
530        partner_company_code: &str,
531        effective_date: NaiveDate,
532    ) -> Customer {
533        let mut customer = self.generate_customer(company_code, effective_date);
534        customer.is_intercompany = true;
535        customer.intercompany_code = Some(partner_company_code.to_string());
536        customer.name = format!("{} - IC", partner_company_code);
537        customer.credit_rating = CreditRating::AAA; // IC always highest rating
538        customer.credit_limit = Decimal::from(100_000_000); // High limit for IC
539        customer.payment_behavior = CustomerPaymentBehavior::OnTime;
540        customer
541    }
542
543    /// Generate a customer with specific credit profile.
544    pub fn generate_customer_with_credit(
545        &mut self,
546        company_code: &str,
547        credit_rating: CreditRating,
548        credit_limit: Decimal,
549        effective_date: NaiveDate,
550    ) -> Customer {
551        let mut customer = self.generate_customer(company_code, effective_date);
552        customer.credit_rating = credit_rating;
553        customer.credit_limit = credit_limit;
554
555        // Adjust payment behavior based on credit rating
556        customer.payment_behavior = match credit_rating {
557            CreditRating::AAA | CreditRating::AA => {
558                if self.rng.random::<f64>() < 0.7 {
559                    CustomerPaymentBehavior::EarlyPayer
560                } else {
561                    CustomerPaymentBehavior::OnTime
562                }
563            }
564            CreditRating::A | CreditRating::BBB => CustomerPaymentBehavior::OnTime,
565            CreditRating::BB | CreditRating::B => CustomerPaymentBehavior::SlightlyLate,
566            CreditRating::CCC | CreditRating::CC => CustomerPaymentBehavior::OftenLate,
567            CreditRating::C | CreditRating::D => CustomerPaymentBehavior::HighRisk,
568        };
569
570        customer
571    }
572
573    /// Generate a customer pool with specified count.
574    pub fn generate_customer_pool(
575        &mut self,
576        count: usize,
577        company_code: &str,
578        effective_date: NaiveDate,
579    ) -> CustomerPool {
580        debug!(count, company_code, %effective_date, "Generating customer pool");
581        let mut pool = CustomerPool::new();
582
583        for _ in 0..count {
584            let customer = self.generate_customer(company_code, effective_date);
585            pool.add_customer(customer);
586        }
587
588        pool
589    }
590
591    /// Generate a customer pool with intercompany customers.
592    pub fn generate_customer_pool_with_ic(
593        &mut self,
594        count: usize,
595        company_code: &str,
596        partner_company_codes: &[String],
597        effective_date: NaiveDate,
598    ) -> CustomerPool {
599        let mut pool = CustomerPool::new();
600
601        // Generate regular customers
602        let regular_count = count.saturating_sub(partner_company_codes.len());
603        for _ in 0..regular_count {
604            let customer = self.generate_customer(company_code, effective_date);
605            pool.add_customer(customer);
606        }
607
608        // Generate IC customers for each partner
609        for partner in partner_company_codes {
610            let customer =
611                self.generate_intercompany_customer(company_code, partner, effective_date);
612            pool.add_customer(customer);
613        }
614
615        pool
616    }
617
618    /// Generate a diverse customer pool with various credit profiles.
619    pub fn generate_diverse_pool(
620        &mut self,
621        count: usize,
622        company_code: &str,
623        effective_date: NaiveDate,
624    ) -> CustomerPool {
625        let mut pool = CustomerPool::new();
626
627        // Generate customers with varied credit ratings ensuring coverage
628        let rating_counts = [
629            (CreditRating::AAA, (count as f64 * 0.05) as usize),
630            (CreditRating::AA, (count as f64 * 0.10) as usize),
631            (CreditRating::A, (count as f64 * 0.20) as usize),
632            (CreditRating::BBB, (count as f64 * 0.30) as usize),
633            (CreditRating::BB, (count as f64 * 0.15) as usize),
634            (CreditRating::B, (count as f64 * 0.10) as usize),
635            (CreditRating::CCC, (count as f64 * 0.07) as usize),
636            (CreditRating::D, (count as f64 * 0.03) as usize),
637        ];
638
639        for (rating, rating_count) in rating_counts {
640            for _ in 0..rating_count {
641                let credit_limit = self.generate_credit_limit(&rating);
642                let customer = self.generate_customer_with_credit(
643                    company_code,
644                    rating,
645                    credit_limit,
646                    effective_date,
647                );
648                pool.add_customer(customer);
649            }
650        }
651
652        // Fill any remaining slots
653        while pool.customers.len() < count {
654            let customer = self.generate_customer(company_code, effective_date);
655            pool.add_customer(customer);
656        }
657
658        pool
659    }
660
661    /// Select a customer name from templates.
662    fn select_customer_name(&mut self) -> (&'static str, &'static str) {
663        let industry_idx = self.rng.random_range(0..CUSTOMER_NAME_TEMPLATES.len());
664        let (industry, names) = CUSTOMER_NAME_TEMPLATES[industry_idx];
665        let name_idx = self.rng.random_range(0..names.len());
666        (industry, names[name_idx])
667    }
668
669    /// Select a unique customer name, appending a suffix if collision detected.
670    fn select_customer_name_unique(&mut self) -> (&'static str, String) {
671        let (industry, name) = self.select_customer_name();
672        let mut name = name.to_string();
673
674        if self.used_names.contains(&name) {
675            let suffixes = [
676                " II",
677                " III",
678                " & Partners",
679                " Group",
680                " Holdings",
681                " International",
682            ];
683            let mut found_unique = false;
684            for suffix in &suffixes {
685                let candidate = format!("{}{}", name, suffix);
686                if !self.used_names.contains(&candidate) {
687                    name = candidate;
688                    found_unique = true;
689                    break;
690                }
691            }
692            if !found_unique {
693                name = format!("{} #{}", name, self.customer_counter);
694            }
695        }
696
697        self.used_names.insert(name.clone());
698        (industry, name)
699    }
700
701    /// Generate auxiliary GL account based on accounting framework.
702    fn generate_auxiliary_gl_account(&self) -> Option<String> {
703        match self.coa_framework {
704            CoAFramework::FrenchPcg => {
705                // French PCG: 411XXXX sub-accounts under clients
706                Some(format!("411{:04}", self.customer_counter))
707            }
708            CoAFramework::GermanSkr04 => {
709                // German SKR04: AR control (1200) + sequential number
710                Some(format!(
711                    "{}{:04}",
712                    datasynth_core::skr::control_accounts::AR_CONTROL,
713                    self.customer_counter
714                ))
715            }
716            CoAFramework::UsGaap => None,
717        }
718    }
719
720    /// Select credit rating based on distribution.
721    fn select_credit_rating(&mut self) -> CreditRating {
722        let roll: f64 = self.rng.random();
723        let mut cumulative = 0.0;
724
725        for (rating, prob) in &self.config.credit_rating_distribution {
726            cumulative += prob;
727            if roll < cumulative {
728                return *rating;
729            }
730        }
731
732        CreditRating::BBB
733    }
734
735    /// Generate credit limit for rating.
736    fn generate_credit_limit(&mut self, rating: &CreditRating) -> Decimal {
737        for (r, min, max) in &self.config.credit_limits {
738            if r == rating {
739                let range = (*max - *min).to_string().parse::<f64>().unwrap_or(0.0);
740                let offset = Decimal::from_f64_retain(self.rng.random::<f64>() * range)
741                    .unwrap_or(Decimal::ZERO);
742                return *min + offset;
743            }
744        }
745
746        Decimal::from(100_000)
747    }
748
749    /// Select payment behavior based on distribution.
750    fn select_payment_behavior(&mut self) -> CustomerPaymentBehavior {
751        let roll: f64 = self.rng.random();
752        let mut cumulative = 0.0;
753
754        for (behavior, prob) in &self.config.payment_behavior_distribution {
755            cumulative += prob;
756            if roll < cumulative {
757                return *behavior;
758            }
759        }
760
761        CustomerPaymentBehavior::OnTime
762    }
763
764    /// Select payment terms based on distribution.
765    fn select_payment_terms(&mut self) -> PaymentTerms {
766        let roll: f64 = self.rng.random();
767        let mut cumulative = 0.0;
768
769        for (terms, prob) in &self.config.payment_terms_distribution {
770            cumulative += prob;
771            if roll < cumulative {
772                return *terms;
773            }
774        }
775
776        PaymentTerms::Net30
777    }
778
779    /// Reset the generator.
780    pub fn reset(&mut self) {
781        self.rng = seeded_rng(self.seed, 0);
782        self.customer_counter = 0;
783        self.used_names.clear();
784    }
785
786    // ===== Customer Segmentation Generation =====
787
788    /// Generate a segmented customer pool with value segments, lifecycle stages, and networks.
789    pub fn generate_segmented_pool(
790        &mut self,
791        count: usize,
792        company_code: &str,
793        effective_date: NaiveDate,
794        total_annual_revenue: Decimal,
795    ) -> SegmentedCustomerPool {
796        let mut pool = SegmentedCustomerPool::new();
797
798        if !self.segmentation_config.enabled {
799            return pool;
800        }
801
802        // Calculate counts by segment
803        let segment_counts = self.calculate_segment_counts(count);
804
805        // Generate customers by segment
806        let mut all_customer_ids: Vec<String> = Vec::new();
807        let mut parent_candidates: Vec<String> = Vec::new();
808
809        for (segment, segment_count) in segment_counts {
810            for _ in 0..segment_count {
811                let customer = self.generate_customer(company_code, effective_date);
812                let customer_id = customer.customer_id.clone();
813
814                let mut segmented =
815                    self.create_segmented_customer(&customer, segment, effective_date);
816
817                // Assign industry
818                segmented.industry = Some(self.select_industry());
819
820                // Assign annual contract value based on segment
821                segmented.annual_contract_value =
822                    self.generate_acv(segment, total_annual_revenue, count);
823
824                // Enterprise customers are candidates for parent relationships
825                if segment == CustomerValueSegment::Enterprise {
826                    parent_candidates.push(customer_id.clone());
827                }
828
829                all_customer_ids.push(customer_id);
830                pool.add_customer(segmented);
831            }
832        }
833
834        // Build referral networks
835        if self.segmentation_config.referral_config.enabled {
836            self.build_referral_networks(&mut pool, &all_customer_ids);
837        }
838
839        // Build corporate hierarchies
840        if self.segmentation_config.hierarchy_config.enabled {
841            self.build_corporate_hierarchies(&mut pool, &all_customer_ids, &parent_candidates);
842        }
843
844        // Calculate engagement metrics and churn risk
845        self.populate_engagement_metrics(&mut pool, effective_date);
846
847        // Calculate statistics
848        pool.calculate_statistics();
849
850        pool
851    }
852
853    /// Calculate customer counts by segment.
854    fn calculate_segment_counts(
855        &mut self,
856        total_count: usize,
857    ) -> Vec<(CustomerValueSegment, usize)> {
858        let dist = &self.segmentation_config.segment_distribution;
859        vec![
860            (
861                CustomerValueSegment::Enterprise,
862                (total_count as f64 * dist.enterprise) as usize,
863            ),
864            (
865                CustomerValueSegment::MidMarket,
866                (total_count as f64 * dist.mid_market) as usize,
867            ),
868            (
869                CustomerValueSegment::Smb,
870                (total_count as f64 * dist.smb) as usize,
871            ),
872            (
873                CustomerValueSegment::Consumer,
874                (total_count as f64 * dist.consumer) as usize,
875            ),
876        ]
877    }
878
879    /// Create a segmented customer from a base customer.
880    fn create_segmented_customer(
881        &mut self,
882        customer: &Customer,
883        segment: CustomerValueSegment,
884        effective_date: NaiveDate,
885    ) -> SegmentedCustomer {
886        let lifecycle_stage = self.generate_lifecycle_stage(effective_date);
887
888        SegmentedCustomer::new(
889            &customer.customer_id,
890            &customer.name,
891            segment,
892            effective_date,
893        )
894        .with_lifecycle_stage(lifecycle_stage)
895    }
896
897    /// Generate lifecycle stage based on distribution.
898    fn generate_lifecycle_stage(&mut self, effective_date: NaiveDate) -> CustomerLifecycleStage {
899        let dist = &self.segmentation_config.lifecycle_distribution;
900        let roll: f64 = self.rng.random();
901        let mut cumulative = 0.0;
902
903        cumulative += dist.prospect;
904        if roll < cumulative {
905            return CustomerLifecycleStage::Prospect {
906                conversion_probability: self.rng.random_range(0.1..0.4),
907                source: Some("Marketing".to_string()),
908                first_contact_date: effective_date
909                    - chrono::Duration::days(self.rng.random_range(1..90)),
910            };
911        }
912
913        cumulative += dist.new;
914        if roll < cumulative {
915            return CustomerLifecycleStage::New {
916                first_order_date: effective_date
917                    - chrono::Duration::days(self.rng.random_range(1..90)),
918                onboarding_complete: self.rng.random::<f64>() > 0.3,
919            };
920        }
921
922        cumulative += dist.growth;
923        if roll < cumulative {
924            return CustomerLifecycleStage::Growth {
925                since: effective_date - chrono::Duration::days(self.rng.random_range(90..365)),
926                growth_rate: self.rng.random_range(0.10..0.50),
927            };
928        }
929
930        cumulative += dist.mature;
931        if roll < cumulative {
932            return CustomerLifecycleStage::Mature {
933                stable_since: effective_date
934                    - chrono::Duration::days(self.rng.random_range(365..1825)),
935                avg_annual_spend: Decimal::from(self.rng.random_range(10000..500000)),
936            };
937        }
938
939        cumulative += dist.at_risk;
940        if roll < cumulative {
941            let triggers = self.generate_risk_triggers();
942            return CustomerLifecycleStage::AtRisk {
943                triggers,
944                flagged_date: effective_date - chrono::Duration::days(self.rng.random_range(7..60)),
945                churn_probability: self.rng.random_range(0.3..0.8),
946            };
947        }
948
949        // Churned
950        CustomerLifecycleStage::Churned {
951            last_activity: effective_date - chrono::Duration::days(self.rng.random_range(90..365)),
952            win_back_probability: self.rng.random_range(0.05..0.25),
953            reason: Some(self.generate_churn_reason()),
954        }
955    }
956
957    /// Generate risk triggers for at-risk customers.
958    fn generate_risk_triggers(&mut self) -> Vec<RiskTrigger> {
959        let all_triggers = [
960            RiskTrigger::DecliningOrderFrequency,
961            RiskTrigger::DecliningOrderValue,
962            RiskTrigger::PaymentIssues,
963            RiskTrigger::Complaints,
964            RiskTrigger::ReducedEngagement,
965            RiskTrigger::ContractExpiring,
966        ];
967
968        let count = self.rng.random_range(1..=3);
969        let mut triggers = Vec::new();
970
971        for _ in 0..count {
972            let idx = self.rng.random_range(0..all_triggers.len());
973            triggers.push(all_triggers[idx].clone());
974        }
975
976        triggers
977    }
978
979    /// Generate churn reason.
980    fn generate_churn_reason(&mut self) -> ChurnReason {
981        let roll: f64 = self.rng.random();
982        if roll < 0.30 {
983            ChurnReason::Competitor
984        } else if roll < 0.50 {
985            ChurnReason::Price
986        } else if roll < 0.65 {
987            ChurnReason::ServiceQuality
988        } else if roll < 0.75 {
989            ChurnReason::BudgetConstraints
990        } else if roll < 0.85 {
991            ChurnReason::ProductFit
992        } else if roll < 0.92 {
993            ChurnReason::Consolidation
994        } else {
995            ChurnReason::Unknown
996        }
997    }
998
999    /// Select an industry based on distribution.
1000    fn select_industry(&mut self) -> String {
1001        let roll: f64 = self.rng.random();
1002        let mut cumulative = 0.0;
1003
1004        for (industry, prob) in &self.segmentation_config.industry_distribution {
1005            cumulative += prob;
1006            if roll < cumulative {
1007                return industry.clone();
1008            }
1009        }
1010
1011        "Other".to_string()
1012    }
1013
1014    /// Generate annual contract value based on segment.
1015    fn generate_acv(
1016        &mut self,
1017        segment: CustomerValueSegment,
1018        total_revenue: Decimal,
1019        total_customers: usize,
1020    ) -> Decimal {
1021        // Calculate expected revenue per customer in this segment
1022        let segment_revenue_share = segment.revenue_share();
1023        let segment_customer_share = segment.customer_share();
1024        let expected_customers_in_segment =
1025            (total_customers as f64 * segment_customer_share) as usize;
1026        let segment_total_revenue = total_revenue
1027            * Decimal::from_f64_retain(segment_revenue_share).unwrap_or(Decimal::ZERO);
1028
1029        let avg_acv = if expected_customers_in_segment > 0 {
1030            segment_total_revenue / Decimal::from(expected_customers_in_segment)
1031        } else {
1032            Decimal::from(10000)
1033        };
1034
1035        // Add variance (±50%)
1036        let variance = self.rng.random_range(0.5..1.5);
1037        avg_acv * Decimal::from_f64_retain(variance).unwrap_or(Decimal::ONE)
1038    }
1039
1040    /// Build referral networks among customers.
1041    fn build_referral_networks(
1042        &mut self,
1043        pool: &mut SegmentedCustomerPool,
1044        customer_ids: &[String],
1045    ) {
1046        let referral_rate = self.segmentation_config.referral_config.referral_rate;
1047        let max_referrals = self
1048            .segmentation_config
1049            .referral_config
1050            .max_referrals_per_customer;
1051
1052        // Track referral counts per customer
1053        let mut referral_counts: std::collections::HashMap<String, usize> =
1054            std::collections::HashMap::new();
1055
1056        // Create customer ID to index mapping
1057        let id_to_idx: std::collections::HashMap<String, usize> = customer_ids
1058            .iter()
1059            .enumerate()
1060            .map(|(idx, id)| (id.clone(), idx))
1061            .collect();
1062
1063        for i in 0..pool.customers.len() {
1064            if self.rng.random::<f64>() < referral_rate {
1065                // This customer was referred - find a referrer
1066                let potential_referrers: Vec<usize> = customer_ids
1067                    .iter()
1068                    .enumerate()
1069                    .filter(|(j, id)| {
1070                        *j != i && referral_counts.get(*id).copied().unwrap_or(0) < max_referrals
1071                    })
1072                    .map(|(j, _)| j)
1073                    .collect();
1074
1075                if !potential_referrers.is_empty() {
1076                    let referrer_idx =
1077                        potential_referrers[self.rng.random_range(0..potential_referrers.len())];
1078                    let referrer_id = customer_ids[referrer_idx].clone();
1079                    let customer_id = pool.customers[i].customer_id.clone();
1080
1081                    // Update the referred customer
1082                    pool.customers[i].network_position.referred_by = Some(referrer_id.clone());
1083
1084                    // Update the referrer's referral list
1085                    if let Some(&ref_idx) = id_to_idx.get(&referrer_id) {
1086                        pool.customers[ref_idx]
1087                            .network_position
1088                            .referrals_made
1089                            .push(customer_id.clone());
1090                    }
1091
1092                    *referral_counts.entry(referrer_id).or_insert(0) += 1;
1093                }
1094            }
1095        }
1096    }
1097
1098    /// Build corporate hierarchies among customers.
1099    fn build_corporate_hierarchies(
1100        &mut self,
1101        pool: &mut SegmentedCustomerPool,
1102        customer_ids: &[String],
1103        parent_candidates: &[String],
1104    ) {
1105        let hierarchy_rate = self.segmentation_config.hierarchy_config.hierarchy_rate;
1106        let billing_consolidation_rate = self
1107            .segmentation_config
1108            .hierarchy_config
1109            .billing_consolidation_rate;
1110
1111        // Create customer ID to index mapping
1112        let id_to_idx: std::collections::HashMap<String, usize> = customer_ids
1113            .iter()
1114            .enumerate()
1115            .map(|(idx, id)| (id.clone(), idx))
1116            .collect();
1117
1118        for i in 0..pool.customers.len() {
1119            // Skip enterprise customers (they are parents) and already-hierarchied customers
1120            if pool.customers[i].segment == CustomerValueSegment::Enterprise
1121                || pool.customers[i].network_position.parent_customer.is_some()
1122            {
1123                continue;
1124            }
1125
1126            if self.rng.random::<f64>() < hierarchy_rate && !parent_candidates.is_empty() {
1127                // Assign a parent
1128                let parent_idx = self.rng.random_range(0..parent_candidates.len());
1129                let parent_id = parent_candidates[parent_idx].clone();
1130                let customer_id = pool.customers[i].customer_id.clone();
1131
1132                // Update the child
1133                pool.customers[i].network_position.parent_customer = Some(parent_id.clone());
1134                pool.customers[i].network_position.billing_consolidation =
1135                    self.rng.random::<f64>() < billing_consolidation_rate;
1136
1137                // Update the parent's child list
1138                if let Some(&parent_idx) = id_to_idx.get(&parent_id) {
1139                    pool.customers[parent_idx]
1140                        .network_position
1141                        .child_customers
1142                        .push(customer_id);
1143                }
1144            }
1145        }
1146    }
1147
1148    /// Populate engagement metrics for customers.
1149    fn populate_engagement_metrics(
1150        &mut self,
1151        pool: &mut SegmentedCustomerPool,
1152        effective_date: NaiveDate,
1153    ) {
1154        for customer in &mut pool.customers {
1155            // Generate engagement based on lifecycle stage and segment
1156            let (base_orders, base_revenue) = match customer.lifecycle_stage {
1157                CustomerLifecycleStage::Mature {
1158                    avg_annual_spend, ..
1159                } => {
1160                    let orders = self.rng.random_range(12..48);
1161                    (orders, avg_annual_spend)
1162                }
1163                CustomerLifecycleStage::Growth { growth_rate, .. } => {
1164                    let orders = self.rng.random_range(6..24);
1165                    let rev = Decimal::from(orders * self.rng.random_range(5000..20000));
1166                    (
1167                        orders,
1168                        rev * Decimal::from_f64_retain(1.0 + growth_rate).unwrap_or(Decimal::ONE),
1169                    )
1170                }
1171                CustomerLifecycleStage::New { .. } => {
1172                    let orders = self.rng.random_range(1..6);
1173                    (
1174                        orders,
1175                        Decimal::from(orders * self.rng.random_range(2000..10000)),
1176                    )
1177                }
1178                CustomerLifecycleStage::AtRisk { .. } => {
1179                    let orders = self.rng.random_range(2..12);
1180                    (
1181                        orders,
1182                        Decimal::from(orders * self.rng.random_range(3000..15000)),
1183                    )
1184                }
1185                CustomerLifecycleStage::Churned { .. } => (0, Decimal::ZERO),
1186                _ => (0, Decimal::ZERO),
1187            };
1188
1189            customer.engagement = CustomerEngagement {
1190                total_orders: base_orders as u32,
1191                orders_last_12_months: (base_orders as f64 * 0.5) as u32,
1192                lifetime_revenue: base_revenue,
1193                revenue_last_12_months: base_revenue
1194                    * Decimal::from_f64_retain(0.5).unwrap_or(Decimal::ZERO),
1195                average_order_value: if base_orders > 0 {
1196                    base_revenue / Decimal::from(base_orders)
1197                } else {
1198                    Decimal::ZERO
1199                },
1200                days_since_last_order: match &customer.lifecycle_stage {
1201                    CustomerLifecycleStage::Churned { last_activity, .. } => {
1202                        (effective_date - *last_activity).num_days().max(0) as u32
1203                    }
1204                    CustomerLifecycleStage::AtRisk { .. } => self.rng.random_range(30..120),
1205                    _ => self.rng.random_range(1..30),
1206                },
1207                last_order_date: Some(
1208                    effective_date - chrono::Duration::days(self.rng.random_range(1..90)),
1209                ),
1210                first_order_date: Some(
1211                    effective_date - chrono::Duration::days(self.rng.random_range(180..1825)),
1212                ),
1213                products_purchased: base_orders as u32 * self.rng.random_range(1..5),
1214                support_tickets: self.rng.random_range(0..10),
1215                nps_score: Some(self.rng.random_range(-20..80) as i8),
1216            };
1217
1218            // Calculate churn risk
1219            customer.calculate_churn_risk();
1220
1221            // Calculate upsell potential based on segment and engagement
1222            customer.upsell_potential = match customer.segment {
1223                CustomerValueSegment::Enterprise => 0.3 + self.rng.random_range(0.0..0.2),
1224                CustomerValueSegment::MidMarket => 0.4 + self.rng.random_range(0.0..0.3),
1225                CustomerValueSegment::Smb => 0.5 + self.rng.random_range(0.0..0.3),
1226                CustomerValueSegment::Consumer => 0.2 + self.rng.random_range(0.0..0.3),
1227            };
1228        }
1229    }
1230
1231    /// Generate a combined output of CustomerPool and SegmentedCustomerPool.
1232    pub fn generate_pool_with_segmentation(
1233        &mut self,
1234        count: usize,
1235        company_code: &str,
1236        effective_date: NaiveDate,
1237        total_annual_revenue: Decimal,
1238    ) -> (CustomerPool, SegmentedCustomerPool) {
1239        let segmented_pool =
1240            self.generate_segmented_pool(count, company_code, effective_date, total_annual_revenue);
1241
1242        // Create a regular CustomerPool from the segmented customers
1243        let mut pool = CustomerPool::new();
1244        for _segmented in &segmented_pool.customers {
1245            let customer = self.generate_customer(company_code, effective_date);
1246            pool.add_customer(customer);
1247        }
1248
1249        (pool, segmented_pool)
1250    }
1251}
1252
1253#[cfg(test)]
1254#[allow(clippy::unwrap_used)]
1255mod tests {
1256    use super::*;
1257
1258    #[test]
1259    fn test_customer_generation() {
1260        let mut gen = CustomerGenerator::new(42);
1261        let customer = gen.generate_customer("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1262
1263        assert!(!customer.customer_id.is_empty());
1264        assert!(!customer.name.is_empty());
1265        assert!(customer.credit_limit > Decimal::ZERO);
1266    }
1267
1268    #[test]
1269    fn test_customer_pool_generation() {
1270        let mut gen = CustomerGenerator::new(42);
1271        let pool =
1272            gen.generate_customer_pool(20, "1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1273
1274        assert_eq!(pool.customers.len(), 20);
1275    }
1276
1277    #[test]
1278    fn test_intercompany_customer() {
1279        let mut gen = CustomerGenerator::new(42);
1280        let customer = gen.generate_intercompany_customer(
1281            "1000",
1282            "2000",
1283            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1284        );
1285
1286        assert!(customer.is_intercompany);
1287        assert_eq!(customer.intercompany_code, Some("2000".to_string()));
1288        assert_eq!(customer.credit_rating, CreditRating::AAA);
1289    }
1290
1291    #[test]
1292    fn test_diverse_pool() {
1293        let mut gen = CustomerGenerator::new(42);
1294        let pool =
1295            gen.generate_diverse_pool(100, "1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1296
1297        // Should have customers with various credit ratings
1298        let aaa_count = pool
1299            .customers
1300            .iter()
1301            .filter(|c| c.credit_rating == CreditRating::AAA)
1302            .count();
1303        let d_count = pool
1304            .customers
1305            .iter()
1306            .filter(|c| c.credit_rating == CreditRating::D)
1307            .count();
1308
1309        assert!(aaa_count > 0);
1310        assert!(d_count > 0);
1311    }
1312
1313    #[test]
1314    fn test_deterministic_generation() {
1315        let mut gen1 = CustomerGenerator::new(42);
1316        let mut gen2 = CustomerGenerator::new(42);
1317
1318        let customer1 =
1319            gen1.generate_customer("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1320        let customer2 =
1321            gen2.generate_customer("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1322
1323        assert_eq!(customer1.customer_id, customer2.customer_id);
1324        assert_eq!(customer1.name, customer2.name);
1325        assert_eq!(customer1.credit_rating, customer2.credit_rating);
1326    }
1327
1328    #[test]
1329    fn test_customer_with_specific_credit() {
1330        let mut gen = CustomerGenerator::new(42);
1331        let customer = gen.generate_customer_with_credit(
1332            "1000",
1333            CreditRating::D,
1334            Decimal::from(5000),
1335            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1336        );
1337
1338        assert_eq!(customer.credit_rating, CreditRating::D);
1339        assert_eq!(customer.credit_limit, Decimal::from(5000));
1340        assert_eq!(customer.payment_behavior, CustomerPaymentBehavior::HighRisk);
1341    }
1342
1343    // ===== Customer Segmentation Tests =====
1344
1345    #[test]
1346    fn test_segmented_pool_generation() {
1347        let segmentation_config = CustomerSegmentationConfig {
1348            enabled: true,
1349            ..Default::default()
1350        };
1351
1352        let mut gen = CustomerGenerator::with_segmentation_config(
1353            42,
1354            CustomerGeneratorConfig::default(),
1355            segmentation_config,
1356        );
1357
1358        let pool = gen.generate_segmented_pool(
1359            100,
1360            "1000",
1361            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1362            Decimal::from(10_000_000),
1363        );
1364
1365        assert_eq!(pool.customers.len(), 100);
1366        assert!(!pool.customers.is_empty());
1367    }
1368
1369    #[test]
1370    fn test_segment_distribution() {
1371        let segmentation_config = CustomerSegmentationConfig {
1372            enabled: true,
1373            segment_distribution: SegmentDistribution {
1374                enterprise: 0.05,
1375                mid_market: 0.20,
1376                smb: 0.50,
1377                consumer: 0.25,
1378            },
1379            ..Default::default()
1380        };
1381
1382        let mut gen = CustomerGenerator::with_segmentation_config(
1383            42,
1384            CustomerGeneratorConfig::default(),
1385            segmentation_config,
1386        );
1387
1388        let pool = gen.generate_segmented_pool(
1389            200,
1390            "1000",
1391            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1392            Decimal::from(10_000_000),
1393        );
1394
1395        // Count by segment
1396        let enterprise_count = pool
1397            .customers
1398            .iter()
1399            .filter(|c| c.segment == CustomerValueSegment::Enterprise)
1400            .count();
1401        let smb_count = pool
1402            .customers
1403            .iter()
1404            .filter(|c| c.segment == CustomerValueSegment::Smb)
1405            .count();
1406
1407        // Enterprise should be ~5% (10 of 200)
1408        assert!((5..=20).contains(&enterprise_count));
1409        // SMB should be ~50% (100 of 200)
1410        assert!((80..=120).contains(&smb_count));
1411    }
1412
1413    #[test]
1414    fn test_referral_network() {
1415        let segmentation_config = CustomerSegmentationConfig {
1416            enabled: true,
1417            referral_config: ReferralConfig {
1418                enabled: true,
1419                referral_rate: 0.30, // Higher rate for testing
1420                max_referrals_per_customer: 5,
1421            },
1422            ..Default::default()
1423        };
1424
1425        let mut gen = CustomerGenerator::with_segmentation_config(
1426            42,
1427            CustomerGeneratorConfig::default(),
1428            segmentation_config,
1429        );
1430
1431        let pool = gen.generate_segmented_pool(
1432            50,
1433            "1000",
1434            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1435            Decimal::from(5_000_000),
1436        );
1437
1438        // Count customers who were referred
1439        let referred_count = pool
1440            .customers
1441            .iter()
1442            .filter(|c| c.network_position.was_referred())
1443            .count();
1444
1445        // Should have some referred customers
1446        assert!(referred_count > 0);
1447    }
1448
1449    #[test]
1450    fn test_corporate_hierarchy() {
1451        let segmentation_config = CustomerSegmentationConfig {
1452            enabled: true,
1453            segment_distribution: SegmentDistribution {
1454                enterprise: 0.10, // More enterprise for testing
1455                mid_market: 0.30,
1456                smb: 0.40,
1457                consumer: 0.20,
1458            },
1459            hierarchy_config: HierarchyConfig {
1460                enabled: true,
1461                hierarchy_rate: 0.50, // Higher rate for testing
1462                max_depth: 3,
1463                billing_consolidation_rate: 0.50,
1464            },
1465            ..Default::default()
1466        };
1467
1468        let mut gen = CustomerGenerator::with_segmentation_config(
1469            42,
1470            CustomerGeneratorConfig::default(),
1471            segmentation_config,
1472        );
1473
1474        let pool = gen.generate_segmented_pool(
1475            50,
1476            "1000",
1477            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1478            Decimal::from(5_000_000),
1479        );
1480
1481        // Count customers in hierarchies (have a parent)
1482        let in_hierarchy_count = pool
1483            .customers
1484            .iter()
1485            .filter(|c| c.network_position.parent_customer.is_some())
1486            .count();
1487
1488        // Should have some customers in hierarchies
1489        assert!(in_hierarchy_count > 0);
1490
1491        // Count enterprise customers with children
1492        let parents_with_children = pool
1493            .customers
1494            .iter()
1495            .filter(|c| {
1496                c.segment == CustomerValueSegment::Enterprise
1497                    && !c.network_position.child_customers.is_empty()
1498            })
1499            .count();
1500
1501        assert!(parents_with_children > 0);
1502    }
1503
1504    #[test]
1505    fn test_lifecycle_stages() {
1506        let segmentation_config = CustomerSegmentationConfig {
1507            enabled: true,
1508            lifecycle_distribution: LifecycleDistribution {
1509                prospect: 0.0,
1510                new: 0.20,
1511                growth: 0.20,
1512                mature: 0.40,
1513                at_risk: 0.15,
1514                churned: 0.05,
1515            },
1516            ..Default::default()
1517        };
1518
1519        let mut gen = CustomerGenerator::with_segmentation_config(
1520            42,
1521            CustomerGeneratorConfig::default(),
1522            segmentation_config,
1523        );
1524
1525        let pool = gen.generate_segmented_pool(
1526            100,
1527            "1000",
1528            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1529            Decimal::from(10_000_000),
1530        );
1531
1532        // Count at-risk customers
1533        let at_risk_count = pool
1534            .customers
1535            .iter()
1536            .filter(|c| matches!(c.lifecycle_stage, CustomerLifecycleStage::AtRisk { .. }))
1537            .count();
1538
1539        // Should be roughly 15%
1540        assert!((5..=30).contains(&at_risk_count));
1541
1542        // Count mature customers
1543        let mature_count = pool
1544            .customers
1545            .iter()
1546            .filter(|c| matches!(c.lifecycle_stage, CustomerLifecycleStage::Mature { .. }))
1547            .count();
1548
1549        // Should be roughly 40%
1550        assert!((25..=55).contains(&mature_count));
1551    }
1552
1553    #[test]
1554    fn test_engagement_metrics() {
1555        let segmentation_config = CustomerSegmentationConfig {
1556            enabled: true,
1557            ..Default::default()
1558        };
1559
1560        let mut gen = CustomerGenerator::with_segmentation_config(
1561            42,
1562            CustomerGeneratorConfig::default(),
1563            segmentation_config,
1564        );
1565
1566        let pool = gen.generate_segmented_pool(
1567            20,
1568            "1000",
1569            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1570            Decimal::from(2_000_000),
1571        );
1572
1573        // All customers should have engagement data populated
1574        for customer in &pool.customers {
1575            // Churned customers may have 0 orders
1576            if !matches!(
1577                customer.lifecycle_stage,
1578                CustomerLifecycleStage::Churned { .. }
1579            ) {
1580                // Active customers should have some orders
1581                assert!(
1582                    customer.engagement.total_orders > 0
1583                        || matches!(
1584                            customer.lifecycle_stage,
1585                            CustomerLifecycleStage::Prospect { .. }
1586                        )
1587                );
1588            }
1589
1590            // Churn risk should be calculated
1591            assert!(customer.churn_risk_score >= 0.0 && customer.churn_risk_score <= 1.0);
1592        }
1593    }
1594
1595    #[test]
1596    fn test_segment_distribution_validation() {
1597        let valid = SegmentDistribution::default();
1598        assert!(valid.validate().is_ok());
1599
1600        let invalid = SegmentDistribution {
1601            enterprise: 0.5,
1602            mid_market: 0.5,
1603            smb: 0.5,
1604            consumer: 0.5,
1605        };
1606        assert!(invalid.validate().is_err());
1607    }
1608
1609    #[test]
1610    fn test_segmentation_disabled() {
1611        let segmentation_config = CustomerSegmentationConfig {
1612            enabled: false,
1613            ..Default::default()
1614        };
1615
1616        let mut gen = CustomerGenerator::with_segmentation_config(
1617            42,
1618            CustomerGeneratorConfig::default(),
1619            segmentation_config,
1620        );
1621
1622        let pool = gen.generate_segmented_pool(
1623            20,
1624            "1000",
1625            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1626            Decimal::from(2_000_000),
1627        );
1628
1629        // Should return empty pool when disabled
1630        assert!(pool.customers.is_empty());
1631    }
1632
1633    #[test]
1634    fn test_customer_auxiliary_gl_account_french() {
1635        let mut gen = CustomerGenerator::new(42);
1636        gen.set_coa_framework(CoAFramework::FrenchPcg);
1637        let customer = gen.generate_customer("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1638
1639        assert!(customer.auxiliary_gl_account.is_some());
1640        let aux = customer.auxiliary_gl_account.unwrap();
1641        assert!(
1642            aux.starts_with("411"),
1643            "French PCG customer auxiliary should start with 411, got {}",
1644            aux
1645        );
1646    }
1647
1648    #[test]
1649    fn test_customer_auxiliary_gl_account_german() {
1650        let mut gen = CustomerGenerator::new(42);
1651        gen.set_coa_framework(CoAFramework::GermanSkr04);
1652        let customer = gen.generate_customer("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1653
1654        assert!(customer.auxiliary_gl_account.is_some());
1655        let aux = customer.auxiliary_gl_account.unwrap();
1656        assert!(
1657            aux.starts_with("1200"),
1658            "German SKR04 customer auxiliary should start with 1200, got {}",
1659            aux
1660        );
1661    }
1662
1663    #[test]
1664    fn test_customer_auxiliary_gl_account_us_gaap() {
1665        let mut gen = CustomerGenerator::new(42);
1666        let customer = gen.generate_customer("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1667        assert!(customer.auxiliary_gl_account.is_none());
1668    }
1669
1670    #[test]
1671    fn test_customer_name_dedup() {
1672        let mut gen = CustomerGenerator::new(42);
1673        let mut names = HashSet::new();
1674
1675        for _ in 0..200 {
1676            let customer =
1677                gen.generate_customer("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1678            assert!(
1679                names.insert(customer.name.clone()),
1680                "Duplicate customer name found: {}",
1681                customer.name
1682            );
1683        }
1684    }
1685}