use std::collections::HashSet;
use chrono::NaiveDate;
use datasynth_core::models::{
BankAccount, DeclineReason, PaymentHistory, PaymentTerms, SpendTier, StrategicLevel,
Substitutability, SupplyChainTier, Vendor, VendorBehavior, VendorCluster, VendorDependency,
VendorLifecycleStage, VendorNetwork, VendorPool, VendorQualityScore, VendorRelationship,
VendorRelationshipType,
};
use datasynth_core::templates::{
AddressGenerator, AddressRegion, SpendCategory, VendorNameGenerator,
};
use datasynth_core::utils::seeded_rng;
use rand::prelude::*;
use rand_chacha::ChaCha8Rng;
use rust_decimal::Decimal;
use tracing::debug;
use crate::coa_generator::CoAFramework;
#[derive(Debug, Clone)]
pub struct VendorGeneratorConfig {
pub payment_terms_distribution: Vec<(PaymentTerms, f64)>,
pub behavior_distribution: Vec<(VendorBehavior, f64)>,
pub intercompany_rate: f64,
pub default_country: String,
pub default_currency: String,
pub generate_bank_accounts: bool,
pub multiple_bank_account_rate: f64,
pub spend_category_distribution: Vec<(SpendCategory, f64)>,
pub primary_region: AddressRegion,
pub use_enhanced_naming: bool,
}
impl Default for VendorGeneratorConfig {
fn default() -> Self {
Self {
payment_terms_distribution: vec![
(PaymentTerms::Net30, 0.40),
(PaymentTerms::Net60, 0.20),
(PaymentTerms::TwoTenNet30, 0.25),
(PaymentTerms::Net15, 0.10),
(PaymentTerms::Immediate, 0.05),
],
behavior_distribution: vec![
(VendorBehavior::Flexible, 0.60),
(VendorBehavior::Strict, 0.25),
(VendorBehavior::VeryFlexible, 0.10),
(VendorBehavior::Aggressive, 0.05),
],
intercompany_rate: 0.05,
default_country: "US".to_string(),
default_currency: "USD".to_string(),
generate_bank_accounts: true,
multiple_bank_account_rate: 0.20,
spend_category_distribution: vec![
(SpendCategory::OfficeSupplies, 0.15),
(SpendCategory::ITServices, 0.12),
(SpendCategory::ProfessionalServices, 0.12),
(SpendCategory::Telecommunications, 0.08),
(SpendCategory::Utilities, 0.08),
(SpendCategory::RawMaterials, 0.10),
(SpendCategory::Logistics, 0.10),
(SpendCategory::Marketing, 0.08),
(SpendCategory::Facilities, 0.07),
(SpendCategory::Staffing, 0.05),
(SpendCategory::Travel, 0.05),
],
primary_region: AddressRegion::NorthAmerica,
use_enhanced_naming: true,
}
}
}
#[derive(Debug, Clone)]
pub struct VendorNetworkConfig {
pub enabled: bool,
pub depth: u8,
pub tier1_count: TierCountConfig,
pub tier2_per_parent: TierCountConfig,
pub tier3_per_parent: TierCountConfig,
pub cluster_distribution: ClusterDistribution,
pub concentration_limits: ConcentrationLimits,
pub strategic_distribution: Vec<(StrategicLevel, f64)>,
pub single_source_percent: f64,
}
#[derive(Debug, Clone)]
pub struct TierCountConfig {
pub min: usize,
pub max: usize,
}
impl TierCountConfig {
pub fn new(min: usize, max: usize) -> Self {
Self { min, max }
}
pub fn sample(&self, rng: &mut impl Rng) -> usize {
rng.random_range(self.min..=self.max)
}
}
#[derive(Debug, Clone)]
pub struct ClusterDistribution {
pub reliable_strategic: f64,
pub standard_operational: f64,
pub transactional: f64,
pub problematic: f64,
}
impl Default for ClusterDistribution {
fn default() -> Self {
Self {
reliable_strategic: 0.20,
standard_operational: 0.50,
transactional: 0.25,
problematic: 0.05,
}
}
}
impl ClusterDistribution {
pub fn validate(&self) -> Result<(), String> {
let sum = self.reliable_strategic
+ self.standard_operational
+ self.transactional
+ self.problematic;
if (sum - 1.0).abs() > 0.01 {
Err(format!("Cluster distribution must sum to 1.0, got {sum}"))
} else {
Ok(())
}
}
pub fn select(&self, roll: f64) -> VendorCluster {
let mut cumulative = 0.0;
cumulative += self.reliable_strategic;
if roll < cumulative {
return VendorCluster::ReliableStrategic;
}
cumulative += self.standard_operational;
if roll < cumulative {
return VendorCluster::StandardOperational;
}
cumulative += self.transactional;
if roll < cumulative {
return VendorCluster::Transactional;
}
VendorCluster::Problematic
}
}
#[derive(Debug, Clone)]
pub struct ConcentrationLimits {
pub max_single_vendor: f64,
pub max_top5: f64,
}
impl Default for ConcentrationLimits {
fn default() -> Self {
Self {
max_single_vendor: 0.15,
max_top5: 0.45,
}
}
}
impl Default for VendorNetworkConfig {
fn default() -> Self {
Self {
enabled: false,
depth: 3,
tier1_count: TierCountConfig::new(50, 100),
tier2_per_parent: TierCountConfig::new(4, 10),
tier3_per_parent: TierCountConfig::new(2, 5),
cluster_distribution: ClusterDistribution::default(),
concentration_limits: ConcentrationLimits::default(),
strategic_distribution: vec![
(StrategicLevel::Critical, 0.05),
(StrategicLevel::Important, 0.15),
(StrategicLevel::Standard, 0.50),
(StrategicLevel::Transactional, 0.30),
],
single_source_percent: 0.05,
}
}
}
const BANK_NAMES: &[&str] = &[
"First National Bank",
"Commerce Bank",
"United Banking Corp",
"Regional Trust Bank",
"Merchants Bank",
"Citizens Financial",
"Pacific Coast Bank",
"Atlantic Commerce Bank",
"Midwest Trust Company",
"Capital One Commercial",
];
pub struct VendorGenerator {
rng: ChaCha8Rng,
seed: u64,
config: VendorGeneratorConfig,
vendor_counter: usize,
vendor_name_gen: VendorNameGenerator,
address_gen: AddressGenerator,
network_config: VendorNetworkConfig,
country_pack: Option<datasynth_core::CountryPack>,
coa_framework: CoAFramework,
used_names: HashSet<String>,
}
impl VendorGenerator {
pub fn new(seed: u64) -> Self {
Self::with_config(seed, VendorGeneratorConfig::default())
}
pub fn with_config(seed: u64, config: VendorGeneratorConfig) -> Self {
Self {
rng: seeded_rng(seed, 0),
seed,
vendor_name_gen: VendorNameGenerator::new(),
address_gen: AddressGenerator::for_region(config.primary_region),
config,
vendor_counter: 0,
network_config: VendorNetworkConfig::default(),
country_pack: None,
coa_framework: CoAFramework::UsGaap,
used_names: HashSet::new(),
}
}
pub fn with_network_config(
seed: u64,
config: VendorGeneratorConfig,
network_config: VendorNetworkConfig,
) -> Self {
Self {
rng: seeded_rng(seed, 0),
seed,
vendor_name_gen: VendorNameGenerator::new(),
address_gen: AddressGenerator::for_region(config.primary_region),
config,
vendor_counter: 0,
network_config,
country_pack: None,
coa_framework: CoAFramework::UsGaap,
used_names: HashSet::new(),
}
}
pub fn set_coa_framework(&mut self, framework: CoAFramework) {
self.coa_framework = framework;
}
pub fn set_network_config(&mut self, network_config: VendorNetworkConfig) {
self.network_config = network_config;
}
pub fn set_country_pack(&mut self, pack: datasynth_core::CountryPack) {
self.country_pack = Some(pack);
}
pub fn set_counter_offset(&mut self, offset: usize) {
self.vendor_counter = offset;
}
pub fn generate_vendor(&mut self, company_code: &str, _effective_date: NaiveDate) -> Vendor {
self.vendor_counter += 1;
let vendor_id = format!("V-{:06}", self.vendor_counter);
let (_category, name) = self.select_vendor_name_unique();
let tax_id = self.generate_tax_id();
let _address = self.address_gen.generate_commercial(&mut self.rng);
let mut vendor = Vendor::new(
&vendor_id,
&name,
datasynth_core::models::VendorType::Supplier,
);
vendor.tax_id = Some(tax_id);
vendor.country = self.config.default_country.clone();
vendor.currency = self.config.default_currency.clone();
vendor.payment_terms = self.select_payment_terms();
vendor.behavior = self.select_vendor_behavior();
vendor.auxiliary_gl_account = self.generate_auxiliary_gl_account();
if self.rng.random::<f64>() < self.config.intercompany_rate {
vendor.is_intercompany = true;
vendor.intercompany_code = Some(format!("IC-{company_code}"));
}
if self.config.generate_bank_accounts {
let bank_account = self.generate_bank_account(&vendor.vendor_id);
vendor.bank_accounts.push(bank_account);
if self.rng.random::<f64>() < self.config.multiple_bank_account_rate {
let bank_account2 = self.generate_bank_account(&vendor.vendor_id);
vendor.bank_accounts.push(bank_account2);
}
}
vendor
}
pub fn generate_intercompany_vendor(
&mut self,
company_code: &str,
partner_company_code: &str,
effective_date: NaiveDate,
) -> Vendor {
let mut vendor = self.generate_vendor(company_code, effective_date);
vendor.is_intercompany = true;
vendor.intercompany_code = Some(partner_company_code.to_string());
vendor.name = format!("{partner_company_code} - IC");
vendor.payment_terms = PaymentTerms::Immediate; vendor
}
pub fn generate_vendor_pool(
&mut self,
count: usize,
company_code: &str,
effective_date: NaiveDate,
) -> VendorPool {
debug!(count, company_code, %effective_date, "Generating vendor pool");
let mut pool = VendorPool::new();
for _ in 0..count {
let vendor = self.generate_vendor(company_code, effective_date);
pool.add_vendor(vendor);
}
pool
}
pub fn generate_vendor_pool_with_ic(
&mut self,
count: usize,
company_code: &str,
partner_company_codes: &[String],
effective_date: NaiveDate,
) -> VendorPool {
let mut pool = VendorPool::new();
let regular_count = count.saturating_sub(partner_company_codes.len());
for _ in 0..regular_count {
let vendor = self.generate_vendor(company_code, effective_date);
pool.add_vendor(vendor);
}
for partner in partner_company_codes {
let vendor = self.generate_intercompany_vendor(company_code, partner, effective_date);
pool.add_vendor(vendor);
}
pool
}
fn select_spend_category(&mut self) -> SpendCategory {
let roll: f64 = self.rng.random();
let mut cumulative = 0.0;
for (category, prob) in &self.config.spend_category_distribution {
cumulative += prob;
if roll < cumulative {
return *category;
}
}
SpendCategory::OfficeSupplies
}
fn select_vendor_name(&mut self) -> (SpendCategory, String) {
let category = self.select_spend_category();
if self.config.use_enhanced_naming {
let name = self.vendor_name_gen.generate(category, &mut self.rng);
(category, name)
} else {
let name = format!("{:?} Vendor {}", category, self.vendor_counter);
(category, name)
}
}
fn select_vendor_name_unique(&mut self) -> (SpendCategory, String) {
let (category, mut name) = self.select_vendor_name();
if self.used_names.contains(&name) {
let suffixes = [
" II",
" III",
" & Co.",
" Group",
" Holdings",
" International",
];
let mut found_unique = false;
for suffix in &suffixes {
let candidate = format!("{name}{suffix}");
if !self.used_names.contains(&candidate) {
name = candidate;
found_unique = true;
break;
}
}
if !found_unique {
name = format!("{} #{}", name, self.vendor_counter);
}
}
self.used_names.insert(name.clone());
(category, name)
}
fn generate_auxiliary_gl_account(&self) -> Option<String> {
match self.coa_framework {
CoAFramework::FrenchPcg => {
Some(format!("401{:04}", self.vendor_counter))
}
CoAFramework::GermanSkr04 => {
Some(format!(
"{}{:04}",
datasynth_core::skr::control_accounts::AP_CONTROL,
self.vendor_counter
))
}
CoAFramework::UsGaap => None,
}
}
fn select_payment_terms(&mut self) -> PaymentTerms {
let roll: f64 = self.rng.random();
let mut cumulative = 0.0;
for (terms, prob) in &self.config.payment_terms_distribution {
cumulative += prob;
if roll < cumulative {
return *terms;
}
}
PaymentTerms::Net30
}
fn select_vendor_behavior(&mut self) -> VendorBehavior {
let roll: f64 = self.rng.random();
let mut cumulative = 0.0;
for (behavior, prob) in &self.config.behavior_distribution {
cumulative += prob;
if roll < cumulative {
return *behavior;
}
}
VendorBehavior::Flexible
}
fn generate_tax_id(&mut self) -> String {
format!(
"{:02}-{:07}",
self.rng.random_range(10..99),
self.rng.random_range(1000000..9999999)
)
}
fn generate_bank_account(&mut self, vendor_id: &str) -> BankAccount {
let bank_idx = self.rng.random_range(0..BANK_NAMES.len());
let bank_name = BANK_NAMES[bank_idx];
let routing = format!("{:09}", self.rng.random_range(100000000u64..999999999));
let account = format!("{:010}", self.rng.random_range(1000000000u64..9999999999));
BankAccount {
bank_name: bank_name.to_string(),
bank_country: "US".to_string(),
account_number: account,
routing_code: routing,
holder_name: format!("Vendor {vendor_id}"),
is_primary: self.vendor_counter == 1,
}
}
pub fn reset(&mut self) {
self.rng = seeded_rng(self.seed, 0);
self.vendor_counter = 0;
self.vendor_name_gen = VendorNameGenerator::new();
self.address_gen = AddressGenerator::for_region(self.config.primary_region);
self.used_names.clear();
}
pub fn generate_vendor_network(
&mut self,
company_code: &str,
effective_date: NaiveDate,
total_annual_spend: Decimal,
) -> VendorNetwork {
let mut network = VendorNetwork::new(company_code);
network.created_date = Some(effective_date);
if !self.network_config.enabled {
return network;
}
let tier1_count = self.network_config.tier1_count.sample(&mut self.rng);
let tier1_ids = self.generate_tier_vendors(
company_code,
effective_date,
tier1_count,
SupplyChainTier::Tier1,
None,
&mut network,
);
if self.network_config.depth >= 2 {
for tier1_id in &tier1_ids {
let tier2_count = self.network_config.tier2_per_parent.sample(&mut self.rng);
let tier2_ids = self.generate_tier_vendors(
company_code,
effective_date,
tier2_count,
SupplyChainTier::Tier2,
Some(tier1_id.clone()),
&mut network,
);
if let Some(rel) = network.get_relationship_mut(tier1_id) {
rel.child_vendors = tier2_ids.clone();
}
if self.network_config.depth >= 3 {
for tier2_id in &tier2_ids {
let tier3_count =
self.network_config.tier3_per_parent.sample(&mut self.rng);
let tier3_ids = self.generate_tier_vendors(
company_code,
effective_date,
tier3_count,
SupplyChainTier::Tier3,
Some(tier2_id.clone()),
&mut network,
);
if let Some(rel) = network.get_relationship_mut(tier2_id) {
rel.child_vendors = tier3_ids;
}
}
}
}
}
self.assign_annual_spend(&mut network, total_annual_spend);
network.calculate_statistics(effective_date);
network
}
fn generate_tier_vendors(
&mut self,
company_code: &str,
effective_date: NaiveDate,
count: usize,
tier: SupplyChainTier,
parent_id: Option<String>,
network: &mut VendorNetwork,
) -> Vec<String> {
let mut vendor_ids = Vec::with_capacity(count);
for _ in 0..count {
let vendor = self.generate_vendor(company_code, effective_date);
let vendor_id = vendor.vendor_id.clone();
let mut relationship = VendorRelationship::new(
vendor_id.clone(),
self.select_relationship_type(),
tier,
self.generate_relationship_start_date(effective_date),
);
if let Some(ref parent) = parent_id {
relationship = relationship.with_parent(parent.clone());
}
relationship = relationship
.with_cluster(self.select_cluster())
.with_strategic_importance(self.select_strategic_level())
.with_spend_tier(self.select_spend_tier());
relationship.lifecycle_stage = self.generate_lifecycle_stage(effective_date);
relationship.quality_score = self.generate_quality_score(&relationship.cluster);
relationship.payment_history = self.generate_payment_history(&relationship.cluster);
if tier == SupplyChainTier::Tier1 {
relationship.dependency = Some(self.generate_dependency(&vendor_id, &vendor.name));
}
network.add_relationship(relationship);
vendor_ids.push(vendor_id);
}
vendor_ids
}
fn select_relationship_type(&mut self) -> VendorRelationshipType {
let roll: f64 = self.rng.random();
if roll < 0.40 {
VendorRelationshipType::DirectSupplier
} else if roll < 0.55 {
VendorRelationshipType::ServiceProvider
} else if roll < 0.70 {
VendorRelationshipType::RawMaterialSupplier
} else if roll < 0.80 {
VendorRelationshipType::Manufacturer
} else if roll < 0.88 {
VendorRelationshipType::Distributor
} else if roll < 0.94 {
VendorRelationshipType::Contractor
} else {
VendorRelationshipType::OemPartner
}
}
fn select_cluster(&mut self) -> VendorCluster {
let roll: f64 = self.rng.random();
self.network_config.cluster_distribution.select(roll)
}
fn select_strategic_level(&mut self) -> StrategicLevel {
let roll: f64 = self.rng.random();
let mut cumulative = 0.0;
for (level, prob) in &self.network_config.strategic_distribution {
cumulative += prob;
if roll < cumulative {
return *level;
}
}
StrategicLevel::Standard
}
fn select_spend_tier(&mut self) -> SpendTier {
let roll: f64 = self.rng.random();
if roll < 0.05 {
SpendTier::Platinum
} else if roll < 0.20 {
SpendTier::Gold
} else if roll < 0.50 {
SpendTier::Silver
} else {
SpendTier::Bronze
}
}
fn generate_relationship_start_date(&mut self, effective_date: NaiveDate) -> NaiveDate {
let days_back: i64 = self.rng.random_range(90..3650); effective_date - chrono::Duration::days(days_back)
}
fn generate_lifecycle_stage(&mut self, effective_date: NaiveDate) -> VendorLifecycleStage {
let roll: f64 = self.rng.random();
if roll < 0.05 {
VendorLifecycleStage::Onboarding {
started: effective_date - chrono::Duration::days(self.rng.random_range(1..60)),
expected_completion: effective_date
+ chrono::Duration::days(self.rng.random_range(30..90)),
}
} else if roll < 0.12 {
VendorLifecycleStage::RampUp {
started: effective_date - chrono::Duration::days(self.rng.random_range(60..180)),
target_volume_percent: self.rng.random_range(50..80) as u8,
}
} else if roll < 0.85 {
VendorLifecycleStage::SteadyState {
since: effective_date - chrono::Duration::days(self.rng.random_range(180..1825)),
}
} else if roll < 0.95 {
VendorLifecycleStage::Decline {
started: effective_date - chrono::Duration::days(self.rng.random_range(30..180)),
reason: DeclineReason::QualityIssues,
}
} else {
VendorLifecycleStage::SteadyState {
since: effective_date - chrono::Duration::days(self.rng.random_range(365..1825)),
}
}
}
fn generate_quality_score(&mut self, cluster: &VendorCluster) -> VendorQualityScore {
let (base_delivery, base_quality, base_invoice, base_response) = match cluster {
VendorCluster::ReliableStrategic => (0.97, 0.96, 0.98, 0.95),
VendorCluster::StandardOperational => (0.92, 0.90, 0.93, 0.85),
VendorCluster::Transactional => (0.85, 0.82, 0.88, 0.75),
VendorCluster::Problematic => (0.70, 0.68, 0.75, 0.60),
};
let delivery_variance: f64 = self.rng.random_range(-0.05..0.05);
let quality_variance: f64 = self.rng.random_range(-0.05..0.05);
let invoice_variance: f64 = self.rng.random_range(-0.05..0.05);
let response_variance: f64 = self.rng.random_range(-0.05..0.05);
VendorQualityScore {
delivery_score: (base_delivery + delivery_variance).clamp(0.0_f64, 1.0_f64),
quality_score: (base_quality + quality_variance).clamp(0.0_f64, 1.0_f64),
invoice_accuracy_score: (base_invoice + invoice_variance).clamp(0.0_f64, 1.0_f64),
responsiveness_score: (base_response + response_variance).clamp(0.0_f64, 1.0_f64),
last_evaluation: NaiveDate::from_ymd_opt(2024, 1, 1).expect("valid default date"),
evaluation_count: self.rng.random_range(1..20),
}
}
fn generate_payment_history(&mut self, cluster: &VendorCluster) -> PaymentHistory {
let total = self.rng.random_range(10..200) as u32;
let on_time_rate = cluster.invoice_accuracy_probability();
let on_time = (total as f64 * on_time_rate) as u32;
let early = (total as f64 * self.rng.random_range(0.05..0.20)) as u32;
let late = total.saturating_sub(on_time).saturating_sub(early);
PaymentHistory {
total_invoices: total,
on_time_payments: on_time,
early_payments: early,
late_payments: late,
total_amount: Decimal::from(total) * Decimal::from(self.rng.random_range(1000..50000)),
average_days_to_pay: self.rng.random_range(20.0..45.0),
last_payment_date: None,
total_discounts: Decimal::from(early) * Decimal::from(self.rng.random_range(50..500)),
}
}
fn generate_dependency(&mut self, vendor_id: &str, vendor_name: &str) -> VendorDependency {
let is_single_source = self.rng.random::<f64>() < self.network_config.single_source_percent;
let substitutability = {
let roll: f64 = self.rng.random();
if roll < 0.60 {
Substitutability::Easy
} else if roll < 0.90 {
Substitutability::Moderate
} else {
Substitutability::Difficult
}
};
let mut dep = VendorDependency::new(vendor_id, self.infer_spend_category(vendor_name));
dep.is_single_source = is_single_source;
dep.substitutability = substitutability;
dep.concentration_percent = self.rng.random_range(0.01..0.20);
if !is_single_source {
let alt_count = self.rng.random_range(1..4);
for i in 0..alt_count {
dep.alternatives.push(format!("ALT-{vendor_id}-{i:03}"));
}
}
dep
}
fn infer_spend_category(&self, _vendor_name: &str) -> String {
"General".to_string()
}
fn assign_annual_spend(&mut self, network: &mut VendorNetwork, total_spend: Decimal) {
let tier1_count = network.tier1_vendors.len();
if tier1_count == 0 {
return;
}
let mut weights: Vec<f64> = (0..tier1_count)
.map(|_| {
let u: f64 = self.rng.random_range(0.01..1.0);
u.powf(-1.0 / 1.5)
})
.collect();
let total_weight: f64 = weights.iter().sum();
for w in &mut weights {
*w /= total_weight;
}
weights.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
for (idx, vendor_id) in network.tier1_vendors.clone().iter().enumerate() {
if let Some(rel) = network.get_relationship_mut(vendor_id) {
let weight = weights.get(idx).copied().unwrap_or(0.01);
let spend = total_spend * Decimal::from_f64_retain(weight).unwrap_or(Decimal::ZERO);
rel.annual_spend = spend;
if let Some(dep) = &mut rel.dependency {
dep.concentration_percent = weight;
}
}
}
let tier2_avg_spend = total_spend / Decimal::from(network.tier2_vendors.len().max(1))
* Decimal::from_f64_retain(0.05).unwrap_or(Decimal::ZERO);
for vendor_id in &network.tier2_vendors.clone() {
if let Some(rel) = network.get_relationship_mut(vendor_id) {
rel.annual_spend = tier2_avg_spend
* Decimal::from_f64_retain(self.rng.random_range(0.5..1.5))
.unwrap_or(Decimal::ONE);
}
}
let tier3_avg_spend = total_spend / Decimal::from(network.tier3_vendors.len().max(1))
* Decimal::from_f64_retain(0.01).unwrap_or(Decimal::ZERO);
for vendor_id in &network.tier3_vendors.clone() {
if let Some(rel) = network.get_relationship_mut(vendor_id) {
rel.annual_spend = tier3_avg_spend
* Decimal::from_f64_retain(self.rng.random_range(0.5..1.5))
.unwrap_or(Decimal::ONE);
}
}
}
pub fn generate_vendor_pool_with_network(
&mut self,
company_code: &str,
effective_date: NaiveDate,
total_annual_spend: Decimal,
) -> (VendorPool, VendorNetwork) {
let network =
self.generate_vendor_network(company_code, effective_date, total_annual_spend);
let mut pool = VendorPool::new();
for _vendor_id in network
.tier1_vendors
.iter()
.chain(network.tier2_vendors.iter())
.chain(network.tier3_vendors.iter())
{
let vendor = self.generate_vendor(company_code, effective_date);
pool.add_vendor(vendor);
}
(pool, network)
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_vendor_generation() {
let mut gen = VendorGenerator::new(42);
let vendor = gen.generate_vendor("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
assert!(!vendor.vendor_id.is_empty());
assert!(!vendor.name.is_empty());
assert!(vendor.tax_id.is_some());
assert!(!vendor.bank_accounts.is_empty());
}
#[test]
fn test_vendor_pool_generation() {
let mut gen = VendorGenerator::new(42);
let pool =
gen.generate_vendor_pool(10, "1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
assert_eq!(pool.vendors.len(), 10);
}
#[test]
fn test_intercompany_vendor() {
let mut gen = VendorGenerator::new(42);
let vendor = gen.generate_intercompany_vendor(
"1000",
"2000",
NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
);
assert!(vendor.is_intercompany);
assert_eq!(vendor.intercompany_code, Some("2000".to_string()));
}
#[test]
fn test_deterministic_generation() {
let mut gen1 = VendorGenerator::new(42);
let mut gen2 = VendorGenerator::new(42);
let vendor1 = gen1.generate_vendor("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
let vendor2 = gen2.generate_vendor("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
assert_eq!(vendor1.vendor_id, vendor2.vendor_id);
assert_eq!(vendor1.name, vendor2.name);
}
#[test]
fn test_vendor_pool_with_ic() {
let config = VendorGeneratorConfig {
intercompany_rate: 0.0,
..Default::default()
};
let mut gen = VendorGenerator::with_config(42, config);
let pool = gen.generate_vendor_pool_with_ic(
10,
"1000",
&["2000".to_string(), "3000".to_string()],
NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
);
assert_eq!(pool.vendors.len(), 10);
let ic_vendors: Vec<_> = pool.vendors.iter().filter(|v| v.is_intercompany).collect();
assert_eq!(ic_vendors.len(), 2);
}
#[test]
fn test_enhanced_vendor_names() {
let mut gen = VendorGenerator::new(42);
let vendor = gen.generate_vendor("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
assert!(!vendor.name.is_empty());
assert!(!vendor.name.starts_with("Vendor "));
}
#[test]
fn test_vendor_network_generation() {
let network_config = VendorNetworkConfig {
enabled: true,
depth: 2,
tier1_count: TierCountConfig::new(5, 10),
tier2_per_parent: TierCountConfig::new(2, 4),
tier3_per_parent: TierCountConfig::new(1, 2),
..Default::default()
};
let mut gen = VendorGenerator::with_network_config(
42,
VendorGeneratorConfig::default(),
network_config,
);
let network = gen.generate_vendor_network(
"1000",
NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
Decimal::from(10_000_000),
);
assert!(!network.tier1_vendors.is_empty());
assert!(!network.tier2_vendors.is_empty());
assert!(network.tier1_vendors.len() >= 5);
assert!(network.tier1_vendors.len() <= 10);
}
#[test]
fn test_vendor_network_relationships() {
let network_config = VendorNetworkConfig {
enabled: true,
depth: 2,
tier1_count: TierCountConfig::new(3, 3),
tier2_per_parent: TierCountConfig::new(2, 2),
..Default::default()
};
let mut gen = VendorGenerator::with_network_config(
42,
VendorGeneratorConfig::default(),
network_config,
);
let network = gen.generate_vendor_network(
"1000",
NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
Decimal::from(5_000_000),
);
for tier2_id in &network.tier2_vendors {
let rel = network.get_relationship(tier2_id).unwrap();
assert!(rel.parent_vendor.is_some());
assert_eq!(rel.tier, SupplyChainTier::Tier2);
}
for tier1_id in &network.tier1_vendors {
let rel = network.get_relationship(tier1_id).unwrap();
assert!(!rel.child_vendors.is_empty());
assert_eq!(rel.tier, SupplyChainTier::Tier1);
}
}
#[test]
fn test_vendor_network_spend_distribution() {
let network_config = VendorNetworkConfig {
enabled: true,
depth: 1,
tier1_count: TierCountConfig::new(10, 10),
..Default::default()
};
let mut gen = VendorGenerator::with_network_config(
42,
VendorGeneratorConfig::default(),
network_config,
);
let total_spend = Decimal::from(10_000_000);
let network = gen.generate_vendor_network(
"1000",
NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
total_spend,
);
let total_assigned: Decimal = network.relationships.values().map(|r| r.annual_spend).sum();
assert!(total_assigned > Decimal::ZERO);
}
#[test]
fn test_vendor_network_cluster_distribution() {
let network_config = VendorNetworkConfig {
enabled: true,
depth: 1,
tier1_count: TierCountConfig::new(100, 100),
cluster_distribution: ClusterDistribution::default(),
..Default::default()
};
let mut gen = VendorGenerator::with_network_config(
42,
VendorGeneratorConfig::default(),
network_config,
);
let network = gen.generate_vendor_network(
"1000",
NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
Decimal::from(10_000_000),
);
let mut cluster_counts: std::collections::HashMap<VendorCluster, usize> =
std::collections::HashMap::new();
for rel in network.relationships.values() {
*cluster_counts.entry(rel.cluster).or_insert(0) += 1;
}
let reliable = cluster_counts
.get(&VendorCluster::ReliableStrategic)
.unwrap_or(&0);
assert!(*reliable >= 10 && *reliable <= 35);
}
#[test]
fn test_cluster_distribution_validation() {
let valid = ClusterDistribution::default();
assert!(valid.validate().is_ok());
let invalid = ClusterDistribution {
reliable_strategic: 0.5,
standard_operational: 0.5,
transactional: 0.5,
problematic: 0.5,
};
assert!(invalid.validate().is_err());
}
#[test]
fn test_vendor_network_disabled() {
let network_config = VendorNetworkConfig {
enabled: false,
..Default::default()
};
let mut gen = VendorGenerator::with_network_config(
42,
VendorGeneratorConfig::default(),
network_config,
);
let network = gen.generate_vendor_network(
"1000",
NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
Decimal::from(10_000_000),
);
assert!(network.tier1_vendors.is_empty());
assert!(network.relationships.is_empty());
}
#[test]
fn test_vendor_auxiliary_gl_account_french() {
let mut gen = VendorGenerator::new(42);
gen.set_coa_framework(CoAFramework::FrenchPcg);
let vendor = gen.generate_vendor("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
assert!(vendor.auxiliary_gl_account.is_some());
let aux = vendor.auxiliary_gl_account.unwrap();
assert!(
aux.starts_with("401"),
"French PCG vendor auxiliary should start with 401, got {}",
aux
);
}
#[test]
fn test_vendor_auxiliary_gl_account_german() {
let mut gen = VendorGenerator::new(42);
gen.set_coa_framework(CoAFramework::GermanSkr04);
let vendor = gen.generate_vendor("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
assert!(vendor.auxiliary_gl_account.is_some());
let aux = vendor.auxiliary_gl_account.unwrap();
assert!(
aux.starts_with("3300"),
"German SKR04 vendor auxiliary should start with 3300, got {}",
aux
);
}
#[test]
fn test_vendor_auxiliary_gl_account_us_gaap() {
let mut gen = VendorGenerator::new(42);
let vendor = gen.generate_vendor("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
assert!(vendor.auxiliary_gl_account.is_none());
}
#[test]
fn test_vendor_name_dedup() {
let mut gen = VendorGenerator::new(42);
let mut names = HashSet::new();
for _ in 0..200 {
let vendor = gen.generate_vendor("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
assert!(
names.insert(vendor.name.clone()),
"Duplicate vendor name found: {}",
vendor.name
);
}
}
}