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