use rand::seq::IndexedRandom;
use rand::Rng;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum PaymentTerms {
Immediate,
Net10,
Net15,
#[default]
Net30,
Net45,
Net60,
Net90,
TwoTenNet30,
OneTenNet30,
TwoFifteenNet45,
EndOfMonth,
EndOfMonthPlus30,
CashOnDelivery,
Prepayment,
}
impl PaymentTerms {
pub fn due_days(&self) -> u16 {
match self {
Self::Immediate | Self::CashOnDelivery => 0,
Self::Prepayment => 0,
Self::Net10 | Self::TwoTenNet30 | Self::OneTenNet30 => 30, Self::Net15 | Self::TwoFifteenNet45 => 45,
Self::Net30 => 30,
Self::Net45 => 45,
Self::Net60 => 60,
Self::Net90 => 90,
Self::EndOfMonth => 30, Self::EndOfMonthPlus30 => 60, }
}
pub fn early_payment_discount(&self) -> Option<(u16, Decimal)> {
match self {
Self::TwoTenNet30 => Some((10, Decimal::from(2))),
Self::OneTenNet30 => Some((10, Decimal::from(1))),
Self::TwoFifteenNet45 => Some((15, Decimal::from(2))),
_ => None,
}
}
pub fn requires_prepayment(&self) -> bool {
matches!(self, Self::Prepayment | Self::CashOnDelivery)
}
pub fn code(&self) -> &'static str {
match self {
Self::Immediate => "IMM",
Self::Net10 => "N10",
Self::Net15 => "N15",
Self::Net30 => "N30",
Self::Net45 => "N45",
Self::Net60 => "N60",
Self::Net90 => "N90",
Self::TwoTenNet30 => "2/10N30",
Self::OneTenNet30 => "1/10N30",
Self::TwoFifteenNet45 => "2/15N45",
Self::EndOfMonth => "EOM",
Self::EndOfMonthPlus30 => "EOM30",
Self::CashOnDelivery => "COD",
Self::Prepayment => "PREP",
}
}
pub fn net_days(&self) -> u16 {
self.due_days()
}
pub fn discount_days(&self) -> Option<u16> {
self.early_payment_discount().map(|(days, _)| days)
}
pub fn discount_percent(&self) -> Option<Decimal> {
self.early_payment_discount().map(|(_, percent)| percent)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum VendorBehavior {
Strict,
#[default]
Flexible,
VeryFlexible,
Aggressive,
}
impl VendorBehavior {
pub fn grace_period_days(&self) -> u16 {
match self {
Self::Strict => 0,
Self::Flexible => 7,
Self::VeryFlexible => 30,
Self::Aggressive => 0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum CustomerPaymentBehavior {
Excellent,
EarlyPayer,
#[default]
Good,
OnTime,
Fair,
SlightlyLate,
Poor,
OftenLate,
VeryPoor,
HighRisk,
}
impl CustomerPaymentBehavior {
pub fn average_days_past_due(&self) -> i16 {
match self {
Self::Excellent | Self::EarlyPayer => -5, Self::Good | Self::OnTime => 0,
Self::Fair | Self::SlightlyLate => 10,
Self::Poor | Self::OftenLate => 30,
Self::VeryPoor | Self::HighRisk => 60,
}
}
pub fn on_time_probability(&self) -> f64 {
match self {
Self::Excellent | Self::EarlyPayer => 0.98,
Self::Good | Self::OnTime => 0.90,
Self::Fair | Self::SlightlyLate => 0.70,
Self::Poor | Self::OftenLate => 0.40,
Self::VeryPoor | Self::HighRisk => 0.20,
}
}
pub fn discount_probability(&self) -> f64 {
match self {
Self::Excellent | Self::EarlyPayer => 0.80,
Self::Good | Self::OnTime => 0.50,
Self::Fair | Self::SlightlyLate => 0.20,
Self::Poor | Self::OftenLate => 0.05,
Self::VeryPoor | Self::HighRisk => 0.01,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum CreditRating {
AAA,
AA,
#[default]
A,
BBB,
BB,
B,
CCC,
CC,
C,
D,
}
impl CreditRating {
pub fn credit_limit_multiplier(&self) -> Decimal {
match self {
Self::AAA => Decimal::from(5),
Self::AA => Decimal::from(4),
Self::A => Decimal::from(3),
Self::BBB => Decimal::from(2),
Self::BB => Decimal::from_str_exact("1.5").unwrap_or(Decimal::from(1)),
Self::B => Decimal::from(1),
Self::CCC => Decimal::from_str_exact("0.5").unwrap_or(Decimal::from(1)),
Self::CC => Decimal::from_str_exact("0.25").unwrap_or(Decimal::from(0)),
Self::C => Decimal::from_str_exact("0.1").unwrap_or(Decimal::from(0)),
Self::D => Decimal::ZERO,
}
}
pub fn is_credit_blocked(&self) -> bool {
matches!(self, Self::D)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BankAccount {
pub bank_name: String,
pub bank_country: String,
pub account_number: String,
pub routing_code: String,
pub holder_name: String,
pub is_primary: bool,
}
impl BankAccount {
pub fn new(
bank_name: impl Into<String>,
account_number: impl Into<String>,
routing_code: impl Into<String>,
holder_name: impl Into<String>,
) -> Self {
Self {
bank_name: bank_name.into(),
bank_country: "US".to_string(),
account_number: account_number.into(),
routing_code: routing_code.into(),
holder_name: holder_name.into(),
is_primary: true,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum VendorType {
#[default]
Supplier,
ServiceProvider,
Utility,
ProfessionalServices,
Technology,
Logistics,
Contractor,
RealEstate,
Financial,
EmployeeReimbursement,
}
impl VendorType {
pub fn typical_expense_categories(&self) -> &'static [&'static str] {
match self {
Self::Supplier => &["Materials", "Inventory", "Office Supplies", "Equipment"],
Self::ServiceProvider => &["Services", "Maintenance", "Support"],
Self::Utility => &["Electricity", "Gas", "Water", "Telecommunications"],
Self::ProfessionalServices => &["Legal", "Audit", "Consulting", "Tax Services"],
Self::Technology => &["Software", "Licenses", "Cloud Services", "IT Support"],
Self::Logistics => &["Freight", "Shipping", "Warehousing", "Customs"],
Self::Contractor => &["Contract Labor", "Professional Fees", "Consulting"],
Self::RealEstate => &["Rent", "Property Management", "Facilities"],
Self::Financial => &["Bank Fees", "Interest", "Insurance", "Financing Costs"],
Self::EmployeeReimbursement => &["Travel", "Meals", "Entertainment", "Expenses"],
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Vendor {
pub vendor_id: String,
pub name: String,
pub vendor_type: VendorType,
pub country: String,
pub payment_terms: PaymentTerms,
pub payment_terms_days: u8,
pub typical_amount_range: (Decimal, Decimal),
pub is_active: bool,
pub account_number: Option<String>,
pub tax_id: Option<String>,
pub bank_accounts: Vec<BankAccount>,
pub is_intercompany: bool,
pub intercompany_code: Option<String>,
pub behavior: VendorBehavior,
pub currency: String,
pub reconciliation_account: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub auxiliary_gl_account: Option<String>,
pub withholding_tax_applicable: bool,
pub withholding_tax_rate: Option<Decimal>,
pub is_one_time: bool,
pub purchasing_org: Option<String>,
}
impl Vendor {
pub fn new(vendor_id: &str, name: &str, vendor_type: VendorType) -> Self {
Self {
vendor_id: vendor_id.to_string(),
name: name.to_string(),
vendor_type,
country: "US".to_string(),
payment_terms: PaymentTerms::Net30,
payment_terms_days: 30,
typical_amount_range: (Decimal::from(100), Decimal::from(10000)),
is_active: true,
account_number: None,
tax_id: None,
bank_accounts: Vec::new(),
is_intercompany: false,
intercompany_code: None,
behavior: VendorBehavior::default(),
currency: "USD".to_string(),
reconciliation_account: None,
auxiliary_gl_account: None,
withholding_tax_applicable: false,
withholding_tax_rate: None,
is_one_time: false,
purchasing_org: None,
}
}
pub fn new_intercompany(vendor_id: &str, name: &str, related_company_code: &str) -> Self {
Self::new(vendor_id, name, VendorType::Supplier).with_intercompany(related_company_code)
}
pub fn with_country(mut self, country: &str) -> Self {
self.country = country.to_string();
self
}
pub fn with_payment_terms_structured(mut self, terms: PaymentTerms) -> Self {
self.payment_terms = terms;
self.payment_terms_days = terms.due_days() as u8;
self
}
pub fn with_payment_terms(mut self, days: u8) -> Self {
self.payment_terms_days = days;
self.payment_terms = match days {
0 => PaymentTerms::Immediate,
1..=15 => PaymentTerms::Net15,
16..=35 => PaymentTerms::Net30,
36..=50 => PaymentTerms::Net45,
51..=70 => PaymentTerms::Net60,
_ => PaymentTerms::Net90,
};
self
}
pub fn with_amount_range(mut self, min: Decimal, max: Decimal) -> Self {
self.typical_amount_range = (min, max);
self
}
pub fn with_intercompany(mut self, related_company_code: &str) -> Self {
self.is_intercompany = true;
self.intercompany_code = Some(related_company_code.to_string());
self
}
pub fn with_bank_account(mut self, account: BankAccount) -> Self {
self.bank_accounts.push(account);
self
}
pub fn with_behavior(mut self, behavior: VendorBehavior) -> Self {
self.behavior = behavior;
self
}
pub fn with_currency(mut self, currency: &str) -> Self {
self.currency = currency.to_string();
self
}
pub fn with_reconciliation_account(mut self, account: &str) -> Self {
self.reconciliation_account = Some(account.to_string());
self
}
pub fn with_withholding_tax(mut self, rate: Decimal) -> Self {
self.withholding_tax_applicable = true;
self.withholding_tax_rate = Some(rate);
self
}
pub fn primary_bank_account(&self) -> Option<&BankAccount> {
self.bank_accounts
.iter()
.find(|a| a.is_primary)
.or_else(|| self.bank_accounts.first())
}
pub fn generate_amount(&self, rng: &mut impl Rng) -> Decimal {
let (min, max) = self.typical_amount_range;
let range = max - min;
let random_fraction =
Decimal::from_f64_retain(rng.random::<f64>()).unwrap_or(Decimal::ZERO);
min + range * random_fraction
}
pub fn calculate_due_date(&self, invoice_date: chrono::NaiveDate) -> chrono::NaiveDate {
invoice_date + chrono::Duration::days(self.payment_terms.due_days() as i64)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum CustomerType {
#[default]
Corporate,
SmallBusiness,
Consumer,
Government,
NonProfit,
Intercompany,
Distributor,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Customer {
pub customer_id: String,
pub name: String,
pub customer_type: CustomerType,
pub country: String,
pub credit_rating: CreditRating,
#[serde(with = "crate::serde_decimal")]
pub credit_limit: Decimal,
#[serde(with = "crate::serde_decimal")]
pub credit_exposure: Decimal,
pub payment_terms: PaymentTerms,
pub payment_terms_days: u8,
pub payment_behavior: CustomerPaymentBehavior,
pub is_active: bool,
pub account_number: Option<String>,
pub typical_order_range: (Decimal, Decimal),
pub is_intercompany: bool,
pub intercompany_code: Option<String>,
pub currency: String,
pub reconciliation_account: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub auxiliary_gl_account: Option<String>,
pub sales_org: Option<String>,
pub distribution_channel: Option<String>,
pub tax_id: Option<String>,
pub credit_blocked: bool,
pub credit_block_reason: Option<String>,
pub dunning_procedure: Option<String>,
pub last_dunning_date: Option<chrono::NaiveDate>,
pub dunning_level: u8,
}
impl Customer {
pub fn new(customer_id: &str, name: &str, customer_type: CustomerType) -> Self {
Self {
customer_id: customer_id.to_string(),
name: name.to_string(),
customer_type,
country: "US".to_string(),
credit_rating: CreditRating::default(),
credit_limit: Decimal::from(100000),
credit_exposure: Decimal::ZERO,
payment_terms: PaymentTerms::Net30,
payment_terms_days: 30,
payment_behavior: CustomerPaymentBehavior::default(),
is_active: true,
account_number: None,
typical_order_range: (Decimal::from(500), Decimal::from(50000)),
is_intercompany: false,
intercompany_code: None,
currency: "USD".to_string(),
reconciliation_account: None,
auxiliary_gl_account: None,
sales_org: None,
distribution_channel: None,
tax_id: None,
credit_blocked: false,
credit_block_reason: None,
dunning_procedure: None,
last_dunning_date: None,
dunning_level: 0,
}
}
pub fn new_intercompany(customer_id: &str, name: &str, related_company_code: &str) -> Self {
Self::new(customer_id, name, CustomerType::Intercompany)
.with_intercompany(related_company_code)
}
pub fn with_country(mut self, country: &str) -> Self {
self.country = country.to_string();
self
}
pub fn with_credit_rating(mut self, rating: CreditRating) -> Self {
self.credit_rating = rating;
self.credit_limit *= rating.credit_limit_multiplier();
if rating.is_credit_blocked() {
self.credit_blocked = true;
self.credit_block_reason = Some("Credit rating D".to_string());
}
self
}
pub fn with_credit_limit(mut self, limit: Decimal) -> Self {
self.credit_limit = limit;
self
}
pub fn with_payment_terms_structured(mut self, terms: PaymentTerms) -> Self {
self.payment_terms = terms;
self.payment_terms_days = terms.due_days() as u8;
self
}
pub fn with_payment_terms(mut self, days: u8) -> Self {
self.payment_terms_days = days;
self.payment_terms = match days {
0 => PaymentTerms::Immediate,
1..=15 => PaymentTerms::Net15,
16..=35 => PaymentTerms::Net30,
36..=50 => PaymentTerms::Net45,
51..=70 => PaymentTerms::Net60,
_ => PaymentTerms::Net90,
};
self
}
pub fn with_payment_behavior(mut self, behavior: CustomerPaymentBehavior) -> Self {
self.payment_behavior = behavior;
self
}
pub fn with_intercompany(mut self, related_company_code: &str) -> Self {
self.is_intercompany = true;
self.intercompany_code = Some(related_company_code.to_string());
self.customer_type = CustomerType::Intercompany;
self.credit_rating = CreditRating::AAA;
self.payment_behavior = CustomerPaymentBehavior::Excellent;
self
}
pub fn with_currency(mut self, currency: &str) -> Self {
self.currency = currency.to_string();
self
}
pub fn with_sales_org(mut self, org: &str) -> Self {
self.sales_org = Some(org.to_string());
self
}
pub fn block_credit(&mut self, reason: &str) {
self.credit_blocked = true;
self.credit_block_reason = Some(reason.to_string());
}
pub fn unblock_credit(&mut self) {
self.credit_blocked = false;
self.credit_block_reason = None;
}
pub fn can_place_order(&self, order_amount: Decimal) -> bool {
if self.credit_blocked {
return false;
}
if !self.is_active {
return false;
}
self.credit_exposure + order_amount <= self.credit_limit
}
pub fn available_credit(&self) -> Decimal {
if self.credit_blocked {
Decimal::ZERO
} else {
(self.credit_limit - self.credit_exposure).max(Decimal::ZERO)
}
}
pub fn add_credit_exposure(&mut self, amount: Decimal) {
self.credit_exposure += amount;
}
pub fn reduce_credit_exposure(&mut self, amount: Decimal) {
self.credit_exposure = (self.credit_exposure - amount).max(Decimal::ZERO);
}
pub fn generate_order_amount(&self, rng: &mut impl Rng) -> Decimal {
let (min, max) = self.typical_order_range;
let range = max - min;
let random_fraction =
Decimal::from_f64_retain(rng.random::<f64>()).unwrap_or(Decimal::ZERO);
min + range * random_fraction
}
pub fn calculate_due_date(&self, invoice_date: chrono::NaiveDate) -> chrono::NaiveDate {
invoice_date + chrono::Duration::days(self.payment_terms.due_days() as i64)
}
pub fn simulate_payment_date(
&self,
due_date: chrono::NaiveDate,
rng: &mut impl Rng,
) -> chrono::NaiveDate {
let days_offset = self.payment_behavior.average_days_past_due();
let variation: i16 = rng.random_range(-5..=10);
let total_offset = days_offset + variation;
due_date + chrono::Duration::days(total_offset as i64)
}
}
#[derive(Debug, Clone, Default)]
pub struct VendorPool {
pub vendors: Vec<Vendor>,
type_index: HashMap<VendorType, Vec<usize>>,
}
impl VendorPool {
pub fn new() -> Self {
Self {
vendors: Vec::new(),
type_index: HashMap::new(),
}
}
pub fn from_vendors(vendors: Vec<Vendor>) -> Self {
let mut pool = Self::new();
for vendor in vendors {
pool.add_vendor(vendor);
}
pool
}
pub fn add_vendor(&mut self, vendor: Vendor) {
let idx = self.vendors.len();
let vendor_type = vendor.vendor_type;
self.vendors.push(vendor);
self.type_index.entry(vendor_type).or_default().push(idx);
}
pub fn random_vendor(&self, rng: &mut impl Rng) -> Option<&Vendor> {
self.vendors.choose(rng)
}
pub fn random_vendor_of_type(
&self,
vendor_type: VendorType,
rng: &mut impl Rng,
) -> Option<&Vendor> {
self.type_index
.get(&vendor_type)
.and_then(|indices| indices.choose(rng))
.map(|&idx| &self.vendors[idx])
}
pub fn rebuild_index(&mut self) {
self.type_index.clear();
for (idx, vendor) in self.vendors.iter().enumerate() {
self.type_index
.entry(vendor.vendor_type)
.or_default()
.push(idx);
}
}
pub fn standard() -> Self {
let mut pool = Self::new();
let suppliers = [
("V-000001", "Acme Supplies Inc", VendorType::Supplier),
("V-000002", "Global Materials Corp", VendorType::Supplier),
("V-000003", "Office Depot Business", VendorType::Supplier),
("V-000004", "Industrial Parts Co", VendorType::Supplier),
("V-000005", "Premium Components Ltd", VendorType::Supplier),
];
let services = [
("V-000010", "CleanCo Services", VendorType::ServiceProvider),
(
"V-000011",
"Building Maintenance Inc",
VendorType::ServiceProvider,
),
(
"V-000012",
"Security Solutions LLC",
VendorType::ServiceProvider,
),
];
let utilities = [
("V-000020", "City Electric Utility", VendorType::Utility),
("V-000021", "Natural Gas Co", VendorType::Utility),
("V-000022", "Metro Water Authority", VendorType::Utility),
("V-000023", "Telecom Network Inc", VendorType::Utility),
];
let professional = [
(
"V-000030",
"Baker & Associates LLP",
VendorType::ProfessionalServices,
),
(
"V-000031",
"PricewaterhouseCoopers",
VendorType::ProfessionalServices,
),
(
"V-000032",
"McKinsey & Company",
VendorType::ProfessionalServices,
),
(
"V-000033",
"Deloitte Consulting",
VendorType::ProfessionalServices,
),
];
let technology = [
("V-000040", "Microsoft Corporation", VendorType::Technology),
("V-000041", "Amazon Web Services", VendorType::Technology),
("V-000042", "Salesforce Inc", VendorType::Technology),
("V-000043", "SAP America Inc", VendorType::Technology),
("V-000044", "Oracle Corporation", VendorType::Technology),
("V-000045", "Adobe Systems", VendorType::Technology),
];
let logistics = [
("V-000050", "FedEx Corporation", VendorType::Logistics),
("V-000051", "UPS Shipping", VendorType::Logistics),
("V-000052", "DHL Express", VendorType::Logistics),
];
let real_estate = [
(
"V-000060",
"Commercial Properties LLC",
VendorType::RealEstate,
),
("V-000061", "CBRE Group", VendorType::RealEstate),
];
for (id, name, vtype) in suppliers {
pool.add_vendor(
Vendor::new(id, name, vtype)
.with_amount_range(Decimal::from(500), Decimal::from(50000)),
);
}
for (id, name, vtype) in services {
pool.add_vendor(
Vendor::new(id, name, vtype)
.with_amount_range(Decimal::from(200), Decimal::from(5000)),
);
}
for (id, name, vtype) in utilities {
pool.add_vendor(
Vendor::new(id, name, vtype)
.with_amount_range(Decimal::from(500), Decimal::from(20000)),
);
}
for (id, name, vtype) in professional {
pool.add_vendor(
Vendor::new(id, name, vtype)
.with_amount_range(Decimal::from(5000), Decimal::from(500000)),
);
}
for (id, name, vtype) in technology {
pool.add_vendor(
Vendor::new(id, name, vtype)
.with_amount_range(Decimal::from(100), Decimal::from(100000)),
);
}
for (id, name, vtype) in logistics {
pool.add_vendor(
Vendor::new(id, name, vtype)
.with_amount_range(Decimal::from(50), Decimal::from(10000)),
);
}
for (id, name, vtype) in real_estate {
pool.add_vendor(
Vendor::new(id, name, vtype)
.with_amount_range(Decimal::from(5000), Decimal::from(100000)),
);
}
pool
}
}
#[derive(Debug, Clone, Default)]
pub struct CustomerPool {
pub customers: Vec<Customer>,
type_index: HashMap<CustomerType, Vec<usize>>,
}
impl CustomerPool {
pub fn new() -> Self {
Self {
customers: Vec::new(),
type_index: HashMap::new(),
}
}
pub fn from_customers(customers: Vec<Customer>) -> Self {
let mut pool = Self::new();
for customer in customers {
pool.add_customer(customer);
}
pool
}
pub fn add_customer(&mut self, customer: Customer) {
let idx = self.customers.len();
let customer_type = customer.customer_type;
self.customers.push(customer);
self.type_index.entry(customer_type).or_default().push(idx);
}
pub fn random_customer(&self, rng: &mut impl Rng) -> Option<&Customer> {
self.customers.choose(rng)
}
pub fn random_customer_of_type(
&self,
customer_type: CustomerType,
rng: &mut impl Rng,
) -> Option<&Customer> {
self.type_index
.get(&customer_type)
.and_then(|indices| indices.choose(rng))
.map(|&idx| &self.customers[idx])
}
pub fn rebuild_index(&mut self) {
self.type_index.clear();
for (idx, customer) in self.customers.iter().enumerate() {
self.type_index
.entry(customer.customer_type)
.or_default()
.push(idx);
}
}
pub fn standard() -> Self {
let mut pool = Self::new();
let corporate = [
("C-000001", "Northwind Traders", CustomerType::Corporate),
("C-000002", "Contoso Corporation", CustomerType::Corporate),
("C-000003", "Adventure Works", CustomerType::Corporate),
("C-000004", "Fabrikam Industries", CustomerType::Corporate),
("C-000005", "Wide World Importers", CustomerType::Corporate),
("C-000006", "Tailspin Toys", CustomerType::Corporate),
("C-000007", "Proseware Inc", CustomerType::Corporate),
("C-000008", "Coho Vineyard", CustomerType::Corporate),
("C-000009", "Alpine Ski House", CustomerType::Corporate),
("C-000010", "VanArsdel Ltd", CustomerType::Corporate),
];
let small_business = [
("C-000020", "Smith & Co LLC", CustomerType::SmallBusiness),
(
"C-000021",
"Johnson Enterprises",
CustomerType::SmallBusiness,
),
(
"C-000022",
"Williams Consulting",
CustomerType::SmallBusiness,
),
(
"C-000023",
"Brown Brothers Shop",
CustomerType::SmallBusiness,
),
(
"C-000024",
"Davis Family Business",
CustomerType::SmallBusiness,
),
];
let government = [
(
"C-000030",
"US Federal Government",
CustomerType::Government,
),
("C-000031", "State of California", CustomerType::Government),
("C-000032", "City of New York", CustomerType::Government),
];
let distributors = [
(
"C-000040",
"National Distribution Co",
CustomerType::Distributor,
),
(
"C-000041",
"Regional Wholesale Inc",
CustomerType::Distributor,
),
(
"C-000042",
"Pacific Distributors",
CustomerType::Distributor,
),
];
for (id, name, ctype) in corporate {
pool.add_customer(
Customer::new(id, name, ctype).with_credit_limit(Decimal::from(500000)),
);
}
for (id, name, ctype) in small_business {
pool.add_customer(
Customer::new(id, name, ctype).with_credit_limit(Decimal::from(50000)),
);
}
for (id, name, ctype) in government {
pool.add_customer(
Customer::new(id, name, ctype)
.with_credit_limit(Decimal::from(1000000))
.with_payment_terms(45),
);
}
for (id, name, ctype) in distributors {
pool.add_customer(
Customer::new(id, name, ctype).with_credit_limit(Decimal::from(250000)),
);
}
pool
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use rand::SeedableRng;
use rand_chacha::ChaCha8Rng;
#[test]
fn test_vendor_creation() {
let vendor = Vendor::new("V-001", "Test Vendor", VendorType::Supplier)
.with_country("DE")
.with_payment_terms(45);
assert_eq!(vendor.vendor_id, "V-001");
assert_eq!(vendor.country, "DE");
assert_eq!(vendor.payment_terms_days, 45);
}
#[test]
fn test_vendor_pool() {
let pool = VendorPool::standard();
assert!(!pool.vendors.is_empty());
let mut rng = ChaCha8Rng::seed_from_u64(42);
let vendor = pool.random_vendor(&mut rng);
assert!(vendor.is_some());
let tech_vendor = pool.random_vendor_of_type(VendorType::Technology, &mut rng);
assert!(tech_vendor.is_some());
}
#[test]
fn test_customer_pool() {
let pool = CustomerPool::standard();
assert!(!pool.customers.is_empty());
let mut rng = ChaCha8Rng::seed_from_u64(42);
let customer = pool.random_customer(&mut rng);
assert!(customer.is_some());
}
#[test]
fn test_amount_generation() {
let mut rng = ChaCha8Rng::seed_from_u64(42);
let vendor = Vendor::new("V-001", "Test", VendorType::Supplier)
.with_amount_range(Decimal::from(100), Decimal::from(1000));
let amount = vendor.generate_amount(&mut rng);
assert!(amount >= Decimal::from(100));
assert!(amount <= Decimal::from(1000));
}
#[test]
fn test_payment_terms() {
assert_eq!(PaymentTerms::Net30.due_days(), 30);
assert_eq!(PaymentTerms::Net60.due_days(), 60);
assert!(PaymentTerms::Prepayment.requires_prepayment());
let discount = PaymentTerms::TwoTenNet30.early_payment_discount();
assert!(discount.is_some());
let (days, percent) = discount.unwrap();
assert_eq!(days, 10);
assert_eq!(percent, Decimal::from(2));
}
#[test]
fn test_credit_rating() {
assert!(
CreditRating::AAA.credit_limit_multiplier() > CreditRating::B.credit_limit_multiplier()
);
assert!(CreditRating::D.is_credit_blocked());
assert!(!CreditRating::A.is_credit_blocked());
}
#[test]
fn test_customer_credit_check() {
let mut customer = Customer::new("C-001", "Test", CustomerType::Corporate)
.with_credit_limit(Decimal::from(10000));
assert!(customer.can_place_order(Decimal::from(5000)));
customer.add_credit_exposure(Decimal::from(8000));
assert!(!customer.can_place_order(Decimal::from(5000)));
assert!(customer.can_place_order(Decimal::from(2000)));
customer.block_credit("Testing");
assert!(!customer.can_place_order(Decimal::from(100)));
}
#[test]
fn test_intercompany_vendor() {
let vendor = Vendor::new_intercompany("V-IC-001", "Subsidiary Co", "2000");
assert!(vendor.is_intercompany);
assert_eq!(vendor.intercompany_code, Some("2000".to_string()));
}
#[test]
fn test_intercompany_customer() {
let customer = Customer::new_intercompany("C-IC-001", "Parent Co", "1000");
assert!(customer.is_intercompany);
assert_eq!(customer.customer_type, CustomerType::Intercompany);
assert_eq!(customer.credit_rating, CreditRating::AAA);
}
#[test]
fn test_payment_behavior() {
assert!(CustomerPaymentBehavior::Excellent.on_time_probability() > 0.95);
assert!(CustomerPaymentBehavior::VeryPoor.on_time_probability() < 0.25);
assert!(CustomerPaymentBehavior::Excellent.average_days_past_due() < 0);
}
#[test]
fn test_vendor_due_date() {
let vendor = Vendor::new("V-001", "Test", VendorType::Supplier)
.with_payment_terms_structured(PaymentTerms::Net30);
let invoice_date = chrono::NaiveDate::from_ymd_opt(2024, 1, 15).unwrap();
let due_date = vendor.calculate_due_date(invoice_date);
assert_eq!(
due_date,
chrono::NaiveDate::from_ymd_opt(2024, 2, 14).unwrap()
);
}
}