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)]
1300mod tests {
1301    use super::*;
1302
1303    #[test]
1304    fn test_customer_generation() {
1305        let mut gen = CustomerGenerator::new(42);
1306        let customer = gen.generate_customer("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1307
1308        assert!(!customer.customer_id.is_empty());
1309        assert!(!customer.name.is_empty());
1310        assert!(customer.credit_limit > Decimal::ZERO);
1311    }
1312
1313    #[test]
1314    fn test_customer_pool_generation() {
1315        let mut gen = CustomerGenerator::new(42);
1316        let pool =
1317            gen.generate_customer_pool(20, "1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1318
1319        assert_eq!(pool.customers.len(), 20);
1320    }
1321
1322    #[test]
1323    fn test_intercompany_customer() {
1324        let mut gen = CustomerGenerator::new(42);
1325        let customer = gen.generate_intercompany_customer(
1326            "1000",
1327            "2000",
1328            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1329        );
1330
1331        assert!(customer.is_intercompany);
1332        assert_eq!(customer.intercompany_code, Some("2000".to_string()));
1333        assert_eq!(customer.credit_rating, CreditRating::AAA);
1334    }
1335
1336    #[test]
1337    fn test_diverse_pool() {
1338        let mut gen = CustomerGenerator::new(42);
1339        let pool =
1340            gen.generate_diverse_pool(100, "1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1341
1342        // Should have customers with various credit ratings
1343        let aaa_count = pool
1344            .customers
1345            .iter()
1346            .filter(|c| c.credit_rating == CreditRating::AAA)
1347            .count();
1348        let d_count = pool
1349            .customers
1350            .iter()
1351            .filter(|c| c.credit_rating == CreditRating::D)
1352            .count();
1353
1354        assert!(aaa_count > 0);
1355        assert!(d_count > 0);
1356    }
1357
1358    #[test]
1359    fn test_deterministic_generation() {
1360        let mut gen1 = CustomerGenerator::new(42);
1361        let mut gen2 = CustomerGenerator::new(42);
1362
1363        let customer1 =
1364            gen1.generate_customer("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1365        let customer2 =
1366            gen2.generate_customer("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1367
1368        assert_eq!(customer1.customer_id, customer2.customer_id);
1369        assert_eq!(customer1.name, customer2.name);
1370        assert_eq!(customer1.credit_rating, customer2.credit_rating);
1371    }
1372
1373    #[test]
1374    fn test_customer_with_specific_credit() {
1375        let mut gen = CustomerGenerator::new(42);
1376        let customer = gen.generate_customer_with_credit(
1377            "1000",
1378            CreditRating::D,
1379            Decimal::from(5000),
1380            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1381        );
1382
1383        assert_eq!(customer.credit_rating, CreditRating::D);
1384        assert_eq!(customer.credit_limit, Decimal::from(5000));
1385        assert_eq!(customer.payment_behavior, CustomerPaymentBehavior::HighRisk);
1386    }
1387
1388    // ===== Customer Segmentation Tests =====
1389
1390    #[test]
1391    fn test_segmented_pool_generation() {
1392        let segmentation_config = CustomerSegmentationConfig {
1393            enabled: true,
1394            ..Default::default()
1395        };
1396
1397        let mut gen = CustomerGenerator::with_segmentation_config(
1398            42,
1399            CustomerGeneratorConfig::default(),
1400            segmentation_config,
1401        );
1402
1403        let pool = gen.generate_segmented_pool(
1404            100,
1405            "1000",
1406            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1407            Decimal::from(10_000_000),
1408        );
1409
1410        assert_eq!(pool.customers.len(), 100);
1411        assert!(!pool.customers.is_empty());
1412    }
1413
1414    #[test]
1415    fn test_segment_distribution() {
1416        let segmentation_config = CustomerSegmentationConfig {
1417            enabled: true,
1418            segment_distribution: SegmentDistribution {
1419                enterprise: 0.05,
1420                mid_market: 0.20,
1421                smb: 0.50,
1422                consumer: 0.25,
1423            },
1424            ..Default::default()
1425        };
1426
1427        let mut gen = CustomerGenerator::with_segmentation_config(
1428            42,
1429            CustomerGeneratorConfig::default(),
1430            segmentation_config,
1431        );
1432
1433        let pool = gen.generate_segmented_pool(
1434            200,
1435            "1000",
1436            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1437            Decimal::from(10_000_000),
1438        );
1439
1440        // Count by segment
1441        let enterprise_count = pool
1442            .customers
1443            .iter()
1444            .filter(|c| c.segment == CustomerValueSegment::Enterprise)
1445            .count();
1446        let smb_count = pool
1447            .customers
1448            .iter()
1449            .filter(|c| c.segment == CustomerValueSegment::Smb)
1450            .count();
1451
1452        // Enterprise should be ~5% (10 of 200)
1453        assert!((5..=20).contains(&enterprise_count));
1454        // SMB should be ~50% (100 of 200)
1455        assert!((80..=120).contains(&smb_count));
1456    }
1457
1458    #[test]
1459    fn test_referral_network() {
1460        let segmentation_config = CustomerSegmentationConfig {
1461            enabled: true,
1462            referral_config: ReferralConfig {
1463                enabled: true,
1464                referral_rate: 0.30, // Higher rate for testing
1465                max_referrals_per_customer: 5,
1466            },
1467            ..Default::default()
1468        };
1469
1470        let mut gen = CustomerGenerator::with_segmentation_config(
1471            42,
1472            CustomerGeneratorConfig::default(),
1473            segmentation_config,
1474        );
1475
1476        let pool = gen.generate_segmented_pool(
1477            50,
1478            "1000",
1479            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1480            Decimal::from(5_000_000),
1481        );
1482
1483        // Count customers who were referred
1484        let referred_count = pool
1485            .customers
1486            .iter()
1487            .filter(|c| c.network_position.was_referred())
1488            .count();
1489
1490        // Should have some referred customers
1491        assert!(referred_count > 0);
1492    }
1493
1494    #[test]
1495    fn test_corporate_hierarchy() {
1496        let segmentation_config = CustomerSegmentationConfig {
1497            enabled: true,
1498            segment_distribution: SegmentDistribution {
1499                enterprise: 0.10, // More enterprise for testing
1500                mid_market: 0.30,
1501                smb: 0.40,
1502                consumer: 0.20,
1503            },
1504            hierarchy_config: HierarchyConfig {
1505                enabled: true,
1506                hierarchy_rate: 0.50, // Higher rate for testing
1507                max_depth: 3,
1508                billing_consolidation_rate: 0.50,
1509            },
1510            ..Default::default()
1511        };
1512
1513        let mut gen = CustomerGenerator::with_segmentation_config(
1514            42,
1515            CustomerGeneratorConfig::default(),
1516            segmentation_config,
1517        );
1518
1519        let pool = gen.generate_segmented_pool(
1520            50,
1521            "1000",
1522            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1523            Decimal::from(5_000_000),
1524        );
1525
1526        // Count customers in hierarchies (have a parent)
1527        let in_hierarchy_count = pool
1528            .customers
1529            .iter()
1530            .filter(|c| c.network_position.parent_customer.is_some())
1531            .count();
1532
1533        // Should have some customers in hierarchies
1534        assert!(in_hierarchy_count > 0);
1535
1536        // Count enterprise customers with children
1537        let parents_with_children = pool
1538            .customers
1539            .iter()
1540            .filter(|c| {
1541                c.segment == CustomerValueSegment::Enterprise
1542                    && !c.network_position.child_customers.is_empty()
1543            })
1544            .count();
1545
1546        assert!(parents_with_children > 0);
1547    }
1548
1549    #[test]
1550    fn test_lifecycle_stages() {
1551        let segmentation_config = CustomerSegmentationConfig {
1552            enabled: true,
1553            lifecycle_distribution: LifecycleDistribution {
1554                prospect: 0.0,
1555                new: 0.20,
1556                growth: 0.20,
1557                mature: 0.40,
1558                at_risk: 0.15,
1559                churned: 0.05,
1560            },
1561            ..Default::default()
1562        };
1563
1564        let mut gen = CustomerGenerator::with_segmentation_config(
1565            42,
1566            CustomerGeneratorConfig::default(),
1567            segmentation_config,
1568        );
1569
1570        let pool = gen.generate_segmented_pool(
1571            100,
1572            "1000",
1573            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1574            Decimal::from(10_000_000),
1575        );
1576
1577        // Count at-risk customers
1578        let at_risk_count = pool
1579            .customers
1580            .iter()
1581            .filter(|c| matches!(c.lifecycle_stage, CustomerLifecycleStage::AtRisk { .. }))
1582            .count();
1583
1584        // Should be roughly 15%
1585        assert!((5..=30).contains(&at_risk_count));
1586
1587        // Count mature customers
1588        let mature_count = pool
1589            .customers
1590            .iter()
1591            .filter(|c| matches!(c.lifecycle_stage, CustomerLifecycleStage::Mature { .. }))
1592            .count();
1593
1594        // Should be roughly 40%
1595        assert!((25..=55).contains(&mature_count));
1596    }
1597
1598    #[test]
1599    fn test_engagement_metrics() {
1600        let segmentation_config = CustomerSegmentationConfig {
1601            enabled: true,
1602            ..Default::default()
1603        };
1604
1605        let mut gen = CustomerGenerator::with_segmentation_config(
1606            42,
1607            CustomerGeneratorConfig::default(),
1608            segmentation_config,
1609        );
1610
1611        let pool = gen.generate_segmented_pool(
1612            20,
1613            "1000",
1614            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1615            Decimal::from(2_000_000),
1616        );
1617
1618        // All customers should have engagement data populated
1619        for customer in &pool.customers {
1620            // Churned customers may have 0 orders
1621            if !matches!(
1622                customer.lifecycle_stage,
1623                CustomerLifecycleStage::Churned { .. }
1624            ) {
1625                // Active customers should have some orders
1626                assert!(
1627                    customer.engagement.total_orders > 0
1628                        || matches!(
1629                            customer.lifecycle_stage,
1630                            CustomerLifecycleStage::Prospect { .. }
1631                        )
1632                );
1633            }
1634
1635            // Churn risk should be calculated
1636            assert!(customer.churn_risk_score >= 0.0 && customer.churn_risk_score <= 1.0);
1637        }
1638    }
1639
1640    #[test]
1641    fn test_segment_distribution_validation() {
1642        let valid = SegmentDistribution::default();
1643        assert!(valid.validate().is_ok());
1644
1645        let invalid = SegmentDistribution {
1646            enterprise: 0.5,
1647            mid_market: 0.5,
1648            smb: 0.5,
1649            consumer: 0.5,
1650        };
1651        assert!(invalid.validate().is_err());
1652    }
1653
1654    #[test]
1655    fn test_segmentation_disabled() {
1656        let segmentation_config = CustomerSegmentationConfig {
1657            enabled: false,
1658            ..Default::default()
1659        };
1660
1661        let mut gen = CustomerGenerator::with_segmentation_config(
1662            42,
1663            CustomerGeneratorConfig::default(),
1664            segmentation_config,
1665        );
1666
1667        let pool = gen.generate_segmented_pool(
1668            20,
1669            "1000",
1670            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1671            Decimal::from(2_000_000),
1672        );
1673
1674        // Should return empty pool when disabled
1675        assert!(pool.customers.is_empty());
1676    }
1677
1678    #[test]
1679    fn test_customer_auxiliary_gl_account_french() {
1680        let mut gen = CustomerGenerator::new(42);
1681        gen.set_coa_framework(CoAFramework::FrenchPcg);
1682        let customer = gen.generate_customer("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1683
1684        assert!(customer.auxiliary_gl_account.is_some());
1685        let aux = customer.auxiliary_gl_account.unwrap();
1686        assert!(
1687            aux.starts_with("411"),
1688            "French PCG customer auxiliary should start with 411, got {}",
1689            aux
1690        );
1691    }
1692
1693    #[test]
1694    fn test_customer_auxiliary_gl_account_german() {
1695        let mut gen = CustomerGenerator::new(42);
1696        gen.set_coa_framework(CoAFramework::GermanSkr04);
1697        let customer = gen.generate_customer("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1698
1699        assert!(customer.auxiliary_gl_account.is_some());
1700        let aux = customer.auxiliary_gl_account.unwrap();
1701        assert!(
1702            aux.starts_with("1200"),
1703            "German SKR04 customer auxiliary should start with 1200, got {}",
1704            aux
1705        );
1706    }
1707
1708    #[test]
1709    fn test_customer_auxiliary_gl_account_us_gaap() {
1710        let mut gen = CustomerGenerator::new(42);
1711        let customer = gen.generate_customer("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1712        assert!(customer.auxiliary_gl_account.is_none());
1713    }
1714
1715    #[test]
1716    fn test_customer_name_dedup() {
1717        let mut gen = CustomerGenerator::new(42);
1718        let mut names = HashSet::new();
1719
1720        for _ in 0..200 {
1721            let customer =
1722                gen.generate_customer("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1723            assert!(
1724                names.insert(customer.name.clone()),
1725                "Duplicate customer name found: {}",
1726                customer.name
1727            );
1728        }
1729    }
1730}