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).unwrap(),
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)]
941mod tests {
942 use super::*;
943
944 #[test]
945 fn test_vendor_generation() {
946 let mut gen = VendorGenerator::new(42);
947 let vendor = gen.generate_vendor("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
948
949 assert!(!vendor.vendor_id.is_empty());
950 assert!(!vendor.name.is_empty());
951 assert!(vendor.tax_id.is_some());
952 assert!(!vendor.bank_accounts.is_empty());
953 }
954
955 #[test]
956 fn test_vendor_pool_generation() {
957 let mut gen = VendorGenerator::new(42);
958 let pool =
959 gen.generate_vendor_pool(10, "1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
960
961 assert_eq!(pool.vendors.len(), 10);
962 }
963
964 #[test]
965 fn test_intercompany_vendor() {
966 let mut gen = VendorGenerator::new(42);
967 let vendor = gen.generate_intercompany_vendor(
968 "1000",
969 "2000",
970 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
971 );
972
973 assert!(vendor.is_intercompany);
974 assert_eq!(vendor.intercompany_code, Some("2000".to_string()));
975 }
976
977 #[test]
978 fn test_deterministic_generation() {
979 let mut gen1 = VendorGenerator::new(42);
980 let mut gen2 = VendorGenerator::new(42);
981
982 let vendor1 = gen1.generate_vendor("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
983 let vendor2 = gen2.generate_vendor("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
984
985 assert_eq!(vendor1.vendor_id, vendor2.vendor_id);
986 assert_eq!(vendor1.name, vendor2.name);
987 }
988
989 #[test]
990 fn test_vendor_pool_with_ic() {
991 let config = VendorGeneratorConfig {
993 intercompany_rate: 0.0,
994 ..Default::default()
995 };
996 let mut gen = VendorGenerator::with_config(42, config);
997 let pool = gen.generate_vendor_pool_with_ic(
998 10,
999 "1000",
1000 &["2000".to_string(), "3000".to_string()],
1001 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1002 );
1003
1004 assert_eq!(pool.vendors.len(), 10);
1005
1006 let ic_vendors: Vec<_> = pool.vendors.iter().filter(|v| v.is_intercompany).collect();
1007 assert_eq!(ic_vendors.len(), 2);
1008 }
1009
1010 #[test]
1011 fn test_enhanced_vendor_names() {
1012 let mut gen = VendorGenerator::new(42);
1013 let vendor = gen.generate_vendor("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
1014
1015 assert!(!vendor.name.is_empty());
1017 assert!(!vendor.name.starts_with("Vendor "));
1019 }
1020
1021 #[test]
1024 fn test_vendor_network_generation() {
1025 let network_config = VendorNetworkConfig {
1026 enabled: true,
1027 depth: 2,
1028 tier1_count: TierCountConfig::new(5, 10),
1029 tier2_per_parent: TierCountConfig::new(2, 4),
1030 tier3_per_parent: TierCountConfig::new(1, 2),
1031 ..Default::default()
1032 };
1033
1034 let mut gen = VendorGenerator::with_network_config(
1035 42,
1036 VendorGeneratorConfig::default(),
1037 network_config,
1038 );
1039
1040 let network = gen.generate_vendor_network(
1041 "1000",
1042 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1043 Decimal::from(10_000_000),
1044 );
1045
1046 assert!(!network.tier1_vendors.is_empty());
1047 assert!(!network.tier2_vendors.is_empty());
1048 assert!(network.tier1_vendors.len() >= 5);
1049 assert!(network.tier1_vendors.len() <= 10);
1050 }
1051
1052 #[test]
1053 fn test_vendor_network_relationships() {
1054 let network_config = VendorNetworkConfig {
1055 enabled: true,
1056 depth: 2,
1057 tier1_count: TierCountConfig::new(3, 3),
1058 tier2_per_parent: TierCountConfig::new(2, 2),
1059 ..Default::default()
1060 };
1061
1062 let mut gen = VendorGenerator::with_network_config(
1063 42,
1064 VendorGeneratorConfig::default(),
1065 network_config,
1066 );
1067
1068 let network = gen.generate_vendor_network(
1069 "1000",
1070 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1071 Decimal::from(5_000_000),
1072 );
1073
1074 for tier2_id in &network.tier2_vendors {
1076 let rel = network.get_relationship(tier2_id).unwrap();
1077 assert!(rel.parent_vendor.is_some());
1078 assert_eq!(rel.tier, SupplyChainTier::Tier2);
1079 }
1080
1081 for tier1_id in &network.tier1_vendors {
1083 let rel = network.get_relationship(tier1_id).unwrap();
1084 assert!(!rel.child_vendors.is_empty());
1085 assert_eq!(rel.tier, SupplyChainTier::Tier1);
1086 }
1087 }
1088
1089 #[test]
1090 fn test_vendor_network_spend_distribution() {
1091 let network_config = VendorNetworkConfig {
1092 enabled: true,
1093 depth: 1,
1094 tier1_count: TierCountConfig::new(10, 10),
1095 ..Default::default()
1096 };
1097
1098 let mut gen = VendorGenerator::with_network_config(
1099 42,
1100 VendorGeneratorConfig::default(),
1101 network_config,
1102 );
1103
1104 let total_spend = Decimal::from(10_000_000);
1105 let network = gen.generate_vendor_network(
1106 "1000",
1107 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1108 total_spend,
1109 );
1110
1111 let total_assigned: Decimal = network.relationships.values().map(|r| r.annual_spend).sum();
1113
1114 assert!(total_assigned > Decimal::ZERO);
1115 }
1116
1117 #[test]
1118 fn test_vendor_network_cluster_distribution() {
1119 let network_config = VendorNetworkConfig {
1120 enabled: true,
1121 depth: 1,
1122 tier1_count: TierCountConfig::new(100, 100),
1123 cluster_distribution: ClusterDistribution::default(),
1124 ..Default::default()
1125 };
1126
1127 let mut gen = VendorGenerator::with_network_config(
1128 42,
1129 VendorGeneratorConfig::default(),
1130 network_config,
1131 );
1132
1133 let network = gen.generate_vendor_network(
1134 "1000",
1135 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1136 Decimal::from(10_000_000),
1137 );
1138
1139 let mut cluster_counts: std::collections::HashMap<VendorCluster, usize> =
1141 std::collections::HashMap::new();
1142 for rel in network.relationships.values() {
1143 *cluster_counts.entry(rel.cluster).or_insert(0) += 1;
1144 }
1145
1146 let reliable = cluster_counts
1148 .get(&VendorCluster::ReliableStrategic)
1149 .unwrap_or(&0);
1150 assert!(*reliable >= 10 && *reliable <= 35);
1151 }
1152
1153 #[test]
1154 fn test_cluster_distribution_validation() {
1155 let valid = ClusterDistribution::default();
1156 assert!(valid.validate().is_ok());
1157
1158 let invalid = ClusterDistribution {
1159 reliable_strategic: 0.5,
1160 standard_operational: 0.5,
1161 transactional: 0.5,
1162 problematic: 0.5,
1163 };
1164 assert!(invalid.validate().is_err());
1165 }
1166
1167 #[test]
1168 fn test_vendor_network_disabled() {
1169 let network_config = VendorNetworkConfig {
1170 enabled: false,
1171 ..Default::default()
1172 };
1173
1174 let mut gen = VendorGenerator::with_network_config(
1175 42,
1176 VendorGeneratorConfig::default(),
1177 network_config,
1178 );
1179
1180 let network = gen.generate_vendor_network(
1181 "1000",
1182 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1183 Decimal::from(10_000_000),
1184 );
1185
1186 assert!(network.tier1_vendors.is_empty());
1187 assert!(network.relationships.is_empty());
1188 }
1189}