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