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}
274
275impl VendorGenerator {
276    /// Create a new vendor generator.
277    pub fn new(seed: u64) -> Self {
278        Self::with_config(seed, VendorGeneratorConfig::default())
279    }
280
281    /// Create a new vendor generator with custom configuration.
282    pub fn with_config(seed: u64, config: VendorGeneratorConfig) -> Self {
283        Self {
284            rng: seeded_rng(seed, 0),
285            seed,
286            vendor_name_gen: VendorNameGenerator::new(),
287            address_gen: AddressGenerator::for_region(config.primary_region),
288            config,
289            vendor_counter: 0,
290            network_config: VendorNetworkConfig::default(),
291            country_pack: None,
292            coa_framework: CoAFramework::UsGaap,
293            used_names: HashSet::new(),
294        }
295    }
296
297    /// Create a new vendor generator with network configuration.
298    pub fn with_network_config(
299        seed: u64,
300        config: VendorGeneratorConfig,
301        network_config: VendorNetworkConfig,
302    ) -> Self {
303        Self {
304            rng: seeded_rng(seed, 0),
305            seed,
306            vendor_name_gen: VendorNameGenerator::new(),
307            address_gen: AddressGenerator::for_region(config.primary_region),
308            config,
309            vendor_counter: 0,
310            network_config,
311            country_pack: None,
312            coa_framework: CoAFramework::UsGaap,
313            used_names: HashSet::new(),
314        }
315    }
316
317    /// Set the accounting framework for auxiliary GL account generation.
318    pub fn set_coa_framework(&mut self, framework: CoAFramework) {
319        self.coa_framework = framework;
320    }
321
322    /// Set network configuration.
323    pub fn set_network_config(&mut self, network_config: VendorNetworkConfig) {
324        self.network_config = network_config;
325    }
326
327    /// Set the country pack for locale-aware generation.
328    pub fn set_country_pack(&mut self, pack: datasynth_core::CountryPack) {
329        self.country_pack = Some(pack);
330    }
331
332    /// Generate a single vendor.
333    pub fn generate_vendor(&mut self, company_code: &str, _effective_date: NaiveDate) -> Vendor {
334        self.vendor_counter += 1;
335
336        let vendor_id = format!("V-{:06}", self.vendor_counter);
337        let (_category, name) = self.select_vendor_name_unique();
338        let tax_id = self.generate_tax_id();
339        let _address = self.address_gen.generate_commercial(&mut self.rng);
340
341        let mut vendor = Vendor::new(
342            &vendor_id,
343            &name,
344            datasynth_core::models::VendorType::Supplier,
345        );
346        vendor.tax_id = Some(tax_id);
347        vendor.country = self.config.default_country.clone();
348        vendor.currency = self.config.default_currency.clone();
349        // Note: category, effective_date, address are not fields on Vendor
350
351        // Set payment terms
352        vendor.payment_terms = self.select_payment_terms();
353
354        // Set behavior
355        vendor.behavior = self.select_vendor_behavior();
356
357        // Set auxiliary GL account based on accounting framework
358        vendor.auxiliary_gl_account = self.generate_auxiliary_gl_account();
359
360        // Check if intercompany
361        if self.rng.random::<f64>() < self.config.intercompany_rate {
362            vendor.is_intercompany = true;
363            vendor.intercompany_code = Some(format!("IC-{}", company_code));
364        }
365
366        // Generate bank accounts
367        if self.config.generate_bank_accounts {
368            let bank_account = self.generate_bank_account(&vendor.vendor_id);
369            vendor.bank_accounts.push(bank_account);
370
371            if self.rng.random::<f64>() < self.config.multiple_bank_account_rate {
372                let bank_account2 = self.generate_bank_account(&vendor.vendor_id);
373                vendor.bank_accounts.push(bank_account2);
374            }
375        }
376
377        vendor
378    }
379
380    /// Generate an intercompany vendor (always intercompany).
381    pub fn generate_intercompany_vendor(
382        &mut self,
383        company_code: &str,
384        partner_company_code: &str,
385        effective_date: NaiveDate,
386    ) -> Vendor {
387        let mut vendor = self.generate_vendor(company_code, effective_date);
388        vendor.is_intercompany = true;
389        vendor.intercompany_code = Some(partner_company_code.to_string());
390        vendor.name = format!("{} - IC", partner_company_code);
391        vendor.payment_terms = PaymentTerms::Immediate; // IC usually immediate
392        vendor
393    }
394
395    /// Generate a vendor pool with specified count.
396    pub fn generate_vendor_pool(
397        &mut self,
398        count: usize,
399        company_code: &str,
400        effective_date: NaiveDate,
401    ) -> VendorPool {
402        debug!(count, company_code, %effective_date, "Generating vendor pool");
403        let mut pool = VendorPool::new();
404
405        for _ in 0..count {
406            let vendor = self.generate_vendor(company_code, effective_date);
407            pool.add_vendor(vendor);
408        }
409
410        pool
411    }
412
413    /// Generate a vendor pool with intercompany vendors.
414    pub fn generate_vendor_pool_with_ic(
415        &mut self,
416        count: usize,
417        company_code: &str,
418        partner_company_codes: &[String],
419        effective_date: NaiveDate,
420    ) -> VendorPool {
421        let mut pool = VendorPool::new();
422
423        // Generate regular vendors
424        let regular_count = count.saturating_sub(partner_company_codes.len());
425        for _ in 0..regular_count {
426            let vendor = self.generate_vendor(company_code, effective_date);
427            pool.add_vendor(vendor);
428        }
429
430        // Generate IC vendors for each partner
431        for partner in partner_company_codes {
432            let vendor = self.generate_intercompany_vendor(company_code, partner, effective_date);
433            pool.add_vendor(vendor);
434        }
435
436        pool
437    }
438
439    /// Select a spend category based on distribution.
440    fn select_spend_category(&mut self) -> SpendCategory {
441        let roll: f64 = self.rng.random();
442        let mut cumulative = 0.0;
443
444        for (category, prob) in &self.config.spend_category_distribution {
445            cumulative += prob;
446            if roll < cumulative {
447                return *category;
448            }
449        }
450
451        SpendCategory::OfficeSupplies
452    }
453
454    /// Select a vendor name using the enhanced realism module or legacy templates.
455    fn select_vendor_name(&mut self) -> (SpendCategory, String) {
456        let category = self.select_spend_category();
457
458        if self.config.use_enhanced_naming {
459            // Use the enhanced VendorNameGenerator from the realism module
460            let name = self.vendor_name_gen.generate(category, &mut self.rng);
461            (category, name)
462        } else {
463            // Fallback to simple category-based names
464            let name = format!("{:?} Vendor {}", category, self.vendor_counter);
465            (category, name)
466        }
467    }
468
469    /// Select a unique vendor name, appending a suffix if collision detected.
470    fn select_vendor_name_unique(&mut self) -> (SpendCategory, String) {
471        let (category, mut name) = self.select_vendor_name();
472
473        // Dedup: if the name was already used, append a disambiguator
474        if self.used_names.contains(&name) {
475            let suffixes = [
476                " II",
477                " III",
478                " & Co.",
479                " Group",
480                " Holdings",
481                " International",
482            ];
483            let mut found_unique = false;
484            for suffix in &suffixes {
485                let candidate = format!("{}{}", name, suffix);
486                if !self.used_names.contains(&candidate) {
487                    name = candidate;
488                    found_unique = true;
489                    break;
490                }
491            }
492            if !found_unique {
493                // Last resort: append counter
494                name = format!("{} #{}", name, self.vendor_counter);
495            }
496        }
497
498        self.used_names.insert(name.clone());
499        (category, name)
500    }
501
502    /// Generate auxiliary GL account based on accounting framework.
503    fn generate_auxiliary_gl_account(&self) -> Option<String> {
504        match self.coa_framework {
505            CoAFramework::FrenchPcg => {
506                // French PCG: 401XXXX sub-accounts under fournisseurs
507                Some(format!("401{:04}", self.vendor_counter))
508            }
509            CoAFramework::GermanSkr04 => {
510                // German SKR04: AP control (3300) + sequential number
511                Some(format!(
512                    "{}{:04}",
513                    datasynth_core::skr::control_accounts::AP_CONTROL,
514                    self.vendor_counter
515                ))
516            }
517            CoAFramework::UsGaap => None,
518        }
519    }
520
521    /// Select payment terms based on distribution.
522    fn select_payment_terms(&mut self) -> PaymentTerms {
523        let roll: f64 = self.rng.random();
524        let mut cumulative = 0.0;
525
526        for (terms, prob) in &self.config.payment_terms_distribution {
527            cumulative += prob;
528            if roll < cumulative {
529                return *terms;
530            }
531        }
532
533        PaymentTerms::Net30
534    }
535
536    /// Select vendor behavior based on distribution.
537    fn select_vendor_behavior(&mut self) -> VendorBehavior {
538        let roll: f64 = self.rng.random();
539        let mut cumulative = 0.0;
540
541        for (behavior, prob) in &self.config.behavior_distribution {
542            cumulative += prob;
543            if roll < cumulative {
544                return *behavior;
545            }
546        }
547
548        VendorBehavior::Flexible
549    }
550
551    /// Generate a tax ID.
552    fn generate_tax_id(&mut self) -> String {
553        format!(
554            "{:02}-{:07}",
555            self.rng.random_range(10..99),
556            self.rng.random_range(1000000..9999999)
557        )
558    }
559
560    /// Generate a bank account.
561    fn generate_bank_account(&mut self, vendor_id: &str) -> BankAccount {
562        let bank_idx = self.rng.random_range(0..BANK_NAMES.len());
563        let bank_name = BANK_NAMES[bank_idx];
564
565        let routing = format!("{:09}", self.rng.random_range(100000000u64..999999999));
566        let account = format!("{:010}", self.rng.random_range(1000000000u64..9999999999));
567
568        BankAccount {
569            bank_name: bank_name.to_string(),
570            bank_country: "US".to_string(),
571            account_number: account,
572            routing_code: routing,
573            holder_name: format!("Vendor {}", vendor_id),
574            is_primary: self.vendor_counter == 1,
575        }
576    }
577
578    /// Reset the generator.
579    pub fn reset(&mut self) {
580        self.rng = seeded_rng(self.seed, 0);
581        self.vendor_counter = 0;
582        self.vendor_name_gen = VendorNameGenerator::new();
583        self.address_gen = AddressGenerator::for_region(self.config.primary_region);
584        self.used_names.clear();
585    }
586
587    // ===== Vendor Network Generation =====
588
589    /// Generate a complete vendor network with tiered supply chain.
590    pub fn generate_vendor_network(
591        &mut self,
592        company_code: &str,
593        effective_date: NaiveDate,
594        total_annual_spend: Decimal,
595    ) -> VendorNetwork {
596        let mut network = VendorNetwork::new(company_code);
597        network.created_date = Some(effective_date);
598
599        if !self.network_config.enabled {
600            return network;
601        }
602
603        // Generate Tier 1 vendors
604        let tier1_count = self.network_config.tier1_count.sample(&mut self.rng);
605        let tier1_ids = self.generate_tier_vendors(
606            company_code,
607            effective_date,
608            tier1_count,
609            SupplyChainTier::Tier1,
610            None,
611            &mut network,
612        );
613
614        // Generate Tier 2 vendors (if depth >= 2)
615        if self.network_config.depth >= 2 {
616            for tier1_id in &tier1_ids {
617                let tier2_count = self.network_config.tier2_per_parent.sample(&mut self.rng);
618                let tier2_ids = self.generate_tier_vendors(
619                    company_code,
620                    effective_date,
621                    tier2_count,
622                    SupplyChainTier::Tier2,
623                    Some(tier1_id.clone()),
624                    &mut network,
625                );
626
627                // Update Tier 1 vendor's children
628                if let Some(rel) = network.get_relationship_mut(tier1_id) {
629                    rel.child_vendors = tier2_ids.clone();
630                }
631
632                // Generate Tier 3 vendors (if depth >= 3)
633                if self.network_config.depth >= 3 {
634                    for tier2_id in &tier2_ids {
635                        let tier3_count =
636                            self.network_config.tier3_per_parent.sample(&mut self.rng);
637                        let tier3_ids = self.generate_tier_vendors(
638                            company_code,
639                            effective_date,
640                            tier3_count,
641                            SupplyChainTier::Tier3,
642                            Some(tier2_id.clone()),
643                            &mut network,
644                        );
645
646                        // Update Tier 2 vendor's children
647                        if let Some(rel) = network.get_relationship_mut(tier2_id) {
648                            rel.child_vendors = tier3_ids;
649                        }
650                    }
651                }
652            }
653        }
654
655        // Assign annual spend to vendors
656        self.assign_annual_spend(&mut network, total_annual_spend);
657
658        // Calculate network statistics
659        network.calculate_statistics(effective_date);
660
661        network
662    }
663
664    /// Generate vendors for a specific tier.
665    fn generate_tier_vendors(
666        &mut self,
667        company_code: &str,
668        effective_date: NaiveDate,
669        count: usize,
670        tier: SupplyChainTier,
671        parent_id: Option<String>,
672        network: &mut VendorNetwork,
673    ) -> Vec<String> {
674        let mut vendor_ids = Vec::with_capacity(count);
675
676        for _ in 0..count {
677            // Generate base vendor
678            let vendor = self.generate_vendor(company_code, effective_date);
679            let vendor_id = vendor.vendor_id.clone();
680
681            // Create relationship
682            let mut relationship = VendorRelationship::new(
683                vendor_id.clone(),
684                self.select_relationship_type(),
685                tier,
686                self.generate_relationship_start_date(effective_date),
687            );
688
689            // Set parent if applicable
690            if let Some(ref parent) = parent_id {
691                relationship = relationship.with_parent(parent.clone());
692            }
693
694            // Assign cluster, strategic level, and spend tier
695            relationship = relationship
696                .with_cluster(self.select_cluster())
697                .with_strategic_importance(self.select_strategic_level())
698                .with_spend_tier(self.select_spend_tier());
699
700            // Set lifecycle stage
701            relationship.lifecycle_stage = self.generate_lifecycle_stage(effective_date);
702
703            // Initialize quality score based on cluster
704            relationship.quality_score = self.generate_quality_score(&relationship.cluster);
705
706            // Initialize payment history based on cluster
707            relationship.payment_history = self.generate_payment_history(&relationship.cluster);
708
709            // Generate dependency analysis for Tier 1 vendors
710            if tier == SupplyChainTier::Tier1 {
711                relationship.dependency = Some(self.generate_dependency(&vendor_id, &vendor.name));
712            }
713
714            network.add_relationship(relationship);
715            vendor_ids.push(vendor_id);
716        }
717
718        vendor_ids
719    }
720
721    /// Select a relationship type.
722    fn select_relationship_type(&mut self) -> VendorRelationshipType {
723        let roll: f64 = self.rng.random();
724        if roll < 0.40 {
725            VendorRelationshipType::DirectSupplier
726        } else if roll < 0.55 {
727            VendorRelationshipType::ServiceProvider
728        } else if roll < 0.70 {
729            VendorRelationshipType::RawMaterialSupplier
730        } else if roll < 0.80 {
731            VendorRelationshipType::Manufacturer
732        } else if roll < 0.88 {
733            VendorRelationshipType::Distributor
734        } else if roll < 0.94 {
735            VendorRelationshipType::Contractor
736        } else {
737            VendorRelationshipType::OemPartner
738        }
739    }
740
741    /// Select a cluster based on distribution.
742    fn select_cluster(&mut self) -> VendorCluster {
743        let roll: f64 = self.rng.random();
744        self.network_config.cluster_distribution.select(roll)
745    }
746
747    /// Select a strategic level based on distribution.
748    fn select_strategic_level(&mut self) -> StrategicLevel {
749        let roll: f64 = self.rng.random();
750        let mut cumulative = 0.0;
751
752        for (level, prob) in &self.network_config.strategic_distribution {
753            cumulative += prob;
754            if roll < cumulative {
755                return *level;
756            }
757        }
758
759        StrategicLevel::Standard
760    }
761
762    /// Select a spend tier.
763    fn select_spend_tier(&mut self) -> SpendTier {
764        let roll: f64 = self.rng.random();
765        if roll < 0.05 {
766            SpendTier::Platinum
767        } else if roll < 0.20 {
768            SpendTier::Gold
769        } else if roll < 0.50 {
770            SpendTier::Silver
771        } else {
772            SpendTier::Bronze
773        }
774    }
775
776    /// Generate a relationship start date (before effective date).
777    fn generate_relationship_start_date(&mut self, effective_date: NaiveDate) -> NaiveDate {
778        let days_back: i64 = self.rng.random_range(90..3650); // 3 months to 10 years
779        effective_date - chrono::Duration::days(days_back)
780    }
781
782    /// Generate lifecycle stage based on probabilities.
783    fn generate_lifecycle_stage(&mut self, effective_date: NaiveDate) -> VendorLifecycleStage {
784        let roll: f64 = self.rng.random();
785        if roll < 0.05 {
786            VendorLifecycleStage::Onboarding {
787                started: effective_date - chrono::Duration::days(self.rng.random_range(1..60)),
788                expected_completion: effective_date
789                    + chrono::Duration::days(self.rng.random_range(30..90)),
790            }
791        } else if roll < 0.12 {
792            VendorLifecycleStage::RampUp {
793                started: effective_date - chrono::Duration::days(self.rng.random_range(60..180)),
794                target_volume_percent: self.rng.random_range(50..80) as u8,
795            }
796        } else if roll < 0.85 {
797            VendorLifecycleStage::SteadyState {
798                since: effective_date - chrono::Duration::days(self.rng.random_range(180..1825)),
799            }
800        } else if roll < 0.95 {
801            VendorLifecycleStage::Decline {
802                started: effective_date - chrono::Duration::days(self.rng.random_range(30..180)),
803                reason: DeclineReason::QualityIssues,
804            }
805        } else {
806            VendorLifecycleStage::SteadyState {
807                since: effective_date - chrono::Duration::days(self.rng.random_range(365..1825)),
808            }
809        }
810    }
811
812    /// Generate quality score based on cluster.
813    fn generate_quality_score(&mut self, cluster: &VendorCluster) -> VendorQualityScore {
814        let (base_delivery, base_quality, base_invoice, base_response) = match cluster {
815            VendorCluster::ReliableStrategic => (0.97, 0.96, 0.98, 0.95),
816            VendorCluster::StandardOperational => (0.92, 0.90, 0.93, 0.85),
817            VendorCluster::Transactional => (0.85, 0.82, 0.88, 0.75),
818            VendorCluster::Problematic => (0.70, 0.68, 0.75, 0.60),
819        };
820
821        // Add some variance
822        let delivery_variance: f64 = self.rng.random_range(-0.05..0.05);
823        let quality_variance: f64 = self.rng.random_range(-0.05..0.05);
824        let invoice_variance: f64 = self.rng.random_range(-0.05..0.05);
825        let response_variance: f64 = self.rng.random_range(-0.05..0.05);
826
827        VendorQualityScore {
828            delivery_score: (base_delivery + delivery_variance).clamp(0.0_f64, 1.0_f64),
829            quality_score: (base_quality + quality_variance).clamp(0.0_f64, 1.0_f64),
830            invoice_accuracy_score: (base_invoice + invoice_variance).clamp(0.0_f64, 1.0_f64),
831            responsiveness_score: (base_response + response_variance).clamp(0.0_f64, 1.0_f64),
832            last_evaluation: NaiveDate::from_ymd_opt(2024, 1, 1).expect("valid default date"),
833            evaluation_count: self.rng.random_range(1..20),
834        }
835    }
836
837    /// Generate payment history based on cluster.
838    fn generate_payment_history(&mut self, cluster: &VendorCluster) -> PaymentHistory {
839        let total = self.rng.random_range(10..200) as u32;
840        let on_time_rate = cluster.invoice_accuracy_probability();
841        let on_time = (total as f64 * on_time_rate) as u32;
842        let early = (total as f64 * self.rng.random_range(0.05..0.20)) as u32;
843        let late = total.saturating_sub(on_time).saturating_sub(early);
844
845        PaymentHistory {
846            total_invoices: total,
847            on_time_payments: on_time,
848            early_payments: early,
849            late_payments: late,
850            total_amount: Decimal::from(total) * Decimal::from(self.rng.random_range(1000..50000)),
851            average_days_to_pay: self.rng.random_range(20.0..45.0),
852            last_payment_date: None,
853            total_discounts: Decimal::from(early) * Decimal::from(self.rng.random_range(50..500)),
854        }
855    }
856
857    /// Generate vendor dependency analysis.
858    fn generate_dependency(&mut self, vendor_id: &str, vendor_name: &str) -> VendorDependency {
859        let is_single_source = self.rng.random::<f64>() < self.network_config.single_source_percent;
860
861        let substitutability = {
862            let roll: f64 = self.rng.random();
863            if roll < 0.60 {
864                Substitutability::Easy
865            } else if roll < 0.90 {
866                Substitutability::Moderate
867            } else {
868                Substitutability::Difficult
869            }
870        };
871
872        let mut dep = VendorDependency::new(vendor_id, self.infer_spend_category(vendor_name));
873        dep.is_single_source = is_single_source;
874        dep.substitutability = substitutability;
875        dep.concentration_percent = self.rng.random_range(0.01..0.20);
876
877        // Generate alternative vendors if not single source
878        if !is_single_source {
879            let alt_count = self.rng.random_range(1..4);
880            for i in 0..alt_count {
881                dep.alternatives.push(format!("ALT-{}-{:03}", vendor_id, i));
882            }
883        }
884
885        dep
886    }
887
888    /// Infer spend category from vendor name (simplified).
889    fn infer_spend_category(&self, _vendor_name: &str) -> String {
890        "General".to_string()
891    }
892
893    /// Assign annual spend to vendors based on Pareto principle.
894    fn assign_annual_spend(&mut self, network: &mut VendorNetwork, total_spend: Decimal) {
895        let tier1_count = network.tier1_vendors.len();
896        if tier1_count == 0 {
897            return;
898        }
899
900        // Generate Pareto-distributed weights
901        let mut weights: Vec<f64> = (0..tier1_count)
902            .map(|_| {
903                // Pareto distribution with alpha = 1.5
904                let u: f64 = self.rng.random_range(0.01..1.0);
905                u.powf(-1.0 / 1.5)
906            })
907            .collect();
908
909        let total_weight: f64 = weights.iter().sum();
910        for w in &mut weights {
911            *w /= total_weight;
912        }
913
914        // Sort to ensure highest weights get assigned to first vendors
915        weights.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
916
917        // Assign spend to Tier 1 vendors
918        for (idx, vendor_id) in network.tier1_vendors.clone().iter().enumerate() {
919            if let Some(rel) = network.get_relationship_mut(vendor_id) {
920                let weight = weights.get(idx).copied().unwrap_or(0.01);
921                let spend = total_spend * Decimal::from_f64_retain(weight).unwrap_or(Decimal::ZERO);
922                rel.annual_spend = spend;
923
924                // Update spend tier based on actual spend
925                if let Some(dep) = &mut rel.dependency {
926                    dep.concentration_percent = weight;
927                }
928            }
929        }
930
931        // Assign smaller amounts to Tier 2/3 (they supply to Tier 1, not directly)
932        let tier2_avg_spend = total_spend / Decimal::from(network.tier2_vendors.len().max(1))
933            * Decimal::from_f64_retain(0.05).unwrap_or(Decimal::ZERO);
934        for vendor_id in &network.tier2_vendors.clone() {
935            if let Some(rel) = network.get_relationship_mut(vendor_id) {
936                rel.annual_spend = tier2_avg_spend
937                    * Decimal::from_f64_retain(self.rng.random_range(0.5..1.5))
938                        .unwrap_or(Decimal::ONE);
939            }
940        }
941
942        let tier3_avg_spend = total_spend / Decimal::from(network.tier3_vendors.len().max(1))
943            * Decimal::from_f64_retain(0.01).unwrap_or(Decimal::ZERO);
944        for vendor_id in &network.tier3_vendors.clone() {
945            if let Some(rel) = network.get_relationship_mut(vendor_id) {
946                rel.annual_spend = tier3_avg_spend
947                    * Decimal::from_f64_retain(self.rng.random_range(0.5..1.5))
948                        .unwrap_or(Decimal::ONE);
949            }
950        }
951    }
952
953    /// Generate a vendor pool with network relationships.
954    pub fn generate_vendor_pool_with_network(
955        &mut self,
956        company_code: &str,
957        effective_date: NaiveDate,
958        total_annual_spend: Decimal,
959    ) -> (VendorPool, VendorNetwork) {
960        let network =
961            self.generate_vendor_network(company_code, effective_date, total_annual_spend);
962
963        // Create VendorPool from network relationships
964        let mut pool = VendorPool::new();
965        for _vendor_id in network
966            .tier1_vendors
967            .iter()
968            .chain(network.tier2_vendors.iter())
969            .chain(network.tier3_vendors.iter())
970        {
971            // Generate a basic vendor for each relationship
972            // In practice, you'd want to store the full Vendor in the relationship
973            let vendor = self.generate_vendor(company_code, effective_date);
974            pool.add_vendor(vendor);
975        }
976
977        (pool, network)
978    }
979}
980
981#[cfg(test)]
982#[allow(clippy::unwrap_used)]
983mod tests {
984    use super::*;
985
986    #[test]
987    fn test_vendor_generation() {
988        let mut gen = VendorGenerator::new(42);
989        let vendor = gen.generate_vendor("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
990
991        assert!(!vendor.vendor_id.is_empty());
992        assert!(!vendor.name.is_empty());
993        assert!(vendor.tax_id.is_some());
994        assert!(!vendor.bank_accounts.is_empty());
995    }
996
997    #[test]
998    fn test_vendor_pool_generation() {
999        let mut gen = VendorGenerator::new(42);
1000        let pool =
1001            gen.generate_vendor_pool(10, "1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1002
1003        assert_eq!(pool.vendors.len(), 10);
1004    }
1005
1006    #[test]
1007    fn test_intercompany_vendor() {
1008        let mut gen = VendorGenerator::new(42);
1009        let vendor = gen.generate_intercompany_vendor(
1010            "1000",
1011            "2000",
1012            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1013        );
1014
1015        assert!(vendor.is_intercompany);
1016        assert_eq!(vendor.intercompany_code, Some("2000".to_string()));
1017    }
1018
1019    #[test]
1020    fn test_deterministic_generation() {
1021        let mut gen1 = VendorGenerator::new(42);
1022        let mut gen2 = VendorGenerator::new(42);
1023
1024        let vendor1 = gen1.generate_vendor("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1025        let vendor2 = gen2.generate_vendor("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1026
1027        assert_eq!(vendor1.vendor_id, vendor2.vendor_id);
1028        assert_eq!(vendor1.name, vendor2.name);
1029    }
1030
1031    #[test]
1032    fn test_vendor_pool_with_ic() {
1033        // Use config with 0 intercompany_rate to test explicit IC vendors only
1034        let config = VendorGeneratorConfig {
1035            intercompany_rate: 0.0,
1036            ..Default::default()
1037        };
1038        let mut gen = VendorGenerator::with_config(42, config);
1039        let pool = gen.generate_vendor_pool_with_ic(
1040            10,
1041            "1000",
1042            &["2000".to_string(), "3000".to_string()],
1043            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1044        );
1045
1046        assert_eq!(pool.vendors.len(), 10);
1047
1048        let ic_vendors: Vec<_> = pool.vendors.iter().filter(|v| v.is_intercompany).collect();
1049        assert_eq!(ic_vendors.len(), 2);
1050    }
1051
1052    #[test]
1053    fn test_enhanced_vendor_names() {
1054        let mut gen = VendorGenerator::new(42);
1055        let vendor = gen.generate_vendor("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1056
1057        // Enhanced naming should produce more varied, realistic names
1058        assert!(!vendor.name.is_empty());
1059        // Should not be a simple generic name format
1060        assert!(!vendor.name.starts_with("Vendor "));
1061    }
1062
1063    // ===== Vendor Network Tests =====
1064
1065    #[test]
1066    fn test_vendor_network_generation() {
1067        let network_config = VendorNetworkConfig {
1068            enabled: true,
1069            depth: 2,
1070            tier1_count: TierCountConfig::new(5, 10),
1071            tier2_per_parent: TierCountConfig::new(2, 4),
1072            tier3_per_parent: TierCountConfig::new(1, 2),
1073            ..Default::default()
1074        };
1075
1076        let mut gen = VendorGenerator::with_network_config(
1077            42,
1078            VendorGeneratorConfig::default(),
1079            network_config,
1080        );
1081
1082        let network = gen.generate_vendor_network(
1083            "1000",
1084            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1085            Decimal::from(10_000_000),
1086        );
1087
1088        assert!(!network.tier1_vendors.is_empty());
1089        assert!(!network.tier2_vendors.is_empty());
1090        assert!(network.tier1_vendors.len() >= 5);
1091        assert!(network.tier1_vendors.len() <= 10);
1092    }
1093
1094    #[test]
1095    fn test_vendor_network_relationships() {
1096        let network_config = VendorNetworkConfig {
1097            enabled: true,
1098            depth: 2,
1099            tier1_count: TierCountConfig::new(3, 3),
1100            tier2_per_parent: TierCountConfig::new(2, 2),
1101            ..Default::default()
1102        };
1103
1104        let mut gen = VendorGenerator::with_network_config(
1105            42,
1106            VendorGeneratorConfig::default(),
1107            network_config,
1108        );
1109
1110        let network = gen.generate_vendor_network(
1111            "1000",
1112            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1113            Decimal::from(5_000_000),
1114        );
1115
1116        // Check that Tier 2 vendors have parents
1117        for tier2_id in &network.tier2_vendors {
1118            let rel = network.get_relationship(tier2_id).unwrap();
1119            assert!(rel.parent_vendor.is_some());
1120            assert_eq!(rel.tier, SupplyChainTier::Tier2);
1121        }
1122
1123        // Check that Tier 1 vendors have children
1124        for tier1_id in &network.tier1_vendors {
1125            let rel = network.get_relationship(tier1_id).unwrap();
1126            assert!(!rel.child_vendors.is_empty());
1127            assert_eq!(rel.tier, SupplyChainTier::Tier1);
1128        }
1129    }
1130
1131    #[test]
1132    fn test_vendor_network_spend_distribution() {
1133        let network_config = VendorNetworkConfig {
1134            enabled: true,
1135            depth: 1,
1136            tier1_count: TierCountConfig::new(10, 10),
1137            ..Default::default()
1138        };
1139
1140        let mut gen = VendorGenerator::with_network_config(
1141            42,
1142            VendorGeneratorConfig::default(),
1143            network_config,
1144        );
1145
1146        let total_spend = Decimal::from(10_000_000);
1147        let network = gen.generate_vendor_network(
1148            "1000",
1149            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1150            total_spend,
1151        );
1152
1153        // Check that spend is distributed
1154        let total_assigned: Decimal = network.relationships.values().map(|r| r.annual_spend).sum();
1155
1156        assert!(total_assigned > Decimal::ZERO);
1157    }
1158
1159    #[test]
1160    fn test_vendor_network_cluster_distribution() {
1161        let network_config = VendorNetworkConfig {
1162            enabled: true,
1163            depth: 1,
1164            tier1_count: TierCountConfig::new(100, 100),
1165            cluster_distribution: ClusterDistribution::default(),
1166            ..Default::default()
1167        };
1168
1169        let mut gen = VendorGenerator::with_network_config(
1170            42,
1171            VendorGeneratorConfig::default(),
1172            network_config,
1173        );
1174
1175        let network = gen.generate_vendor_network(
1176            "1000",
1177            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1178            Decimal::from(10_000_000),
1179        );
1180
1181        // Count clusters
1182        let mut cluster_counts: std::collections::HashMap<VendorCluster, usize> =
1183            std::collections::HashMap::new();
1184        for rel in network.relationships.values() {
1185            *cluster_counts.entry(rel.cluster).or_insert(0) += 1;
1186        }
1187
1188        // ReliableStrategic should be roughly 20%
1189        let reliable = cluster_counts
1190            .get(&VendorCluster::ReliableStrategic)
1191            .unwrap_or(&0);
1192        assert!(*reliable >= 10 && *reliable <= 35);
1193    }
1194
1195    #[test]
1196    fn test_cluster_distribution_validation() {
1197        let valid = ClusterDistribution::default();
1198        assert!(valid.validate().is_ok());
1199
1200        let invalid = ClusterDistribution {
1201            reliable_strategic: 0.5,
1202            standard_operational: 0.5,
1203            transactional: 0.5,
1204            problematic: 0.5,
1205        };
1206        assert!(invalid.validate().is_err());
1207    }
1208
1209    #[test]
1210    fn test_vendor_network_disabled() {
1211        let network_config = VendorNetworkConfig {
1212            enabled: false,
1213            ..Default::default()
1214        };
1215
1216        let mut gen = VendorGenerator::with_network_config(
1217            42,
1218            VendorGeneratorConfig::default(),
1219            network_config,
1220        );
1221
1222        let network = gen.generate_vendor_network(
1223            "1000",
1224            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1225            Decimal::from(10_000_000),
1226        );
1227
1228        assert!(network.tier1_vendors.is_empty());
1229        assert!(network.relationships.is_empty());
1230    }
1231
1232    #[test]
1233    fn test_vendor_auxiliary_gl_account_french() {
1234        let mut gen = VendorGenerator::new(42);
1235        gen.set_coa_framework(CoAFramework::FrenchPcg);
1236        let vendor = gen.generate_vendor("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1237
1238        assert!(vendor.auxiliary_gl_account.is_some());
1239        let aux = vendor.auxiliary_gl_account.unwrap();
1240        assert!(
1241            aux.starts_with("401"),
1242            "French PCG vendor auxiliary should start with 401, got {}",
1243            aux
1244        );
1245    }
1246
1247    #[test]
1248    fn test_vendor_auxiliary_gl_account_german() {
1249        let mut gen = VendorGenerator::new(42);
1250        gen.set_coa_framework(CoAFramework::GermanSkr04);
1251        let vendor = gen.generate_vendor("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1252
1253        assert!(vendor.auxiliary_gl_account.is_some());
1254        let aux = vendor.auxiliary_gl_account.unwrap();
1255        assert!(
1256            aux.starts_with("3300"),
1257            "German SKR04 vendor auxiliary should start with 3300, got {}",
1258            aux
1259        );
1260    }
1261
1262    #[test]
1263    fn test_vendor_auxiliary_gl_account_us_gaap() {
1264        let mut gen = VendorGenerator::new(42);
1265        // US GAAP is default, should have no auxiliary GL account
1266        let vendor = gen.generate_vendor("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1267        assert!(vendor.auxiliary_gl_account.is_none());
1268    }
1269
1270    #[test]
1271    fn test_vendor_name_dedup() {
1272        let mut gen = VendorGenerator::new(42);
1273        let mut names = HashSet::new();
1274
1275        for _ in 0..200 {
1276            let vendor = gen.generate_vendor("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1277            assert!(
1278                names.insert(vendor.name.clone()),
1279                "Duplicate vendor name found: {}",
1280                vendor.name
1281            );
1282        }
1283    }
1284}