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)]
1022mod tests {
1023 use super::*;
1024
1025 #[test]
1026 fn test_vendor_generation() {
1027 let mut gen = VendorGenerator::new(42);
1028 let vendor = gen.generate_vendor("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1029
1030 assert!(!vendor.vendor_id.is_empty());
1031 assert!(!vendor.name.is_empty());
1032 assert!(vendor.tax_id.is_some());
1033 assert!(!vendor.bank_accounts.is_empty());
1034 }
1035
1036 #[test]
1037 fn test_vendor_pool_generation() {
1038 let mut gen = VendorGenerator::new(42);
1039 let pool =
1040 gen.generate_vendor_pool(10, "1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1041
1042 assert_eq!(pool.vendors.len(), 10);
1043 }
1044
1045 #[test]
1046 fn test_intercompany_vendor() {
1047 let mut gen = VendorGenerator::new(42);
1048 let vendor = gen.generate_intercompany_vendor(
1049 "1000",
1050 "2000",
1051 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1052 );
1053
1054 assert!(vendor.is_intercompany);
1055 assert_eq!(vendor.intercompany_code, Some("2000".to_string()));
1056 }
1057
1058 #[test]
1059 fn test_deterministic_generation() {
1060 let mut gen1 = VendorGenerator::new(42);
1061 let mut gen2 = VendorGenerator::new(42);
1062
1063 let vendor1 = gen1.generate_vendor("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1064 let vendor2 = gen2.generate_vendor("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1065
1066 assert_eq!(vendor1.vendor_id, vendor2.vendor_id);
1067 assert_eq!(vendor1.name, vendor2.name);
1068 }
1069
1070 #[test]
1071 fn test_vendor_pool_with_ic() {
1072 let config = VendorGeneratorConfig {
1074 intercompany_rate: 0.0,
1075 ..Default::default()
1076 };
1077 let mut gen = VendorGenerator::with_config(42, config);
1078 let pool = gen.generate_vendor_pool_with_ic(
1079 10,
1080 "1000",
1081 &["2000".to_string(), "3000".to_string()],
1082 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1083 );
1084
1085 assert_eq!(pool.vendors.len(), 10);
1086
1087 let ic_vendors: Vec<_> = pool.vendors.iter().filter(|v| v.is_intercompany).collect();
1088 assert_eq!(ic_vendors.len(), 2);
1089 }
1090
1091 #[test]
1092 fn test_enhanced_vendor_names() {
1093 let mut gen = VendorGenerator::new(42);
1094 let vendor = gen.generate_vendor("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1095
1096 assert!(!vendor.name.is_empty());
1098 assert!(!vendor.name.starts_with("Vendor "));
1100 }
1101
1102 #[test]
1105 fn test_vendor_network_generation() {
1106 let network_config = VendorNetworkConfig {
1107 enabled: true,
1108 depth: 2,
1109 tier1_count: TierCountConfig::new(5, 10),
1110 tier2_per_parent: TierCountConfig::new(2, 4),
1111 tier3_per_parent: TierCountConfig::new(1, 2),
1112 ..Default::default()
1113 };
1114
1115 let mut gen = VendorGenerator::with_network_config(
1116 42,
1117 VendorGeneratorConfig::default(),
1118 network_config,
1119 );
1120
1121 let network = gen.generate_vendor_network(
1122 "1000",
1123 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1124 Decimal::from(10_000_000),
1125 );
1126
1127 assert!(!network.tier1_vendors.is_empty());
1128 assert!(!network.tier2_vendors.is_empty());
1129 assert!(network.tier1_vendors.len() >= 5);
1130 assert!(network.tier1_vendors.len() <= 10);
1131 }
1132
1133 #[test]
1134 fn test_vendor_network_relationships() {
1135 let network_config = VendorNetworkConfig {
1136 enabled: true,
1137 depth: 2,
1138 tier1_count: TierCountConfig::new(3, 3),
1139 tier2_per_parent: TierCountConfig::new(2, 2),
1140 ..Default::default()
1141 };
1142
1143 let mut gen = VendorGenerator::with_network_config(
1144 42,
1145 VendorGeneratorConfig::default(),
1146 network_config,
1147 );
1148
1149 let network = gen.generate_vendor_network(
1150 "1000",
1151 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1152 Decimal::from(5_000_000),
1153 );
1154
1155 for tier2_id in &network.tier2_vendors {
1157 let rel = network.get_relationship(tier2_id).unwrap();
1158 assert!(rel.parent_vendor.is_some());
1159 assert_eq!(rel.tier, SupplyChainTier::Tier2);
1160 }
1161
1162 for tier1_id in &network.tier1_vendors {
1164 let rel = network.get_relationship(tier1_id).unwrap();
1165 assert!(!rel.child_vendors.is_empty());
1166 assert_eq!(rel.tier, SupplyChainTier::Tier1);
1167 }
1168 }
1169
1170 #[test]
1171 fn test_vendor_network_spend_distribution() {
1172 let network_config = VendorNetworkConfig {
1173 enabled: true,
1174 depth: 1,
1175 tier1_count: TierCountConfig::new(10, 10),
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 total_spend = Decimal::from(10_000_000);
1186 let network = gen.generate_vendor_network(
1187 "1000",
1188 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1189 total_spend,
1190 );
1191
1192 let total_assigned: Decimal = network.relationships.values().map(|r| r.annual_spend).sum();
1194
1195 assert!(total_assigned > Decimal::ZERO);
1196 }
1197
1198 #[test]
1199 fn test_vendor_network_cluster_distribution() {
1200 let network_config = VendorNetworkConfig {
1201 enabled: true,
1202 depth: 1,
1203 tier1_count: TierCountConfig::new(100, 100),
1204 cluster_distribution: ClusterDistribution::default(),
1205 ..Default::default()
1206 };
1207
1208 let mut gen = VendorGenerator::with_network_config(
1209 42,
1210 VendorGeneratorConfig::default(),
1211 network_config,
1212 );
1213
1214 let network = gen.generate_vendor_network(
1215 "1000",
1216 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1217 Decimal::from(10_000_000),
1218 );
1219
1220 let mut cluster_counts: std::collections::HashMap<VendorCluster, usize> =
1222 std::collections::HashMap::new();
1223 for rel in network.relationships.values() {
1224 *cluster_counts.entry(rel.cluster).or_insert(0) += 1;
1225 }
1226
1227 let reliable = cluster_counts
1229 .get(&VendorCluster::ReliableStrategic)
1230 .unwrap_or(&0);
1231 assert!(*reliable >= 10 && *reliable <= 35);
1232 }
1233
1234 #[test]
1235 fn test_cluster_distribution_validation() {
1236 let valid = ClusterDistribution::default();
1237 assert!(valid.validate().is_ok());
1238
1239 let invalid = ClusterDistribution {
1240 reliable_strategic: 0.5,
1241 standard_operational: 0.5,
1242 transactional: 0.5,
1243 problematic: 0.5,
1244 };
1245 assert!(invalid.validate().is_err());
1246 }
1247
1248 #[test]
1249 fn test_vendor_network_disabled() {
1250 let network_config = VendorNetworkConfig {
1251 enabled: false,
1252 ..Default::default()
1253 };
1254
1255 let mut gen = VendorGenerator::with_network_config(
1256 42,
1257 VendorGeneratorConfig::default(),
1258 network_config,
1259 );
1260
1261 let network = gen.generate_vendor_network(
1262 "1000",
1263 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1264 Decimal::from(10_000_000),
1265 );
1266
1267 assert!(network.tier1_vendors.is_empty());
1268 assert!(network.relationships.is_empty());
1269 }
1270
1271 #[test]
1272 fn test_vendor_auxiliary_gl_account_french() {
1273 let mut gen = VendorGenerator::new(42);
1274 gen.set_coa_framework(CoAFramework::FrenchPcg);
1275 let vendor = gen.generate_vendor("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1276
1277 assert!(vendor.auxiliary_gl_account.is_some());
1278 let aux = vendor.auxiliary_gl_account.unwrap();
1279 assert!(
1280 aux.starts_with("401"),
1281 "French PCG vendor auxiliary should start with 401, got {}",
1282 aux
1283 );
1284 }
1285
1286 #[test]
1287 fn test_vendor_auxiliary_gl_account_german() {
1288 let mut gen = VendorGenerator::new(42);
1289 gen.set_coa_framework(CoAFramework::GermanSkr04);
1290 let vendor = gen.generate_vendor("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1291
1292 assert!(vendor.auxiliary_gl_account.is_some());
1293 let aux = vendor.auxiliary_gl_account.unwrap();
1294 assert!(
1295 aux.starts_with("3300"),
1296 "German SKR04 vendor auxiliary should start with 3300, got {}",
1297 aux
1298 );
1299 }
1300
1301 #[test]
1302 fn test_vendor_auxiliary_gl_account_us_gaap() {
1303 let mut gen = VendorGenerator::new(42);
1304 let vendor = gen.generate_vendor("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1306 assert!(vendor.auxiliary_gl_account.is_none());
1307 }
1308
1309 #[test]
1310 fn test_vendor_name_dedup() {
1311 let mut gen = VendorGenerator::new(42);
1312 let mut names = HashSet::new();
1313
1314 for _ in 0..200 {
1315 let vendor = gen.generate_vendor("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1316 assert!(
1317 names.insert(vendor.name.clone()),
1318 "Duplicate vendor name found: {}",
1319 vendor.name
1320 );
1321 }
1322 }
1323}