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