Skip to main content

datasynth_generators/master_data/
vendor_generator.rs

1//! Enhanced vendor generator with realistic payment behavior and bank accounts.
2//!
3//! Now integrates with the realism module for sophisticated vendor naming
4//! based on spend categories, industry patterns, and well-known brands.
5//!
6//! Also supports multi-tier vendor network generation with:
7//! - Supply chain tiers (Tier 1, 2, 3)
8//! - Vendor clustering (Reliable, Standard, Transactional, Problematic)
9//! - Strategic importance and spend tier classification
10//! - Concentration analysis and dependency tracking
11
12use std::collections::HashSet;
13
14use chrono::NaiveDate;
15use datasynth_core::models::{
16    BankAccount, DeclineReason, PaymentHistory, PaymentTerms, SpendTier, StrategicLevel,
17    Substitutability, SupplyChainTier, Vendor, VendorBehavior, VendorCluster, VendorDependency,
18    VendorLifecycleStage, VendorNetwork, VendorPool, VendorQualityScore, VendorRelationship,
19    VendorRelationshipType,
20};
21use datasynth_core::templates::{
22    AddressGenerator, AddressRegion, SpendCategory, VendorNameGenerator,
23};
24use datasynth_core::utils::seeded_rng;
25use rand::prelude::*;
26use rand_chacha::ChaCha8Rng;
27use rust_decimal::Decimal;
28use tracing::debug;
29
30use crate::coa_generator::CoAFramework;
31
32/// Configuration for vendor generation.
33#[derive(Debug, Clone)]
34pub struct VendorGeneratorConfig {
35    /// Distribution of payment terms (terms, probability)
36    pub payment_terms_distribution: Vec<(PaymentTerms, f64)>,
37    /// Distribution of vendor behaviors (behavior, probability)
38    pub behavior_distribution: Vec<(VendorBehavior, f64)>,
39    /// Probability of vendor being intercompany
40    pub intercompany_rate: f64,
41    /// Default country for vendors
42    pub default_country: String,
43    /// Default currency
44    pub default_currency: String,
45    /// Generate bank accounts
46    pub generate_bank_accounts: bool,
47    /// Probability of vendor having multiple bank accounts
48    pub multiple_bank_account_rate: f64,
49    /// Distribution of spend categories (category, probability)
50    pub spend_category_distribution: Vec<(SpendCategory, f64)>,
51    /// Primary region for address generation
52    pub primary_region: AddressRegion,
53    /// Use enhanced realistic naming (via realism module)
54    pub use_enhanced_naming: bool,
55}
56
57impl Default for VendorGeneratorConfig {
58    fn default() -> Self {
59        Self {
60            payment_terms_distribution: vec![
61                (PaymentTerms::Net30, 0.40),
62                (PaymentTerms::Net60, 0.20),
63                (PaymentTerms::TwoTenNet30, 0.25),
64                (PaymentTerms::Net15, 0.10),
65                (PaymentTerms::Immediate, 0.05),
66            ],
67            behavior_distribution: vec![
68                (VendorBehavior::Flexible, 0.60),
69                (VendorBehavior::Strict, 0.25),
70                (VendorBehavior::VeryFlexible, 0.10),
71                (VendorBehavior::Aggressive, 0.05),
72            ],
73            intercompany_rate: 0.05,
74            default_country: "US".to_string(),
75            default_currency: "USD".to_string(),
76            generate_bank_accounts: true,
77            multiple_bank_account_rate: 0.20,
78            spend_category_distribution: vec![
79                (SpendCategory::OfficeSupplies, 0.15),
80                (SpendCategory::ITServices, 0.12),
81                (SpendCategory::ProfessionalServices, 0.12),
82                (SpendCategory::Telecommunications, 0.08),
83                (SpendCategory::Utilities, 0.08),
84                (SpendCategory::RawMaterials, 0.10),
85                (SpendCategory::Logistics, 0.10),
86                (SpendCategory::Marketing, 0.08),
87                (SpendCategory::Facilities, 0.07),
88                (SpendCategory::Staffing, 0.05),
89                (SpendCategory::Travel, 0.05),
90            ],
91            primary_region: AddressRegion::NorthAmerica,
92            use_enhanced_naming: true,
93        }
94    }
95}
96
97/// Configuration for vendor network generation.
98#[derive(Debug, Clone)]
99pub struct VendorNetworkConfig {
100    /// Enable vendor network generation
101    pub enabled: bool,
102    /// Maximum depth of supply chain tiers (1-3)
103    pub depth: u8,
104    /// Number of Tier 1 vendors to generate
105    pub tier1_count: TierCountConfig,
106    /// Number of Tier 2 vendors per Tier 1 parent
107    pub tier2_per_parent: TierCountConfig,
108    /// Number of Tier 3 vendors per Tier 2 parent
109    pub tier3_per_parent: TierCountConfig,
110    /// Cluster distribution
111    pub cluster_distribution: ClusterDistribution,
112    /// Concentration limits
113    pub concentration_limits: ConcentrationLimits,
114    /// Strategic level distribution
115    pub strategic_distribution: Vec<(StrategicLevel, f64)>,
116    /// Single-source percentage
117    pub single_source_percent: f64,
118}
119
120/// Count range for tier generation.
121#[derive(Debug, Clone)]
122pub struct TierCountConfig {
123    /// Minimum count
124    pub min: usize,
125    /// Maximum count
126    pub max: usize,
127}
128
129impl TierCountConfig {
130    /// Create a new tier count config.
131    pub fn new(min: usize, max: usize) -> Self {
132        Self { min, max }
133    }
134
135    /// Sample a count from the range.
136    pub fn sample(&self, rng: &mut impl Rng) -> usize {
137        rng.random_range(self.min..=self.max)
138    }
139}
140
141/// Distribution of vendor clusters.
142#[derive(Debug, Clone)]
143pub struct ClusterDistribution {
144    /// Reliable strategic vendors (default: 20%)
145    pub reliable_strategic: f64,
146    /// Standard operational vendors (default: 50%)
147    pub standard_operational: f64,
148    /// Transactional vendors (default: 25%)
149    pub transactional: f64,
150    /// Problematic vendors (default: 5%)
151    pub problematic: f64,
152}
153
154impl Default for ClusterDistribution {
155    fn default() -> Self {
156        Self {
157            reliable_strategic: 0.20,
158            standard_operational: 0.50,
159            transactional: 0.25,
160            problematic: 0.05,
161        }
162    }
163}
164
165impl ClusterDistribution {
166    /// Validate that distribution sums to 1.0.
167    pub fn validate(&self) -> Result<(), String> {
168        let sum = self.reliable_strategic
169            + self.standard_operational
170            + self.transactional
171            + self.problematic;
172        if (sum - 1.0).abs() > 0.01 {
173            Err(format!("Cluster distribution must sum to 1.0, got {sum}"))
174        } else {
175            Ok(())
176        }
177    }
178
179    /// Select a cluster based on the distribution.
180    pub fn select(&self, roll: f64) -> VendorCluster {
181        let mut cumulative = 0.0;
182
183        cumulative += self.reliable_strategic;
184        if roll < cumulative {
185            return VendorCluster::ReliableStrategic;
186        }
187
188        cumulative += self.standard_operational;
189        if roll < cumulative {
190            return VendorCluster::StandardOperational;
191        }
192
193        cumulative += self.transactional;
194        if roll < cumulative {
195            return VendorCluster::Transactional;
196        }
197
198        VendorCluster::Problematic
199    }
200}
201
202/// Concentration limits for vendor spend.
203#[derive(Debug, Clone)]
204pub struct ConcentrationLimits {
205    /// Maximum concentration for a single vendor (default: 15%)
206    pub max_single_vendor: f64,
207    /// Maximum concentration for top 5 vendors (default: 45%)
208    pub max_top5: f64,
209}
210
211impl Default for ConcentrationLimits {
212    fn default() -> Self {
213        Self {
214            max_single_vendor: 0.15,
215            max_top5: 0.45,
216        }
217    }
218}
219
220impl Default for VendorNetworkConfig {
221    fn default() -> Self {
222        Self {
223            enabled: false,
224            depth: 3,
225            tier1_count: TierCountConfig::new(50, 100),
226            tier2_per_parent: TierCountConfig::new(4, 10),
227            tier3_per_parent: TierCountConfig::new(2, 5),
228            cluster_distribution: ClusterDistribution::default(),
229            concentration_limits: ConcentrationLimits::default(),
230            strategic_distribution: vec![
231                (StrategicLevel::Critical, 0.05),
232                (StrategicLevel::Important, 0.15),
233                (StrategicLevel::Standard, 0.50),
234                (StrategicLevel::Transactional, 0.30),
235            ],
236            single_source_percent: 0.05,
237        }
238    }
239}
240
241/// Bank name templates.
242const BANK_NAMES: &[&str] = &[
243    "First National Bank",
244    "Commerce Bank",
245    "United Banking Corp",
246    "Regional Trust Bank",
247    "Merchants Bank",
248    "Citizens Financial",
249    "Pacific Coast Bank",
250    "Atlantic Commerce Bank",
251    "Midwest Trust Company",
252    "Capital One Commercial",
253];
254
255/// Generator for vendor master data.
256pub struct VendorGenerator {
257    rng: ChaCha8Rng,
258    seed: u64,
259    config: VendorGeneratorConfig,
260    vendor_counter: usize,
261    /// Enhanced vendor name generator from realism module
262    vendor_name_gen: VendorNameGenerator,
263    /// Address generator for vendor addresses
264    address_gen: AddressGenerator,
265    /// Network configuration
266    network_config: VendorNetworkConfig,
267    /// Optional country pack for locale-aware generation
268    country_pack: Option<datasynth_core::CountryPack>,
269    /// Accounting framework for auxiliary GL account generation
270    coa_framework: CoAFramework,
271    /// Tracks used vendor names for deduplication
272    used_names: HashSet<String>,
273    /// Optional template provider for user-supplied bank names (v3.2.0+)
274    template_provider: Option<datasynth_core::templates::SharedTemplateProvider>,
275}
276
277impl VendorGenerator {
278    /// Create a new vendor generator.
279    pub fn new(seed: u64) -> Self {
280        Self::with_config(seed, VendorGeneratorConfig::default())
281    }
282
283    /// Create a new vendor generator with custom configuration.
284    pub fn with_config(seed: u64, config: VendorGeneratorConfig) -> Self {
285        Self {
286            rng: seeded_rng(seed, 0),
287            seed,
288            vendor_name_gen: VendorNameGenerator::new(),
289            address_gen: AddressGenerator::for_region(config.primary_region),
290            config,
291            vendor_counter: 0,
292            network_config: VendorNetworkConfig::default(),
293            country_pack: None,
294            coa_framework: CoAFramework::UsGaap,
295            used_names: HashSet::new(),
296            template_provider: None,
297        }
298    }
299
300    /// Create a new vendor generator with network configuration.
301    pub fn with_network_config(
302        seed: u64,
303        config: VendorGeneratorConfig,
304        network_config: VendorNetworkConfig,
305    ) -> Self {
306        Self {
307            rng: seeded_rng(seed, 0),
308            seed,
309            vendor_name_gen: VendorNameGenerator::new(),
310            address_gen: AddressGenerator::for_region(config.primary_region),
311            config,
312            vendor_counter: 0,
313            network_config,
314            country_pack: None,
315            coa_framework: CoAFramework::UsGaap,
316            used_names: HashSet::new(),
317            template_provider: None,
318        }
319    }
320
321    /// Set the accounting framework for auxiliary GL account generation.
322    pub fn set_coa_framework(&mut self, framework: CoAFramework) {
323        self.coa_framework = framework;
324    }
325
326    /// Set network configuration.
327    pub fn set_network_config(&mut self, network_config: VendorNetworkConfig) {
328        self.network_config = network_config;
329    }
330
331    /// Set the country pack for locale-aware generation.
332    pub fn set_country_pack(&mut self, pack: datasynth_core::CountryPack) {
333        self.country_pack = Some(pack);
334    }
335
336    /// Set a template provider so user-supplied bank names (and other
337    /// template-provided strings) override the embedded pools. (v3.2.0+)
338    ///
339    /// When `None` (default), the embedded `BANK_NAMES` pool is used —
340    /// byte-identical to pre-v3.2.0 output.
341    pub fn set_template_provider(
342        &mut self,
343        provider: datasynth_core::templates::SharedTemplateProvider,
344    ) {
345        self.template_provider = Some(provider);
346    }
347
348    /// Set a counter offset so that generated IDs start after a given value.
349    ///
350    /// This is used when generating vendors for multiple companies in parallel
351    /// to ensure globally unique IDs. For example, if company 0 generates 100
352    /// vendors (V-000001..V-000100), company 1 should set offset=100 so its
353    /// vendors start at V-000101.
354    pub fn set_counter_offset(&mut self, offset: usize) {
355        self.vendor_counter = offset;
356    }
357
358    /// Generate a single vendor.
359    pub fn generate_vendor(&mut self, company_code: &str, _effective_date: NaiveDate) -> Vendor {
360        self.vendor_counter += 1;
361
362        let vendor_id = format!("V-{:06}", self.vendor_counter);
363        let (_category, name) = self.select_vendor_name_unique();
364        let tax_id = self.generate_tax_id();
365        let _address = self.address_gen.generate_commercial(&mut self.rng);
366
367        let mut vendor = Vendor::new(
368            &vendor_id,
369            &name,
370            datasynth_core::models::VendorType::Supplier,
371        );
372        vendor.tax_id = Some(tax_id);
373        vendor.country = self.config.default_country.clone();
374        vendor.currency = self.config.default_currency.clone();
375        // Note: category, effective_date, address are not fields on Vendor
376
377        // Set payment terms
378        vendor.payment_terms = self.select_payment_terms();
379
380        // Set behavior
381        vendor.behavior = self.select_vendor_behavior();
382
383        // Set auxiliary GL account based on accounting framework
384        vendor.auxiliary_gl_account = self.generate_auxiliary_gl_account();
385
386        // Check if intercompany
387        if self.rng.random::<f64>() < self.config.intercompany_rate {
388            vendor.is_intercompany = true;
389            vendor.intercompany_code = Some(format!("IC-{company_code}"));
390        }
391
392        // Generate bank accounts
393        if self.config.generate_bank_accounts {
394            let bank_account = self.generate_bank_account(&vendor.vendor_id);
395            vendor.bank_accounts.push(bank_account);
396
397            if self.rng.random::<f64>() < self.config.multiple_bank_account_rate {
398                let bank_account2 = self.generate_bank_account(&vendor.vendor_id);
399                vendor.bank_accounts.push(bank_account2);
400            }
401        }
402
403        vendor
404    }
405
406    /// Generate an intercompany vendor (always intercompany).
407    pub fn generate_intercompany_vendor(
408        &mut self,
409        company_code: &str,
410        partner_company_code: &str,
411        effective_date: NaiveDate,
412    ) -> Vendor {
413        let mut vendor = self.generate_vendor(company_code, effective_date);
414        vendor.is_intercompany = true;
415        vendor.intercompany_code = Some(partner_company_code.to_string());
416        vendor.name = format!("{partner_company_code} - IC");
417        vendor.payment_terms = PaymentTerms::Immediate; // IC usually immediate
418        vendor
419    }
420
421    /// Generate a vendor pool with specified count.
422    pub fn generate_vendor_pool(
423        &mut self,
424        count: usize,
425        company_code: &str,
426        effective_date: NaiveDate,
427    ) -> VendorPool {
428        debug!(count, company_code, %effective_date, "Generating vendor pool");
429        let mut pool = VendorPool::new();
430
431        for _ in 0..count {
432            let vendor = self.generate_vendor(company_code, effective_date);
433            pool.add_vendor(vendor);
434        }
435
436        pool
437    }
438
439    /// Generate a vendor pool with intercompany vendors.
440    pub fn generate_vendor_pool_with_ic(
441        &mut self,
442        count: usize,
443        company_code: &str,
444        partner_company_codes: &[String],
445        effective_date: NaiveDate,
446    ) -> VendorPool {
447        let mut pool = VendorPool::new();
448
449        // Generate regular vendors
450        let regular_count = count.saturating_sub(partner_company_codes.len());
451        for _ in 0..regular_count {
452            let vendor = self.generate_vendor(company_code, effective_date);
453            pool.add_vendor(vendor);
454        }
455
456        // Generate IC vendors for each partner
457        for partner in partner_company_codes {
458            let vendor = self.generate_intercompany_vendor(company_code, partner, effective_date);
459            pool.add_vendor(vendor);
460        }
461
462        pool
463    }
464
465    /// Select a spend category based on distribution.
466    fn select_spend_category(&mut self) -> SpendCategory {
467        let roll: f64 = self.rng.random();
468        let mut cumulative = 0.0;
469
470        for (category, prob) in &self.config.spend_category_distribution {
471            cumulative += prob;
472            if roll < cumulative {
473                return *category;
474            }
475        }
476
477        SpendCategory::OfficeSupplies
478    }
479
480    /// Select a vendor name using the enhanced realism module or legacy templates.
481    fn select_vendor_name(&mut self) -> (SpendCategory, String) {
482        let category = self.select_spend_category();
483
484        if self.config.use_enhanced_naming {
485            // Use the enhanced VendorNameGenerator from the realism module
486            let name = self.vendor_name_gen.generate(category, &mut self.rng);
487            (category, name)
488        } else {
489            // Fallback to simple category-based names
490            let name = format!("{:?} Vendor {}", category, self.vendor_counter);
491            (category, name)
492        }
493    }
494
495    /// Select a unique vendor name, appending a suffix if collision detected.
496    fn select_vendor_name_unique(&mut self) -> (SpendCategory, String) {
497        let (category, mut name) = self.select_vendor_name();
498
499        // Dedup: if the name was already used, append a disambiguator
500        if self.used_names.contains(&name) {
501            let suffixes = [
502                " II",
503                " III",
504                " & Co.",
505                " Group",
506                " Holdings",
507                " International",
508            ];
509            let mut found_unique = false;
510            for suffix in &suffixes {
511                let candidate = format!("{name}{suffix}");
512                if !self.used_names.contains(&candidate) {
513                    name = candidate;
514                    found_unique = true;
515                    break;
516                }
517            }
518            if !found_unique {
519                // Last resort: append counter
520                name = format!("{} #{}", name, self.vendor_counter);
521            }
522        }
523
524        self.used_names.insert(name.clone());
525        (category, name)
526    }
527
528    /// Generate auxiliary GL account based on accounting framework.
529    fn generate_auxiliary_gl_account(&self) -> Option<String> {
530        match self.coa_framework {
531            CoAFramework::FrenchPcg => {
532                // French PCG: 401XXXX sub-accounts under fournisseurs
533                Some(format!("401{:04}", self.vendor_counter))
534            }
535            CoAFramework::GermanSkr04 => {
536                // German SKR04: AP control (3300) + sequential number
537                Some(format!(
538                    "{}{:04}",
539                    datasynth_core::skr::control_accounts::AP_CONTROL,
540                    self.vendor_counter
541                ))
542            }
543            CoAFramework::UsGaap => None,
544        }
545    }
546
547    /// Select payment terms based on distribution.
548    fn select_payment_terms(&mut self) -> PaymentTerms {
549        let roll: f64 = self.rng.random();
550        let mut cumulative = 0.0;
551
552        for (terms, prob) in &self.config.payment_terms_distribution {
553            cumulative += prob;
554            if roll < cumulative {
555                return *terms;
556            }
557        }
558
559        PaymentTerms::Net30
560    }
561
562    /// Select vendor behavior based on distribution.
563    fn select_vendor_behavior(&mut self) -> VendorBehavior {
564        let roll: f64 = self.rng.random();
565        let mut cumulative = 0.0;
566
567        for (behavior, prob) in &self.config.behavior_distribution {
568            cumulative += prob;
569            if roll < cumulative {
570                return *behavior;
571            }
572        }
573
574        VendorBehavior::Flexible
575    }
576
577    /// Generate a tax ID.
578    fn generate_tax_id(&mut self) -> String {
579        format!(
580            "{:02}-{:07}",
581            self.rng.random_range(10..99),
582            self.rng.random_range(1000000..9999999)
583        )
584    }
585
586    /// Generate a bank account.
587    fn generate_bank_account(&mut self, vendor_id: &str) -> BankAccount {
588        // v3.2.0+: prefer user-supplied bank names from the template
589        // provider when available. Falls back to the embedded pool
590        // (byte-identical behavior to pre-v3.2.0).
591        let bank_name: String = match &self.template_provider {
592            Some(provider) => match provider.get_bank_name(&mut self.rng) {
593                Some(name) => name,
594                None => {
595                    let idx = self.rng.random_range(0..BANK_NAMES.len());
596                    BANK_NAMES[idx].to_string()
597                }
598            },
599            None => {
600                let idx = self.rng.random_range(0..BANK_NAMES.len());
601                BANK_NAMES[idx].to_string()
602            }
603        };
604
605        let routing = format!("{:09}", self.rng.random_range(100000000u64..999999999));
606        let account = format!("{:010}", self.rng.random_range(1000000000u64..9999999999));
607
608        BankAccount {
609            bank_name: bank_name.to_string(),
610            bank_country: "US".to_string(),
611            account_number: account,
612            routing_code: routing,
613            holder_name: format!("Vendor {vendor_id}"),
614            is_primary: self.vendor_counter == 1,
615        }
616    }
617
618    /// Reset the generator.
619    pub fn reset(&mut self) {
620        self.rng = seeded_rng(self.seed, 0);
621        self.vendor_counter = 0;
622        self.vendor_name_gen = VendorNameGenerator::new();
623        self.address_gen = AddressGenerator::for_region(self.config.primary_region);
624        self.used_names.clear();
625    }
626
627    // ===== Vendor Network Generation =====
628
629    /// Generate a complete vendor network with tiered supply chain.
630    pub fn generate_vendor_network(
631        &mut self,
632        company_code: &str,
633        effective_date: NaiveDate,
634        total_annual_spend: Decimal,
635    ) -> VendorNetwork {
636        let mut network = VendorNetwork::new(company_code);
637        network.created_date = Some(effective_date);
638
639        if !self.network_config.enabled {
640            return network;
641        }
642
643        // Generate Tier 1 vendors
644        let tier1_count = self.network_config.tier1_count.sample(&mut self.rng);
645        let tier1_ids = self.generate_tier_vendors(
646            company_code,
647            effective_date,
648            tier1_count,
649            SupplyChainTier::Tier1,
650            None,
651            &mut network,
652        );
653
654        // Generate Tier 2 vendors (if depth >= 2)
655        if self.network_config.depth >= 2 {
656            for tier1_id in &tier1_ids {
657                let tier2_count = self.network_config.tier2_per_parent.sample(&mut self.rng);
658                let tier2_ids = self.generate_tier_vendors(
659                    company_code,
660                    effective_date,
661                    tier2_count,
662                    SupplyChainTier::Tier2,
663                    Some(tier1_id.clone()),
664                    &mut network,
665                );
666
667                // Update Tier 1 vendor's children
668                if let Some(rel) = network.get_relationship_mut(tier1_id) {
669                    rel.child_vendors = tier2_ids.clone();
670                }
671
672                // Generate Tier 3 vendors (if depth >= 3)
673                if self.network_config.depth >= 3 {
674                    for tier2_id in &tier2_ids {
675                        let tier3_count =
676                            self.network_config.tier3_per_parent.sample(&mut self.rng);
677                        let tier3_ids = self.generate_tier_vendors(
678                            company_code,
679                            effective_date,
680                            tier3_count,
681                            SupplyChainTier::Tier3,
682                            Some(tier2_id.clone()),
683                            &mut network,
684                        );
685
686                        // Update Tier 2 vendor's children
687                        if let Some(rel) = network.get_relationship_mut(tier2_id) {
688                            rel.child_vendors = tier3_ids;
689                        }
690                    }
691                }
692            }
693        }
694
695        // Assign annual spend to vendors
696        self.assign_annual_spend(&mut network, total_annual_spend);
697
698        // Calculate network statistics
699        network.calculate_statistics(effective_date);
700
701        network
702    }
703
704    /// Generate vendors for a specific tier.
705    fn generate_tier_vendors(
706        &mut self,
707        company_code: &str,
708        effective_date: NaiveDate,
709        count: usize,
710        tier: SupplyChainTier,
711        parent_id: Option<String>,
712        network: &mut VendorNetwork,
713    ) -> Vec<String> {
714        let mut vendor_ids = Vec::with_capacity(count);
715
716        for _ in 0..count {
717            // Generate base vendor
718            let vendor = self.generate_vendor(company_code, effective_date);
719            let vendor_id = vendor.vendor_id.clone();
720
721            // Create relationship
722            let mut relationship = VendorRelationship::new(
723                vendor_id.clone(),
724                self.select_relationship_type(),
725                tier,
726                self.generate_relationship_start_date(effective_date),
727            );
728
729            // Set parent if applicable
730            if let Some(ref parent) = parent_id {
731                relationship = relationship.with_parent(parent.clone());
732            }
733
734            // Assign cluster, strategic level, and spend tier
735            relationship = relationship
736                .with_cluster(self.select_cluster())
737                .with_strategic_importance(self.select_strategic_level())
738                .with_spend_tier(self.select_spend_tier());
739
740            // Set lifecycle stage
741            relationship.lifecycle_stage = self.generate_lifecycle_stage(effective_date);
742
743            // Initialize quality score based on cluster
744            relationship.quality_score = self.generate_quality_score(&relationship.cluster);
745
746            // Initialize payment history based on cluster
747            relationship.payment_history = self.generate_payment_history(&relationship.cluster);
748
749            // Generate dependency analysis for Tier 1 vendors
750            if tier == SupplyChainTier::Tier1 {
751                relationship.dependency = Some(self.generate_dependency(&vendor_id, &vendor.name));
752            }
753
754            network.add_relationship(relationship);
755            vendor_ids.push(vendor_id);
756        }
757
758        vendor_ids
759    }
760
761    /// Select a relationship type.
762    fn select_relationship_type(&mut self) -> VendorRelationshipType {
763        let roll: f64 = self.rng.random();
764        if roll < 0.40 {
765            VendorRelationshipType::DirectSupplier
766        } else if roll < 0.55 {
767            VendorRelationshipType::ServiceProvider
768        } else if roll < 0.70 {
769            VendorRelationshipType::RawMaterialSupplier
770        } else if roll < 0.80 {
771            VendorRelationshipType::Manufacturer
772        } else if roll < 0.88 {
773            VendorRelationshipType::Distributor
774        } else if roll < 0.94 {
775            VendorRelationshipType::Contractor
776        } else {
777            VendorRelationshipType::OemPartner
778        }
779    }
780
781    /// Select a cluster based on distribution.
782    fn select_cluster(&mut self) -> VendorCluster {
783        let roll: f64 = self.rng.random();
784        self.network_config.cluster_distribution.select(roll)
785    }
786
787    /// Select a strategic level based on distribution.
788    fn select_strategic_level(&mut self) -> StrategicLevel {
789        let roll: f64 = self.rng.random();
790        let mut cumulative = 0.0;
791
792        for (level, prob) in &self.network_config.strategic_distribution {
793            cumulative += prob;
794            if roll < cumulative {
795                return *level;
796            }
797        }
798
799        StrategicLevel::Standard
800    }
801
802    /// Select a spend tier.
803    fn select_spend_tier(&mut self) -> SpendTier {
804        let roll: f64 = self.rng.random();
805        if roll < 0.05 {
806            SpendTier::Platinum
807        } else if roll < 0.20 {
808            SpendTier::Gold
809        } else if roll < 0.50 {
810            SpendTier::Silver
811        } else {
812            SpendTier::Bronze
813        }
814    }
815
816    /// Generate a relationship start date (before effective date).
817    fn generate_relationship_start_date(&mut self, effective_date: NaiveDate) -> NaiveDate {
818        let days_back: i64 = self.rng.random_range(90..3650); // 3 months to 10 years
819        effective_date - chrono::Duration::days(days_back)
820    }
821
822    /// Generate lifecycle stage based on probabilities.
823    fn generate_lifecycle_stage(&mut self, effective_date: NaiveDate) -> VendorLifecycleStage {
824        let roll: f64 = self.rng.random();
825        if roll < 0.05 {
826            VendorLifecycleStage::Onboarding {
827                started: effective_date - chrono::Duration::days(self.rng.random_range(1..60)),
828                expected_completion: effective_date
829                    + chrono::Duration::days(self.rng.random_range(30..90)),
830            }
831        } else if roll < 0.12 {
832            VendorLifecycleStage::RampUp {
833                started: effective_date - chrono::Duration::days(self.rng.random_range(60..180)),
834                target_volume_percent: self.rng.random_range(50..80) as u8,
835            }
836        } else if roll < 0.85 {
837            VendorLifecycleStage::SteadyState {
838                since: effective_date - chrono::Duration::days(self.rng.random_range(180..1825)),
839            }
840        } else if roll < 0.95 {
841            VendorLifecycleStage::Decline {
842                started: effective_date - chrono::Duration::days(self.rng.random_range(30..180)),
843                reason: DeclineReason::QualityIssues,
844            }
845        } else {
846            VendorLifecycleStage::SteadyState {
847                since: effective_date - chrono::Duration::days(self.rng.random_range(365..1825)),
848            }
849        }
850    }
851
852    /// Generate quality score based on cluster.
853    fn generate_quality_score(&mut self, cluster: &VendorCluster) -> VendorQualityScore {
854        let (base_delivery, base_quality, base_invoice, base_response) = match cluster {
855            VendorCluster::ReliableStrategic => (0.97, 0.96, 0.98, 0.95),
856            VendorCluster::StandardOperational => (0.92, 0.90, 0.93, 0.85),
857            VendorCluster::Transactional => (0.85, 0.82, 0.88, 0.75),
858            VendorCluster::Problematic => (0.70, 0.68, 0.75, 0.60),
859        };
860
861        // Add some variance
862        let delivery_variance: f64 = self.rng.random_range(-0.05..0.05);
863        let quality_variance: f64 = self.rng.random_range(-0.05..0.05);
864        let invoice_variance: f64 = self.rng.random_range(-0.05..0.05);
865        let response_variance: f64 = self.rng.random_range(-0.05..0.05);
866
867        VendorQualityScore {
868            delivery_score: (base_delivery + delivery_variance).clamp(0.0_f64, 1.0_f64),
869            quality_score: (base_quality + quality_variance).clamp(0.0_f64, 1.0_f64),
870            invoice_accuracy_score: (base_invoice + invoice_variance).clamp(0.0_f64, 1.0_f64),
871            responsiveness_score: (base_response + response_variance).clamp(0.0_f64, 1.0_f64),
872            last_evaluation: NaiveDate::from_ymd_opt(2024, 1, 1).expect("valid default date"),
873            evaluation_count: self.rng.random_range(1..20),
874        }
875    }
876
877    /// Generate payment history based on cluster.
878    fn generate_payment_history(&mut self, cluster: &VendorCluster) -> PaymentHistory {
879        let total = self.rng.random_range(10..200) as u32;
880        let on_time_rate = cluster.invoice_accuracy_probability();
881        let on_time = (total as f64 * on_time_rate) as u32;
882        let early = (total as f64 * self.rng.random_range(0.05..0.20)) as u32;
883        let late = total.saturating_sub(on_time).saturating_sub(early);
884
885        PaymentHistory {
886            total_invoices: total,
887            on_time_payments: on_time,
888            early_payments: early,
889            late_payments: late,
890            total_amount: Decimal::from(total) * Decimal::from(self.rng.random_range(1000..50000)),
891            average_days_to_pay: self.rng.random_range(20.0..45.0),
892            last_payment_date: None,
893            total_discounts: Decimal::from(early) * Decimal::from(self.rng.random_range(50..500)),
894        }
895    }
896
897    /// Generate vendor dependency analysis.
898    fn generate_dependency(&mut self, vendor_id: &str, vendor_name: &str) -> VendorDependency {
899        let is_single_source = self.rng.random::<f64>() < self.network_config.single_source_percent;
900
901        let substitutability = {
902            let roll: f64 = self.rng.random();
903            if roll < 0.60 {
904                Substitutability::Easy
905            } else if roll < 0.90 {
906                Substitutability::Moderate
907            } else {
908                Substitutability::Difficult
909            }
910        };
911
912        let mut dep = VendorDependency::new(vendor_id, self.infer_spend_category(vendor_name));
913        dep.is_single_source = is_single_source;
914        dep.substitutability = substitutability;
915        dep.concentration_percent = self.rng.random_range(0.01..0.20);
916
917        // Generate alternative vendors if not single source
918        if !is_single_source {
919            let alt_count = self.rng.random_range(1..4);
920            for i in 0..alt_count {
921                dep.alternatives.push(format!("ALT-{vendor_id}-{i:03}"));
922            }
923        }
924
925        dep
926    }
927
928    /// Infer spend category from vendor name (simplified).
929    fn infer_spend_category(&self, _vendor_name: &str) -> String {
930        "General".to_string()
931    }
932
933    /// Assign annual spend to vendors based on Pareto principle.
934    fn assign_annual_spend(&mut self, network: &mut VendorNetwork, total_spend: Decimal) {
935        let tier1_count = network.tier1_vendors.len();
936        if tier1_count == 0 {
937            return;
938        }
939
940        // Generate Pareto-distributed weights
941        let mut weights: Vec<f64> = (0..tier1_count)
942            .map(|_| {
943                // Pareto distribution with alpha = 1.5
944                let u: f64 = self.rng.random_range(0.01..1.0);
945                u.powf(-1.0 / 1.5)
946            })
947            .collect();
948
949        let total_weight: f64 = weights.iter().sum();
950        for w in &mut weights {
951            *w /= total_weight;
952        }
953
954        // Sort to ensure highest weights get assigned to first vendors
955        weights.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
956
957        // Assign spend to Tier 1 vendors
958        for (idx, vendor_id) in network.tier1_vendors.clone().iter().enumerate() {
959            if let Some(rel) = network.get_relationship_mut(vendor_id) {
960                let weight = weights.get(idx).copied().unwrap_or(0.01);
961                let spend = total_spend * Decimal::from_f64_retain(weight).unwrap_or(Decimal::ZERO);
962                rel.annual_spend = spend;
963
964                // Update spend tier based on actual spend
965                if let Some(dep) = &mut rel.dependency {
966                    dep.concentration_percent = weight;
967                }
968            }
969        }
970
971        // Assign smaller amounts to Tier 2/3 (they supply to Tier 1, not directly)
972        let tier2_avg_spend = total_spend / Decimal::from(network.tier2_vendors.len().max(1))
973            * Decimal::from_f64_retain(0.05).unwrap_or(Decimal::ZERO);
974        for vendor_id in &network.tier2_vendors.clone() {
975            if let Some(rel) = network.get_relationship_mut(vendor_id) {
976                rel.annual_spend = tier2_avg_spend
977                    * Decimal::from_f64_retain(self.rng.random_range(0.5..1.5))
978                        .unwrap_or(Decimal::ONE);
979            }
980        }
981
982        let tier3_avg_spend = total_spend / Decimal::from(network.tier3_vendors.len().max(1))
983            * Decimal::from_f64_retain(0.01).unwrap_or(Decimal::ZERO);
984        for vendor_id in &network.tier3_vendors.clone() {
985            if let Some(rel) = network.get_relationship_mut(vendor_id) {
986                rel.annual_spend = tier3_avg_spend
987                    * Decimal::from_f64_retain(self.rng.random_range(0.5..1.5))
988                        .unwrap_or(Decimal::ONE);
989            }
990        }
991    }
992
993    /// Generate a vendor pool with network relationships.
994    pub fn generate_vendor_pool_with_network(
995        &mut self,
996        company_code: &str,
997        effective_date: NaiveDate,
998        total_annual_spend: Decimal,
999    ) -> (VendorPool, VendorNetwork) {
1000        let network =
1001            self.generate_vendor_network(company_code, effective_date, total_annual_spend);
1002
1003        // Create VendorPool from network relationships
1004        let mut pool = VendorPool::new();
1005        for _vendor_id in network
1006            .tier1_vendors
1007            .iter()
1008            .chain(network.tier2_vendors.iter())
1009            .chain(network.tier3_vendors.iter())
1010        {
1011            // Generate a basic vendor for each relationship
1012            // In practice, you'd want to store the full Vendor in the relationship
1013            let vendor = self.generate_vendor(company_code, effective_date);
1014            pool.add_vendor(vendor);
1015        }
1016
1017        (pool, network)
1018    }
1019}
1020
1021#[cfg(test)]
1022mod tests {
1023    use super::*;
1024
1025    #[test]
1026    fn test_vendor_generation() {
1027        let mut gen = VendorGenerator::new(42);
1028        let vendor = gen.generate_vendor("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1029
1030        assert!(!vendor.vendor_id.is_empty());
1031        assert!(!vendor.name.is_empty());
1032        assert!(vendor.tax_id.is_some());
1033        assert!(!vendor.bank_accounts.is_empty());
1034    }
1035
1036    #[test]
1037    fn test_vendor_pool_generation() {
1038        let mut gen = VendorGenerator::new(42);
1039        let pool =
1040            gen.generate_vendor_pool(10, "1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1041
1042        assert_eq!(pool.vendors.len(), 10);
1043    }
1044
1045    #[test]
1046    fn test_intercompany_vendor() {
1047        let mut gen = VendorGenerator::new(42);
1048        let vendor = gen.generate_intercompany_vendor(
1049            "1000",
1050            "2000",
1051            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1052        );
1053
1054        assert!(vendor.is_intercompany);
1055        assert_eq!(vendor.intercompany_code, Some("2000".to_string()));
1056    }
1057
1058    #[test]
1059    fn test_deterministic_generation() {
1060        let mut gen1 = VendorGenerator::new(42);
1061        let mut gen2 = VendorGenerator::new(42);
1062
1063        let vendor1 = gen1.generate_vendor("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1064        let vendor2 = gen2.generate_vendor("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1065
1066        assert_eq!(vendor1.vendor_id, vendor2.vendor_id);
1067        assert_eq!(vendor1.name, vendor2.name);
1068    }
1069
1070    #[test]
1071    fn test_vendor_pool_with_ic() {
1072        // Use config with 0 intercompany_rate to test explicit IC vendors only
1073        let config = VendorGeneratorConfig {
1074            intercompany_rate: 0.0,
1075            ..Default::default()
1076        };
1077        let mut gen = VendorGenerator::with_config(42, config);
1078        let pool = gen.generate_vendor_pool_with_ic(
1079            10,
1080            "1000",
1081            &["2000".to_string(), "3000".to_string()],
1082            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1083        );
1084
1085        assert_eq!(pool.vendors.len(), 10);
1086
1087        let ic_vendors: Vec<_> = pool.vendors.iter().filter(|v| v.is_intercompany).collect();
1088        assert_eq!(ic_vendors.len(), 2);
1089    }
1090
1091    #[test]
1092    fn test_enhanced_vendor_names() {
1093        let mut gen = VendorGenerator::new(42);
1094        let vendor = gen.generate_vendor("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1095
1096        // Enhanced naming should produce more varied, realistic names
1097        assert!(!vendor.name.is_empty());
1098        // Should not be a simple generic name format
1099        assert!(!vendor.name.starts_with("Vendor "));
1100    }
1101
1102    // ===== Vendor Network Tests =====
1103
1104    #[test]
1105    fn test_vendor_network_generation() {
1106        let network_config = VendorNetworkConfig {
1107            enabled: true,
1108            depth: 2,
1109            tier1_count: TierCountConfig::new(5, 10),
1110            tier2_per_parent: TierCountConfig::new(2, 4),
1111            tier3_per_parent: TierCountConfig::new(1, 2),
1112            ..Default::default()
1113        };
1114
1115        let mut gen = VendorGenerator::with_network_config(
1116            42,
1117            VendorGeneratorConfig::default(),
1118            network_config,
1119        );
1120
1121        let network = gen.generate_vendor_network(
1122            "1000",
1123            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1124            Decimal::from(10_000_000),
1125        );
1126
1127        assert!(!network.tier1_vendors.is_empty());
1128        assert!(!network.tier2_vendors.is_empty());
1129        assert!(network.tier1_vendors.len() >= 5);
1130        assert!(network.tier1_vendors.len() <= 10);
1131    }
1132
1133    #[test]
1134    fn test_vendor_network_relationships() {
1135        let network_config = VendorNetworkConfig {
1136            enabled: true,
1137            depth: 2,
1138            tier1_count: TierCountConfig::new(3, 3),
1139            tier2_per_parent: TierCountConfig::new(2, 2),
1140            ..Default::default()
1141        };
1142
1143        let mut gen = VendorGenerator::with_network_config(
1144            42,
1145            VendorGeneratorConfig::default(),
1146            network_config,
1147        );
1148
1149        let network = gen.generate_vendor_network(
1150            "1000",
1151            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1152            Decimal::from(5_000_000),
1153        );
1154
1155        // Check that Tier 2 vendors have parents
1156        for tier2_id in &network.tier2_vendors {
1157            let rel = network.get_relationship(tier2_id).unwrap();
1158            assert!(rel.parent_vendor.is_some());
1159            assert_eq!(rel.tier, SupplyChainTier::Tier2);
1160        }
1161
1162        // Check that Tier 1 vendors have children
1163        for tier1_id in &network.tier1_vendors {
1164            let rel = network.get_relationship(tier1_id).unwrap();
1165            assert!(!rel.child_vendors.is_empty());
1166            assert_eq!(rel.tier, SupplyChainTier::Tier1);
1167        }
1168    }
1169
1170    #[test]
1171    fn test_vendor_network_spend_distribution() {
1172        let network_config = VendorNetworkConfig {
1173            enabled: true,
1174            depth: 1,
1175            tier1_count: TierCountConfig::new(10, 10),
1176            ..Default::default()
1177        };
1178
1179        let mut gen = VendorGenerator::with_network_config(
1180            42,
1181            VendorGeneratorConfig::default(),
1182            network_config,
1183        );
1184
1185        let total_spend = Decimal::from(10_000_000);
1186        let network = gen.generate_vendor_network(
1187            "1000",
1188            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1189            total_spend,
1190        );
1191
1192        // Check that spend is distributed
1193        let total_assigned: Decimal = network.relationships.values().map(|r| r.annual_spend).sum();
1194
1195        assert!(total_assigned > Decimal::ZERO);
1196    }
1197
1198    #[test]
1199    fn test_vendor_network_cluster_distribution() {
1200        let network_config = VendorNetworkConfig {
1201            enabled: true,
1202            depth: 1,
1203            tier1_count: TierCountConfig::new(100, 100),
1204            cluster_distribution: ClusterDistribution::default(),
1205            ..Default::default()
1206        };
1207
1208        let mut gen = VendorGenerator::with_network_config(
1209            42,
1210            VendorGeneratorConfig::default(),
1211            network_config,
1212        );
1213
1214        let network = gen.generate_vendor_network(
1215            "1000",
1216            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1217            Decimal::from(10_000_000),
1218        );
1219
1220        // Count clusters
1221        let mut cluster_counts: std::collections::HashMap<VendorCluster, usize> =
1222            std::collections::HashMap::new();
1223        for rel in network.relationships.values() {
1224            *cluster_counts.entry(rel.cluster).or_insert(0) += 1;
1225        }
1226
1227        // ReliableStrategic should be roughly 20%
1228        let reliable = cluster_counts
1229            .get(&VendorCluster::ReliableStrategic)
1230            .unwrap_or(&0);
1231        assert!(*reliable >= 10 && *reliable <= 35);
1232    }
1233
1234    #[test]
1235    fn test_cluster_distribution_validation() {
1236        let valid = ClusterDistribution::default();
1237        assert!(valid.validate().is_ok());
1238
1239        let invalid = ClusterDistribution {
1240            reliable_strategic: 0.5,
1241            standard_operational: 0.5,
1242            transactional: 0.5,
1243            problematic: 0.5,
1244        };
1245        assert!(invalid.validate().is_err());
1246    }
1247
1248    #[test]
1249    fn test_vendor_network_disabled() {
1250        let network_config = VendorNetworkConfig {
1251            enabled: false,
1252            ..Default::default()
1253        };
1254
1255        let mut gen = VendorGenerator::with_network_config(
1256            42,
1257            VendorGeneratorConfig::default(),
1258            network_config,
1259        );
1260
1261        let network = gen.generate_vendor_network(
1262            "1000",
1263            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1264            Decimal::from(10_000_000),
1265        );
1266
1267        assert!(network.tier1_vendors.is_empty());
1268        assert!(network.relationships.is_empty());
1269    }
1270
1271    #[test]
1272    fn test_vendor_auxiliary_gl_account_french() {
1273        let mut gen = VendorGenerator::new(42);
1274        gen.set_coa_framework(CoAFramework::FrenchPcg);
1275        let vendor = gen.generate_vendor("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1276
1277        assert!(vendor.auxiliary_gl_account.is_some());
1278        let aux = vendor.auxiliary_gl_account.unwrap();
1279        assert!(
1280            aux.starts_with("401"),
1281            "French PCG vendor auxiliary should start with 401, got {}",
1282            aux
1283        );
1284    }
1285
1286    #[test]
1287    fn test_vendor_auxiliary_gl_account_german() {
1288        let mut gen = VendorGenerator::new(42);
1289        gen.set_coa_framework(CoAFramework::GermanSkr04);
1290        let vendor = gen.generate_vendor("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1291
1292        assert!(vendor.auxiliary_gl_account.is_some());
1293        let aux = vendor.auxiliary_gl_account.unwrap();
1294        assert!(
1295            aux.starts_with("3300"),
1296            "German SKR04 vendor auxiliary should start with 3300, got {}",
1297            aux
1298        );
1299    }
1300
1301    #[test]
1302    fn test_vendor_auxiliary_gl_account_us_gaap() {
1303        let mut gen = VendorGenerator::new(42);
1304        // US GAAP is default, should have no auxiliary GL account
1305        let vendor = gen.generate_vendor("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1306        assert!(vendor.auxiliary_gl_account.is_none());
1307    }
1308
1309    #[test]
1310    fn test_vendor_name_dedup() {
1311        let mut gen = VendorGenerator::new(42);
1312        let mut names = HashSet::new();
1313
1314        for _ in 0..200 {
1315            let vendor = gen.generate_vendor("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1316            assert!(
1317                names.insert(vendor.name.clone()),
1318                "Duplicate vendor name found: {}",
1319                vendor.name
1320            );
1321        }
1322    }
1323}