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 set_counter_offset(&mut self, offset: usize) {
339 self.vendor_counter = offset;
340 }
341
342 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 vendor.payment_terms = self.select_payment_terms();
363
364 vendor.behavior = self.select_vendor_behavior();
366
367 vendor.auxiliary_gl_account = self.generate_auxiliary_gl_account();
369
370 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 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 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; vendor
403 }
404
405 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 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 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 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 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 fn select_vendor_name(&mut self) -> (SpendCategory, String) {
466 let category = self.select_spend_category();
467
468 if self.config.use_enhanced_naming {
469 let name = self.vendor_name_gen.generate(category, &mut self.rng);
471 (category, name)
472 } else {
473 let name = format!("{:?} Vendor {}", category, self.vendor_counter);
475 (category, name)
476 }
477 }
478
479 fn select_vendor_name_unique(&mut self) -> (SpendCategory, String) {
481 let (category, mut name) = self.select_vendor_name();
482
483 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 name = format!("{} #{}", name, self.vendor_counter);
505 }
506 }
507
508 self.used_names.insert(name.clone());
509 (category, name)
510 }
511
512 fn generate_auxiliary_gl_account(&self) -> Option<String> {
514 match self.coa_framework {
515 CoAFramework::FrenchPcg => {
516 Some(format!("401{:04}", self.vendor_counter))
518 }
519 CoAFramework::GermanSkr04 => {
520 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 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 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 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 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 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 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 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 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 if let Some(rel) = network.get_relationship_mut(tier1_id) {
639 rel.child_vendors = tier2_ids.clone();
640 }
641
642 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 if let Some(rel) = network.get_relationship_mut(tier2_id) {
658 rel.child_vendors = tier3_ids;
659 }
660 }
661 }
662 }
663 }
664
665 self.assign_annual_spend(&mut network, total_annual_spend);
667
668 network.calculate_statistics(effective_date);
670
671 network
672 }
673
674 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 let vendor = self.generate_vendor(company_code, effective_date);
689 let vendor_id = vendor.vendor_id.clone();
690
691 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 if let Some(ref parent) = parent_id {
701 relationship = relationship.with_parent(parent.clone());
702 }
703
704 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 relationship.lifecycle_stage = self.generate_lifecycle_stage(effective_date);
712
713 relationship.quality_score = self.generate_quality_score(&relationship.cluster);
715
716 relationship.payment_history = self.generate_payment_history(&relationship.cluster);
718
719 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 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 fn select_cluster(&mut self) -> VendorCluster {
753 let roll: f64 = self.rng.random();
754 self.network_config.cluster_distribution.select(roll)
755 }
756
757 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 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 fn generate_relationship_start_date(&mut self, effective_date: NaiveDate) -> NaiveDate {
788 let days_back: i64 = self.rng.random_range(90..3650); effective_date - chrono::Duration::days(days_back)
790 }
791
792 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 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 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 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 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 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 fn infer_spend_category(&self, _vendor_name: &str) -> String {
900 "General".to_string()
901 }
902
903 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 let mut weights: Vec<f64> = (0..tier1_count)
912 .map(|_| {
913 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 weights.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
926
927 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 if let Some(dep) = &mut rel.dependency {
936 dep.concentration_percent = weight;
937 }
938 }
939 }
940
941 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 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 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 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 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 assert!(!vendor.name.is_empty());
1069 assert!(!vendor.name.starts_with("Vendor "));
1071 }
1072
1073 #[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 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 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 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 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 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 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}