Skip to main content

datasynth_generators/master_data/
vendor_generator.rs

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