1use chrono::NaiveDate;
13use datasynth_core::models::{
14 BankAccount, DeclineReason, PaymentHistory, PaymentTerms, SpendTier, StrategicLevel,
15 Substitutability, SupplyChainTier, Vendor, VendorBehavior, VendorCluster, VendorDependency,
16 VendorLifecycleStage, VendorNetwork, VendorPool, VendorQualityScore, VendorRelationship,
17 VendorRelationshipType,
18};
19use datasynth_core::templates::{
20 AddressGenerator, AddressRegion, SpendCategory, VendorNameGenerator,
21};
22use rand::prelude::*;
23use rand_chacha::ChaCha8Rng;
24use rust_decimal::Decimal;
25
26#[derive(Debug, Clone)]
28pub struct VendorGeneratorConfig {
29 pub payment_terms_distribution: Vec<(PaymentTerms, f64)>,
31 pub behavior_distribution: Vec<(VendorBehavior, f64)>,
33 pub intercompany_rate: f64,
35 pub default_country: String,
37 pub default_currency: String,
39 pub generate_bank_accounts: bool,
41 pub multiple_bank_account_rate: f64,
43 pub spend_category_distribution: Vec<(SpendCategory, f64)>,
45 pub primary_region: AddressRegion,
47 pub use_enhanced_naming: bool,
49}
50
51impl Default for VendorGeneratorConfig {
52 fn default() -> Self {
53 Self {
54 payment_terms_distribution: vec![
55 (PaymentTerms::Net30, 0.40),
56 (PaymentTerms::Net60, 0.20),
57 (PaymentTerms::TwoTenNet30, 0.25),
58 (PaymentTerms::Net15, 0.10),
59 (PaymentTerms::Immediate, 0.05),
60 ],
61 behavior_distribution: vec![
62 (VendorBehavior::Flexible, 0.60),
63 (VendorBehavior::Strict, 0.25),
64 (VendorBehavior::VeryFlexible, 0.10),
65 (VendorBehavior::Aggressive, 0.05),
66 ],
67 intercompany_rate: 0.05,
68 default_country: "US".to_string(),
69 default_currency: "USD".to_string(),
70 generate_bank_accounts: true,
71 multiple_bank_account_rate: 0.20,
72 spend_category_distribution: vec![
73 (SpendCategory::OfficeSupplies, 0.15),
74 (SpendCategory::ITServices, 0.12),
75 (SpendCategory::ProfessionalServices, 0.12),
76 (SpendCategory::Telecommunications, 0.08),
77 (SpendCategory::Utilities, 0.08),
78 (SpendCategory::RawMaterials, 0.10),
79 (SpendCategory::Logistics, 0.10),
80 (SpendCategory::Marketing, 0.08),
81 (SpendCategory::Facilities, 0.07),
82 (SpendCategory::Staffing, 0.05),
83 (SpendCategory::Travel, 0.05),
84 ],
85 primary_region: AddressRegion::NorthAmerica,
86 use_enhanced_naming: true,
87 }
88 }
89}
90
91#[derive(Debug, Clone)]
93pub struct VendorNetworkConfig {
94 pub enabled: bool,
96 pub depth: u8,
98 pub tier1_count: TierCountConfig,
100 pub tier2_per_parent: TierCountConfig,
102 pub tier3_per_parent: TierCountConfig,
104 pub cluster_distribution: ClusterDistribution,
106 pub concentration_limits: ConcentrationLimits,
108 pub strategic_distribution: Vec<(StrategicLevel, f64)>,
110 pub single_source_percent: f64,
112}
113
114#[derive(Debug, Clone)]
116pub struct TierCountConfig {
117 pub min: usize,
119 pub max: usize,
121}
122
123impl TierCountConfig {
124 pub fn new(min: usize, max: usize) -> Self {
126 Self { min, max }
127 }
128
129 pub fn sample(&self, rng: &mut impl Rng) -> usize {
131 rng.gen_range(self.min..=self.max)
132 }
133}
134
135#[derive(Debug, Clone)]
137pub struct ClusterDistribution {
138 pub reliable_strategic: f64,
140 pub standard_operational: f64,
142 pub transactional: f64,
144 pub problematic: f64,
146}
147
148impl Default for ClusterDistribution {
149 fn default() -> Self {
150 Self {
151 reliable_strategic: 0.20,
152 standard_operational: 0.50,
153 transactional: 0.25,
154 problematic: 0.05,
155 }
156 }
157}
158
159impl ClusterDistribution {
160 pub fn validate(&self) -> Result<(), String> {
162 let sum = self.reliable_strategic
163 + self.standard_operational
164 + self.transactional
165 + self.problematic;
166 if (sum - 1.0).abs() > 0.01 {
167 Err(format!("Cluster distribution must sum to 1.0, got {}", sum))
168 } else {
169 Ok(())
170 }
171 }
172
173 pub fn select(&self, roll: f64) -> VendorCluster {
175 let mut cumulative = 0.0;
176
177 cumulative += self.reliable_strategic;
178 if roll < cumulative {
179 return VendorCluster::ReliableStrategic;
180 }
181
182 cumulative += self.standard_operational;
183 if roll < cumulative {
184 return VendorCluster::StandardOperational;
185 }
186
187 cumulative += self.transactional;
188 if roll < cumulative {
189 return VendorCluster::Transactional;
190 }
191
192 VendorCluster::Problematic
193 }
194}
195
196#[derive(Debug, Clone)]
198pub struct ConcentrationLimits {
199 pub max_single_vendor: f64,
201 pub max_top5: f64,
203}
204
205impl Default for ConcentrationLimits {
206 fn default() -> Self {
207 Self {
208 max_single_vendor: 0.15,
209 max_top5: 0.45,
210 }
211 }
212}
213
214impl Default for VendorNetworkConfig {
215 fn default() -> Self {
216 Self {
217 enabled: false,
218 depth: 3,
219 tier1_count: TierCountConfig::new(50, 100),
220 tier2_per_parent: TierCountConfig::new(4, 10),
221 tier3_per_parent: TierCountConfig::new(2, 5),
222 cluster_distribution: ClusterDistribution::default(),
223 concentration_limits: ConcentrationLimits::default(),
224 strategic_distribution: vec![
225 (StrategicLevel::Critical, 0.05),
226 (StrategicLevel::Important, 0.15),
227 (StrategicLevel::Standard, 0.50),
228 (StrategicLevel::Transactional, 0.30),
229 ],
230 single_source_percent: 0.05,
231 }
232 }
233}
234
235#[allow(dead_code)]
238const VENDOR_NAME_TEMPLATES_LEGACY: &[(&str, &[&str])] = &[
239 (
240 "Manufacturing",
241 &[
242 "Global Manufacturing Solutions",
243 "Precision Parts Inc.",
244 "Industrial Components Ltd.",
245 "Advanced Materials Corp.",
246 ],
247 ),
248 (
249 "Services",
250 &[
251 "Professional Services Group",
252 "Consulting Partners LLC",
253 "Business Solutions Inc.",
254 "Technical Services Corp.",
255 ],
256 ),
257 (
258 "Technology",
259 &[
260 "Tech Solutions Inc.",
261 "Digital Systems Corp.",
262 "Software Innovations LLC",
263 "Cloud Services Partners",
264 ],
265 ),
266];
267
268const BANK_NAMES: &[&str] = &[
270 "First National Bank",
271 "Commerce Bank",
272 "United Banking Corp",
273 "Regional Trust Bank",
274 "Merchants Bank",
275 "Citizens Financial",
276 "Pacific Coast Bank",
277 "Atlantic Commerce Bank",
278 "Midwest Trust Company",
279 "Capital One Commercial",
280];
281
282pub struct VendorGenerator {
284 rng: ChaCha8Rng,
285 seed: u64,
286 config: VendorGeneratorConfig,
287 vendor_counter: usize,
288 vendor_name_gen: VendorNameGenerator,
290 address_gen: AddressGenerator,
292 network_config: VendorNetworkConfig,
294}
295
296impl VendorGenerator {
297 pub fn new(seed: u64) -> Self {
299 Self::with_config(seed, VendorGeneratorConfig::default())
300 }
301
302 pub fn with_config(seed: u64, config: VendorGeneratorConfig) -> Self {
304 Self {
305 rng: ChaCha8Rng::seed_from_u64(seed),
306 seed,
307 vendor_name_gen: VendorNameGenerator::new(),
308 address_gen: AddressGenerator::for_region(config.primary_region),
309 config,
310 vendor_counter: 0,
311 network_config: VendorNetworkConfig::default(),
312 }
313 }
314
315 pub fn with_network_config(
317 seed: u64,
318 config: VendorGeneratorConfig,
319 network_config: VendorNetworkConfig,
320 ) -> Self {
321 Self {
322 rng: ChaCha8Rng::seed_from_u64(seed),
323 seed,
324 vendor_name_gen: VendorNameGenerator::new(),
325 address_gen: AddressGenerator::for_region(config.primary_region),
326 config,
327 vendor_counter: 0,
328 network_config,
329 }
330 }
331
332 pub fn set_network_config(&mut self, network_config: VendorNetworkConfig) {
334 self.network_config = network_config;
335 }
336
337 pub fn generate_vendor(&mut self, company_code: &str, _effective_date: NaiveDate) -> Vendor {
339 self.vendor_counter += 1;
340
341 let vendor_id = format!("V-{:06}", self.vendor_counter);
342 let (category, name) = self.select_vendor_name();
343 let tax_id = self.generate_tax_id();
344 let _address = self.address_gen.generate_commercial(&mut self.rng);
345
346 let _spend_category = category;
348
349 let mut vendor = Vendor::new(
350 &vendor_id,
351 &name,
352 datasynth_core::models::VendorType::Supplier,
353 );
354 vendor.tax_id = Some(tax_id);
355 vendor.country = self.config.default_country.clone();
356 vendor.currency = self.config.default_currency.clone();
357 vendor.payment_terms = self.select_payment_terms();
361
362 vendor.behavior = self.select_vendor_behavior();
364
365 if self.rng.gen::<f64>() < self.config.intercompany_rate {
367 vendor.is_intercompany = true;
368 vendor.intercompany_code = Some(format!("IC-{}", company_code));
369 }
370
371 if self.config.generate_bank_accounts {
373 let bank_account = self.generate_bank_account(&vendor.vendor_id);
374 vendor.bank_accounts.push(bank_account);
375
376 if self.rng.gen::<f64>() < self.config.multiple_bank_account_rate {
377 let bank_account2 = self.generate_bank_account(&vendor.vendor_id);
378 vendor.bank_accounts.push(bank_account2);
379 }
380 }
381
382 vendor
383 }
384
385 pub fn generate_intercompany_vendor(
387 &mut self,
388 company_code: &str,
389 partner_company_code: &str,
390 effective_date: NaiveDate,
391 ) -> Vendor {
392 let mut vendor = self.generate_vendor(company_code, effective_date);
393 vendor.is_intercompany = true;
394 vendor.intercompany_code = Some(partner_company_code.to_string());
395 vendor.name = format!("{} - IC", partner_company_code);
396 vendor.payment_terms = PaymentTerms::Immediate; vendor
398 }
399
400 pub fn generate_vendor_pool(
402 &mut self,
403 count: usize,
404 company_code: &str,
405 effective_date: NaiveDate,
406 ) -> VendorPool {
407 let mut pool = VendorPool::new();
408
409 for _ in 0..count {
410 let vendor = self.generate_vendor(company_code, effective_date);
411 pool.add_vendor(vendor);
412 }
413
414 pool
415 }
416
417 pub fn generate_vendor_pool_with_ic(
419 &mut self,
420 count: usize,
421 company_code: &str,
422 partner_company_codes: &[String],
423 effective_date: NaiveDate,
424 ) -> VendorPool {
425 let mut pool = VendorPool::new();
426
427 let regular_count = count.saturating_sub(partner_company_codes.len());
429 for _ in 0..regular_count {
430 let vendor = self.generate_vendor(company_code, effective_date);
431 pool.add_vendor(vendor);
432 }
433
434 for partner in partner_company_codes {
436 let vendor = self.generate_intercompany_vendor(company_code, partner, effective_date);
437 pool.add_vendor(vendor);
438 }
439
440 pool
441 }
442
443 fn select_spend_category(&mut self) -> SpendCategory {
445 let roll: f64 = self.rng.gen();
446 let mut cumulative = 0.0;
447
448 for (category, prob) in &self.config.spend_category_distribution {
449 cumulative += prob;
450 if roll < cumulative {
451 return *category;
452 }
453 }
454
455 SpendCategory::OfficeSupplies
456 }
457
458 fn select_vendor_name(&mut self) -> (SpendCategory, String) {
460 let category = self.select_spend_category();
461
462 if self.config.use_enhanced_naming {
463 let name = self.vendor_name_gen.generate(category, &mut self.rng);
465 (category, name)
466 } else {
467 let name = format!("{:?} Vendor {}", category, self.vendor_counter);
469 (category, name)
470 }
471 }
472
473 fn select_payment_terms(&mut self) -> PaymentTerms {
475 let roll: f64 = self.rng.gen();
476 let mut cumulative = 0.0;
477
478 for (terms, prob) in &self.config.payment_terms_distribution {
479 cumulative += prob;
480 if roll < cumulative {
481 return *terms;
482 }
483 }
484
485 PaymentTerms::Net30
486 }
487
488 fn select_vendor_behavior(&mut self) -> VendorBehavior {
490 let roll: f64 = self.rng.gen();
491 let mut cumulative = 0.0;
492
493 for (behavior, prob) in &self.config.behavior_distribution {
494 cumulative += prob;
495 if roll < cumulative {
496 return *behavior;
497 }
498 }
499
500 VendorBehavior::Flexible
501 }
502
503 fn generate_tax_id(&mut self) -> String {
505 format!(
506 "{:02}-{:07}",
507 self.rng.gen_range(10..99),
508 self.rng.gen_range(1000000..9999999)
509 )
510 }
511
512 fn generate_bank_account(&mut self, vendor_id: &str) -> BankAccount {
514 let bank_idx = self.rng.gen_range(0..BANK_NAMES.len());
515 let bank_name = BANK_NAMES[bank_idx];
516
517 let routing = format!("{:09}", self.rng.gen_range(100000000u64..999999999));
518 let account = format!("{:010}", self.rng.gen_range(1000000000u64..9999999999));
519
520 BankAccount {
521 bank_name: bank_name.to_string(),
522 bank_country: "US".to_string(),
523 account_number: account,
524 routing_code: routing,
525 holder_name: format!("Vendor {}", vendor_id),
526 is_primary: self.vendor_counter == 1,
527 }
528 }
529
530 #[allow(dead_code)]
532 fn generate_address(&mut self) -> String {
533 use datasynth_core::templates::AddressStyle;
534 let address = self.address_gen.generate_commercial(&mut self.rng);
535 address.format(AddressStyle::SingleLine)
536 }
537
538 pub fn reset(&mut self) {
540 self.rng = ChaCha8Rng::seed_from_u64(self.seed);
541 self.vendor_counter = 0;
542 self.vendor_name_gen = VendorNameGenerator::new();
543 self.address_gen = AddressGenerator::for_region(self.config.primary_region);
544 }
545
546 pub fn generate_vendor_network(
550 &mut self,
551 company_code: &str,
552 effective_date: NaiveDate,
553 total_annual_spend: Decimal,
554 ) -> VendorNetwork {
555 let mut network = VendorNetwork::new(company_code);
556 network.created_date = Some(effective_date);
557
558 if !self.network_config.enabled {
559 return network;
560 }
561
562 let tier1_count = self.network_config.tier1_count.sample(&mut self.rng);
564 let tier1_ids = self.generate_tier_vendors(
565 company_code,
566 effective_date,
567 tier1_count,
568 SupplyChainTier::Tier1,
569 None,
570 &mut network,
571 );
572
573 if self.network_config.depth >= 2 {
575 for tier1_id in &tier1_ids {
576 let tier2_count = self.network_config.tier2_per_parent.sample(&mut self.rng);
577 let tier2_ids = self.generate_tier_vendors(
578 company_code,
579 effective_date,
580 tier2_count,
581 SupplyChainTier::Tier2,
582 Some(tier1_id.clone()),
583 &mut network,
584 );
585
586 if let Some(rel) = network.get_relationship_mut(tier1_id) {
588 rel.child_vendors = tier2_ids.clone();
589 }
590
591 if self.network_config.depth >= 3 {
593 for tier2_id in &tier2_ids {
594 let tier3_count =
595 self.network_config.tier3_per_parent.sample(&mut self.rng);
596 let tier3_ids = self.generate_tier_vendors(
597 company_code,
598 effective_date,
599 tier3_count,
600 SupplyChainTier::Tier3,
601 Some(tier2_id.clone()),
602 &mut network,
603 );
604
605 if let Some(rel) = network.get_relationship_mut(tier2_id) {
607 rel.child_vendors = tier3_ids;
608 }
609 }
610 }
611 }
612 }
613
614 self.assign_annual_spend(&mut network, total_annual_spend);
616
617 network.calculate_statistics(effective_date);
619
620 network
621 }
622
623 fn generate_tier_vendors(
625 &mut self,
626 company_code: &str,
627 effective_date: NaiveDate,
628 count: usize,
629 tier: SupplyChainTier,
630 parent_id: Option<String>,
631 network: &mut VendorNetwork,
632 ) -> Vec<String> {
633 let mut vendor_ids = Vec::with_capacity(count);
634
635 for _ in 0..count {
636 let vendor = self.generate_vendor(company_code, effective_date);
638 let vendor_id = vendor.vendor_id.clone();
639
640 let mut relationship = VendorRelationship::new(
642 vendor_id.clone(),
643 self.select_relationship_type(),
644 tier,
645 self.generate_relationship_start_date(effective_date),
646 );
647
648 if let Some(ref parent) = parent_id {
650 relationship = relationship.with_parent(parent.clone());
651 }
652
653 relationship = relationship
655 .with_cluster(self.select_cluster())
656 .with_strategic_importance(self.select_strategic_level())
657 .with_spend_tier(self.select_spend_tier());
658
659 relationship.lifecycle_stage = self.generate_lifecycle_stage(effective_date);
661
662 relationship.quality_score = self.generate_quality_score(&relationship.cluster);
664
665 relationship.payment_history = self.generate_payment_history(&relationship.cluster);
667
668 if tier == SupplyChainTier::Tier1 {
670 relationship.dependency = Some(self.generate_dependency(&vendor_id, &vendor.name));
671 }
672
673 network.add_relationship(relationship);
674 vendor_ids.push(vendor_id);
675 }
676
677 vendor_ids
678 }
679
680 fn select_relationship_type(&mut self) -> VendorRelationshipType {
682 let roll: f64 = self.rng.gen();
683 if roll < 0.40 {
684 VendorRelationshipType::DirectSupplier
685 } else if roll < 0.55 {
686 VendorRelationshipType::ServiceProvider
687 } else if roll < 0.70 {
688 VendorRelationshipType::RawMaterialSupplier
689 } else if roll < 0.80 {
690 VendorRelationshipType::Manufacturer
691 } else if roll < 0.88 {
692 VendorRelationshipType::Distributor
693 } else if roll < 0.94 {
694 VendorRelationshipType::Contractor
695 } else {
696 VendorRelationshipType::OemPartner
697 }
698 }
699
700 fn select_cluster(&mut self) -> VendorCluster {
702 let roll: f64 = self.rng.gen();
703 self.network_config.cluster_distribution.select(roll)
704 }
705
706 fn select_strategic_level(&mut self) -> StrategicLevel {
708 let roll: f64 = self.rng.gen();
709 let mut cumulative = 0.0;
710
711 for (level, prob) in &self.network_config.strategic_distribution {
712 cumulative += prob;
713 if roll < cumulative {
714 return *level;
715 }
716 }
717
718 StrategicLevel::Standard
719 }
720
721 fn select_spend_tier(&mut self) -> SpendTier {
723 let roll: f64 = self.rng.gen();
724 if roll < 0.05 {
725 SpendTier::Platinum
726 } else if roll < 0.20 {
727 SpendTier::Gold
728 } else if roll < 0.50 {
729 SpendTier::Silver
730 } else {
731 SpendTier::Bronze
732 }
733 }
734
735 fn generate_relationship_start_date(&mut self, effective_date: NaiveDate) -> NaiveDate {
737 let days_back: i64 = self.rng.gen_range(90..3650); effective_date - chrono::Duration::days(days_back)
739 }
740
741 fn generate_lifecycle_stage(&mut self, effective_date: NaiveDate) -> VendorLifecycleStage {
743 let roll: f64 = self.rng.gen();
744 if roll < 0.05 {
745 VendorLifecycleStage::Onboarding {
746 started: effective_date - chrono::Duration::days(self.rng.gen_range(1..60)),
747 expected_completion: effective_date
748 + chrono::Duration::days(self.rng.gen_range(30..90)),
749 }
750 } else if roll < 0.12 {
751 VendorLifecycleStage::RampUp {
752 started: effective_date - chrono::Duration::days(self.rng.gen_range(60..180)),
753 target_volume_percent: self.rng.gen_range(50..80) as u8,
754 }
755 } else if roll < 0.85 {
756 VendorLifecycleStage::SteadyState {
757 since: effective_date - chrono::Duration::days(self.rng.gen_range(180..1825)),
758 }
759 } else if roll < 0.95 {
760 VendorLifecycleStage::Decline {
761 started: effective_date - chrono::Duration::days(self.rng.gen_range(30..180)),
762 reason: DeclineReason::QualityIssues,
763 }
764 } else {
765 VendorLifecycleStage::SteadyState {
766 since: effective_date - chrono::Duration::days(self.rng.gen_range(365..1825)),
767 }
768 }
769 }
770
771 fn generate_quality_score(&mut self, cluster: &VendorCluster) -> VendorQualityScore {
773 let (base_delivery, base_quality, base_invoice, base_response) = match cluster {
774 VendorCluster::ReliableStrategic => (0.97, 0.96, 0.98, 0.95),
775 VendorCluster::StandardOperational => (0.92, 0.90, 0.93, 0.85),
776 VendorCluster::Transactional => (0.85, 0.82, 0.88, 0.75),
777 VendorCluster::Problematic => (0.70, 0.68, 0.75, 0.60),
778 };
779
780 let delivery_variance: f64 = self.rng.gen_range(-0.05..0.05);
782 let quality_variance: f64 = self.rng.gen_range(-0.05..0.05);
783 let invoice_variance: f64 = self.rng.gen_range(-0.05..0.05);
784 let response_variance: f64 = self.rng.gen_range(-0.05..0.05);
785
786 VendorQualityScore {
787 delivery_score: (base_delivery + delivery_variance).clamp(0.0_f64, 1.0_f64),
788 quality_score: (base_quality + quality_variance).clamp(0.0_f64, 1.0_f64),
789 invoice_accuracy_score: (base_invoice + invoice_variance).clamp(0.0_f64, 1.0_f64),
790 responsiveness_score: (base_response + response_variance).clamp(0.0_f64, 1.0_f64),
791 last_evaluation: NaiveDate::from_ymd_opt(2024, 1, 1).expect("valid default date"),
792 evaluation_count: self.rng.gen_range(1..20),
793 }
794 }
795
796 fn generate_payment_history(&mut self, cluster: &VendorCluster) -> PaymentHistory {
798 let total = self.rng.gen_range(10..200) as u32;
799 let on_time_rate = cluster.invoice_accuracy_probability();
800 let on_time = (total as f64 * on_time_rate) as u32;
801 let early = (total as f64 * self.rng.gen_range(0.05..0.20)) as u32;
802 let late = total.saturating_sub(on_time).saturating_sub(early);
803
804 PaymentHistory {
805 total_invoices: total,
806 on_time_payments: on_time,
807 early_payments: early,
808 late_payments: late,
809 total_amount: Decimal::from(total) * Decimal::from(self.rng.gen_range(1000..50000)),
810 average_days_to_pay: self.rng.gen_range(20.0..45.0),
811 last_payment_date: None,
812 total_discounts: Decimal::from(early) * Decimal::from(self.rng.gen_range(50..500)),
813 }
814 }
815
816 fn generate_dependency(&mut self, vendor_id: &str, vendor_name: &str) -> VendorDependency {
818 let is_single_source = self.rng.gen::<f64>() < self.network_config.single_source_percent;
819
820 let substitutability = {
821 let roll: f64 = self.rng.gen();
822 if roll < 0.60 {
823 Substitutability::Easy
824 } else if roll < 0.90 {
825 Substitutability::Moderate
826 } else {
827 Substitutability::Difficult
828 }
829 };
830
831 let mut dep = VendorDependency::new(vendor_id, self.infer_spend_category(vendor_name));
832 dep.is_single_source = is_single_source;
833 dep.substitutability = substitutability;
834 dep.concentration_percent = self.rng.gen_range(0.01..0.20);
835
836 if !is_single_source {
838 let alt_count = self.rng.gen_range(1..4);
839 for i in 0..alt_count {
840 dep.alternatives.push(format!("ALT-{}-{:03}", vendor_id, i));
841 }
842 }
843
844 dep
845 }
846
847 fn infer_spend_category(&self, _vendor_name: &str) -> String {
849 "General".to_string()
850 }
851
852 fn assign_annual_spend(&mut self, network: &mut VendorNetwork, total_spend: Decimal) {
854 let tier1_count = network.tier1_vendors.len();
855 if tier1_count == 0 {
856 return;
857 }
858
859 let mut weights: Vec<f64> = (0..tier1_count)
861 .map(|_| {
862 let u: f64 = self.rng.gen_range(0.01..1.0);
864 u.powf(-1.0 / 1.5)
865 })
866 .collect();
867
868 let total_weight: f64 = weights.iter().sum();
869 for w in &mut weights {
870 *w /= total_weight;
871 }
872
873 weights.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
875
876 for (idx, vendor_id) in network.tier1_vendors.clone().iter().enumerate() {
878 if let Some(rel) = network.get_relationship_mut(vendor_id) {
879 let weight = weights.get(idx).copied().unwrap_or(0.01);
880 let spend = total_spend * Decimal::from_f64_retain(weight).unwrap_or(Decimal::ZERO);
881 rel.annual_spend = spend;
882
883 if let Some(dep) = &mut rel.dependency {
885 dep.concentration_percent = weight;
886 }
887 }
888 }
889
890 let tier2_avg_spend = total_spend / Decimal::from(network.tier2_vendors.len().max(1))
892 * Decimal::from_f64_retain(0.05).unwrap_or(Decimal::ZERO);
893 for vendor_id in &network.tier2_vendors.clone() {
894 if let Some(rel) = network.get_relationship_mut(vendor_id) {
895 rel.annual_spend = tier2_avg_spend
896 * Decimal::from_f64_retain(self.rng.gen_range(0.5..1.5))
897 .unwrap_or(Decimal::ONE);
898 }
899 }
900
901 let tier3_avg_spend = total_spend / Decimal::from(network.tier3_vendors.len().max(1))
902 * Decimal::from_f64_retain(0.01).unwrap_or(Decimal::ZERO);
903 for vendor_id in &network.tier3_vendors.clone() {
904 if let Some(rel) = network.get_relationship_mut(vendor_id) {
905 rel.annual_spend = tier3_avg_spend
906 * Decimal::from_f64_retain(self.rng.gen_range(0.5..1.5))
907 .unwrap_or(Decimal::ONE);
908 }
909 }
910 }
911
912 pub fn generate_vendor_pool_with_network(
914 &mut self,
915 company_code: &str,
916 effective_date: NaiveDate,
917 total_annual_spend: Decimal,
918 ) -> (VendorPool, VendorNetwork) {
919 let network =
920 self.generate_vendor_network(company_code, effective_date, total_annual_spend);
921
922 let mut pool = VendorPool::new();
924 for _vendor_id in network
925 .tier1_vendors
926 .iter()
927 .chain(network.tier2_vendors.iter())
928 .chain(network.tier3_vendors.iter())
929 {
930 let vendor = self.generate_vendor(company_code, effective_date);
933 pool.add_vendor(vendor);
934 }
935
936 (pool, network)
937 }
938}
939
940#[cfg(test)]
941#[allow(clippy::unwrap_used)]
942mod tests {
943 use super::*;
944
945 #[test]
946 fn test_vendor_generation() {
947 let mut gen = VendorGenerator::new(42);
948 let vendor = gen.generate_vendor("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
949
950 assert!(!vendor.vendor_id.is_empty());
951 assert!(!vendor.name.is_empty());
952 assert!(vendor.tax_id.is_some());
953 assert!(!vendor.bank_accounts.is_empty());
954 }
955
956 #[test]
957 fn test_vendor_pool_generation() {
958 let mut gen = VendorGenerator::new(42);
959 let pool =
960 gen.generate_vendor_pool(10, "1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
961
962 assert_eq!(pool.vendors.len(), 10);
963 }
964
965 #[test]
966 fn test_intercompany_vendor() {
967 let mut gen = VendorGenerator::new(42);
968 let vendor = gen.generate_intercompany_vendor(
969 "1000",
970 "2000",
971 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
972 );
973
974 assert!(vendor.is_intercompany);
975 assert_eq!(vendor.intercompany_code, Some("2000".to_string()));
976 }
977
978 #[test]
979 fn test_deterministic_generation() {
980 let mut gen1 = VendorGenerator::new(42);
981 let mut gen2 = VendorGenerator::new(42);
982
983 let vendor1 = gen1.generate_vendor("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
984 let vendor2 = gen2.generate_vendor("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
985
986 assert_eq!(vendor1.vendor_id, vendor2.vendor_id);
987 assert_eq!(vendor1.name, vendor2.name);
988 }
989
990 #[test]
991 fn test_vendor_pool_with_ic() {
992 let config = VendorGeneratorConfig {
994 intercompany_rate: 0.0,
995 ..Default::default()
996 };
997 let mut gen = VendorGenerator::with_config(42, config);
998 let pool = gen.generate_vendor_pool_with_ic(
999 10,
1000 "1000",
1001 &["2000".to_string(), "3000".to_string()],
1002 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1003 );
1004
1005 assert_eq!(pool.vendors.len(), 10);
1006
1007 let ic_vendors: Vec<_> = pool.vendors.iter().filter(|v| v.is_intercompany).collect();
1008 assert_eq!(ic_vendors.len(), 2);
1009 }
1010
1011 #[test]
1012 fn test_enhanced_vendor_names() {
1013 let mut gen = VendorGenerator::new(42);
1014 let vendor = gen.generate_vendor("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1015
1016 assert!(!vendor.name.is_empty());
1018 assert!(!vendor.name.starts_with("Vendor "));
1020 }
1021
1022 #[test]
1025 fn test_vendor_network_generation() {
1026 let network_config = VendorNetworkConfig {
1027 enabled: true,
1028 depth: 2,
1029 tier1_count: TierCountConfig::new(5, 10),
1030 tier2_per_parent: TierCountConfig::new(2, 4),
1031 tier3_per_parent: TierCountConfig::new(1, 2),
1032 ..Default::default()
1033 };
1034
1035 let mut gen = VendorGenerator::with_network_config(
1036 42,
1037 VendorGeneratorConfig::default(),
1038 network_config,
1039 );
1040
1041 let network = gen.generate_vendor_network(
1042 "1000",
1043 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1044 Decimal::from(10_000_000),
1045 );
1046
1047 assert!(!network.tier1_vendors.is_empty());
1048 assert!(!network.tier2_vendors.is_empty());
1049 assert!(network.tier1_vendors.len() >= 5);
1050 assert!(network.tier1_vendors.len() <= 10);
1051 }
1052
1053 #[test]
1054 fn test_vendor_network_relationships() {
1055 let network_config = VendorNetworkConfig {
1056 enabled: true,
1057 depth: 2,
1058 tier1_count: TierCountConfig::new(3, 3),
1059 tier2_per_parent: TierCountConfig::new(2, 2),
1060 ..Default::default()
1061 };
1062
1063 let mut gen = VendorGenerator::with_network_config(
1064 42,
1065 VendorGeneratorConfig::default(),
1066 network_config,
1067 );
1068
1069 let network = gen.generate_vendor_network(
1070 "1000",
1071 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1072 Decimal::from(5_000_000),
1073 );
1074
1075 for tier2_id in &network.tier2_vendors {
1077 let rel = network.get_relationship(tier2_id).unwrap();
1078 assert!(rel.parent_vendor.is_some());
1079 assert_eq!(rel.tier, SupplyChainTier::Tier2);
1080 }
1081
1082 for tier1_id in &network.tier1_vendors {
1084 let rel = network.get_relationship(tier1_id).unwrap();
1085 assert!(!rel.child_vendors.is_empty());
1086 assert_eq!(rel.tier, SupplyChainTier::Tier1);
1087 }
1088 }
1089
1090 #[test]
1091 fn test_vendor_network_spend_distribution() {
1092 let network_config = VendorNetworkConfig {
1093 enabled: true,
1094 depth: 1,
1095 tier1_count: TierCountConfig::new(10, 10),
1096 ..Default::default()
1097 };
1098
1099 let mut gen = VendorGenerator::with_network_config(
1100 42,
1101 VendorGeneratorConfig::default(),
1102 network_config,
1103 );
1104
1105 let total_spend = Decimal::from(10_000_000);
1106 let network = gen.generate_vendor_network(
1107 "1000",
1108 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1109 total_spend,
1110 );
1111
1112 let total_assigned: Decimal = network.relationships.values().map(|r| r.annual_spend).sum();
1114
1115 assert!(total_assigned > Decimal::ZERO);
1116 }
1117
1118 #[test]
1119 fn test_vendor_network_cluster_distribution() {
1120 let network_config = VendorNetworkConfig {
1121 enabled: true,
1122 depth: 1,
1123 tier1_count: TierCountConfig::new(100, 100),
1124 cluster_distribution: ClusterDistribution::default(),
1125 ..Default::default()
1126 };
1127
1128 let mut gen = VendorGenerator::with_network_config(
1129 42,
1130 VendorGeneratorConfig::default(),
1131 network_config,
1132 );
1133
1134 let network = gen.generate_vendor_network(
1135 "1000",
1136 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1137 Decimal::from(10_000_000),
1138 );
1139
1140 let mut cluster_counts: std::collections::HashMap<VendorCluster, usize> =
1142 std::collections::HashMap::new();
1143 for rel in network.relationships.values() {
1144 *cluster_counts.entry(rel.cluster).or_insert(0) += 1;
1145 }
1146
1147 let reliable = cluster_counts
1149 .get(&VendorCluster::ReliableStrategic)
1150 .unwrap_or(&0);
1151 assert!(*reliable >= 10 && *reliable <= 35);
1152 }
1153
1154 #[test]
1155 fn test_cluster_distribution_validation() {
1156 let valid = ClusterDistribution::default();
1157 assert!(valid.validate().is_ok());
1158
1159 let invalid = ClusterDistribution {
1160 reliable_strategic: 0.5,
1161 standard_operational: 0.5,
1162 transactional: 0.5,
1163 problematic: 0.5,
1164 };
1165 assert!(invalid.validate().is_err());
1166 }
1167
1168 #[test]
1169 fn test_vendor_network_disabled() {
1170 let network_config = VendorNetworkConfig {
1171 enabled: false,
1172 ..Default::default()
1173 };
1174
1175 let mut gen = VendorGenerator::with_network_config(
1176 42,
1177 VendorGeneratorConfig::default(),
1178 network_config,
1179 );
1180
1181 let network = gen.generate_vendor_network(
1182 "1000",
1183 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1184 Decimal::from(10_000_000),
1185 );
1186
1187 assert!(network.tier1_vendors.is_empty());
1188 assert!(network.relationships.is_empty());
1189 }
1190}