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 template_provider: Option<datasynth_core::templates::SharedTemplateProvider>,
275}
276
277impl VendorGenerator {
278 pub fn new(seed: u64) -> Self {
280 Self::with_config(seed, VendorGeneratorConfig::default())
281 }
282
283 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 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 pub fn set_coa_framework(&mut self, framework: CoAFramework) {
323 self.coa_framework = framework;
324 }
325
326 pub fn set_network_config(&mut self, network_config: VendorNetworkConfig) {
328 self.network_config = network_config;
329 }
330
331 pub fn set_country_pack(&mut self, pack: datasynth_core::CountryPack) {
333 self.country_pack = Some(pack);
334 }
335
336 pub fn set_template_provider(
342 &mut self,
343 provider: datasynth_core::templates::SharedTemplateProvider,
344 ) {
345 self.template_provider = Some(provider);
346 }
347
348 pub fn set_counter_offset(&mut self, offset: usize) {
355 self.vendor_counter = offset;
356 }
357
358 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 vendor.payment_terms = self.select_payment_terms();
379
380 vendor.behavior = self.select_vendor_behavior();
382
383 vendor.auxiliary_gl_account = self.generate_auxiliary_gl_account();
385
386 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 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 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; vendor
419 }
420
421 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 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 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 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 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 fn select_vendor_name(&mut self) -> (SpendCategory, String) {
482 let category = self.select_spend_category();
483
484 if self.config.use_enhanced_naming {
485 let name = self.vendor_name_gen.generate(category, &mut self.rng);
487 (category, name)
488 } else {
489 let name = format!("{:?} Vendor {}", category, self.vendor_counter);
491 (category, name)
492 }
493 }
494
495 fn select_vendor_name_unique(&mut self) -> (SpendCategory, String) {
497 let (category, mut name) = self.select_vendor_name();
498
499 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 name = format!("{} #{}", name, self.vendor_counter);
521 }
522 }
523
524 self.used_names.insert(name.clone());
525 (category, name)
526 }
527
528 fn generate_auxiliary_gl_account(&self) -> Option<String> {
530 match self.coa_framework {
531 CoAFramework::FrenchPcg => {
532 Some(format!("401{:04}", self.vendor_counter))
534 }
535 CoAFramework::GermanSkr04 => {
536 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 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 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 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 fn generate_bank_account(&mut self, vendor_id: &str) -> BankAccount {
588 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 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 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 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 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 if let Some(rel) = network.get_relationship_mut(tier1_id) {
669 rel.child_vendors = tier2_ids.clone();
670 }
671
672 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 if let Some(rel) = network.get_relationship_mut(tier2_id) {
688 rel.child_vendors = tier3_ids;
689 }
690 }
691 }
692 }
693 }
694
695 self.assign_annual_spend(&mut network, total_annual_spend);
697
698 network.calculate_statistics(effective_date);
700
701 network
702 }
703
704 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 let vendor = self.generate_vendor(company_code, effective_date);
719 let vendor_id = vendor.vendor_id.clone();
720
721 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 if let Some(ref parent) = parent_id {
731 relationship = relationship.with_parent(parent.clone());
732 }
733
734 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 relationship.lifecycle_stage = self.generate_lifecycle_stage(effective_date);
742
743 relationship.quality_score = self.generate_quality_score(&relationship.cluster);
745
746 relationship.payment_history = self.generate_payment_history(&relationship.cluster);
748
749 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 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 fn select_cluster(&mut self) -> VendorCluster {
783 let roll: f64 = self.rng.random();
784 self.network_config.cluster_distribution.select(roll)
785 }
786
787 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 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 fn generate_relationship_start_date(&mut self, effective_date: NaiveDate) -> NaiveDate {
818 let days_back: i64 = self.rng.random_range(90..3650); effective_date - chrono::Duration::days(days_back)
820 }
821
822 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 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 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 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 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 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 fn infer_spend_category(&self, _vendor_name: &str) -> String {
930 "General".to_string()
931 }
932
933 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 let mut weights: Vec<f64> = (0..tier1_count)
942 .map(|_| {
943 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 weights.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
956
957 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 if let Some(dep) = &mut rel.dependency {
966 dep.concentration_percent = weight;
967 }
968 }
969 }
970
971 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 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 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 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 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 assert!(!vendor.name.is_empty());
1099 assert!(!vendor.name.starts_with("Vendor "));
1101 }
1102
1103 #[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 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 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 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 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 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 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}