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