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