Skip to main content

datasynth_core/models/
master_data.rs

1//! Master data models for vendors and customers.
2//!
3//! Provides realistic vendor and customer entities for transaction
4//! attribution and header/line text generation. Includes payment terms,
5//! behavioral patterns, and intercompany support for enterprise simulation.
6
7use rand::seq::SliceRandom;
8use rand::Rng;
9use rust_decimal::Decimal;
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12
13/// Payment terms for vendor/customer relationships.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
15#[serde(rename_all = "snake_case")]
16pub enum PaymentTerms {
17    /// Due immediately
18    Immediate,
19    /// Net 10 days
20    Net10,
21    /// Net 15 days
22    Net15,
23    /// Net 30 days (most common)
24    #[default]
25    Net30,
26    /// Net 45 days
27    Net45,
28    /// Net 60 days
29    Net60,
30    /// Net 90 days
31    Net90,
32    /// 2% discount if paid within 10 days, otherwise net 30
33    TwoTenNet30,
34    /// 1% discount if paid within 10 days, otherwise net 30
35    OneTenNet30,
36    /// 2% discount if paid within 15 days, otherwise net 45
37    TwoFifteenNet45,
38    /// End of month
39    EndOfMonth,
40    /// End of month plus 30 days
41    EndOfMonthPlus30,
42    /// Cash on delivery
43    CashOnDelivery,
44    /// Prepayment required
45    Prepayment,
46}
47
48impl PaymentTerms {
49    /// Get the due date offset in days from invoice date.
50    pub fn due_days(&self) -> u16 {
51        match self {
52            Self::Immediate | Self::CashOnDelivery => 0,
53            Self::Prepayment => 0,
54            Self::Net10 | Self::TwoTenNet30 | Self::OneTenNet30 => 30, // Final due date
55            Self::Net15 | Self::TwoFifteenNet45 => 45,
56            Self::Net30 => 30,
57            Self::Net45 => 45,
58            Self::Net60 => 60,
59            Self::Net90 => 90,
60            Self::EndOfMonth => 30,       // Approximate
61            Self::EndOfMonthPlus30 => 60, // Approximate
62        }
63    }
64
65    /// Get discount percentage if paid early.
66    pub fn early_payment_discount(&self) -> Option<(u16, Decimal)> {
67        match self {
68            Self::TwoTenNet30 => Some((10, Decimal::from(2))),
69            Self::OneTenNet30 => Some((10, Decimal::from(1))),
70            Self::TwoFifteenNet45 => Some((15, Decimal::from(2))),
71            _ => None,
72        }
73    }
74
75    /// Check if this requires prepayment.
76    pub fn requires_prepayment(&self) -> bool {
77        matches!(self, Self::Prepayment | Self::CashOnDelivery)
78    }
79
80    /// Get the payment terms code (for display/export).
81    pub fn code(&self) -> &'static str {
82        match self {
83            Self::Immediate => "IMM",
84            Self::Net10 => "N10",
85            Self::Net15 => "N15",
86            Self::Net30 => "N30",
87            Self::Net45 => "N45",
88            Self::Net60 => "N60",
89            Self::Net90 => "N90",
90            Self::TwoTenNet30 => "2/10N30",
91            Self::OneTenNet30 => "1/10N30",
92            Self::TwoFifteenNet45 => "2/15N45",
93            Self::EndOfMonth => "EOM",
94            Self::EndOfMonthPlus30 => "EOM30",
95            Self::CashOnDelivery => "COD",
96            Self::Prepayment => "PREP",
97        }
98    }
99
100    /// Get the net payment days.
101    pub fn net_days(&self) -> u16 {
102        self.due_days()
103    }
104
105    /// Get the discount days (days within which discount applies).
106    pub fn discount_days(&self) -> Option<u16> {
107        self.early_payment_discount().map(|(days, _)| days)
108    }
109
110    /// Get the discount percent.
111    pub fn discount_percent(&self) -> Option<Decimal> {
112        self.early_payment_discount().map(|(_, percent)| percent)
113    }
114}
115
116/// Vendor payment behavior for simulation.
117#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
118#[serde(rename_all = "snake_case")]
119pub enum VendorBehavior {
120    /// Strict - expects payment exactly on due date
121    Strict,
122    /// Flexible - accepts some late payments
123    #[default]
124    Flexible,
125    /// Very flexible - rarely follows up on late payments
126    VeryFlexible,
127    /// Aggressive - immediate follow-up on overdue
128    Aggressive,
129}
130
131impl VendorBehavior {
132    /// Get typical grace period in days beyond due date.
133    pub fn grace_period_days(&self) -> u16 {
134        match self {
135            Self::Strict => 0,
136            Self::Flexible => 7,
137            Self::VeryFlexible => 30,
138            Self::Aggressive => 0,
139        }
140    }
141}
142
143/// Customer payment behavior for simulation.
144#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
145#[serde(rename_all = "snake_case")]
146pub enum CustomerPaymentBehavior {
147    /// Excellent - always pays early or on time
148    Excellent,
149    /// Early payer (alias for Excellent)
150    EarlyPayer,
151    /// Good - usually pays on time
152    #[default]
153    Good,
154    /// On time payer (alias for Good)
155    OnTime,
156    /// Fair - sometimes late
157    Fair,
158    /// Slightly late (alias for Fair)
159    SlightlyLate,
160    /// Poor - frequently late
161    Poor,
162    /// Often late (alias for Poor)
163    OftenLate,
164    /// Very Poor - chronically delinquent
165    VeryPoor,
166    /// High risk (alias for VeryPoor)
167    HighRisk,
168}
169
170impl CustomerPaymentBehavior {
171    /// Get average days past due for this behavior.
172    pub fn average_days_past_due(&self) -> i16 {
173        match self {
174            Self::Excellent | Self::EarlyPayer => -5, // Pays early
175            Self::Good | Self::OnTime => 0,
176            Self::Fair | Self::SlightlyLate => 10,
177            Self::Poor | Self::OftenLate => 30,
178            Self::VeryPoor | Self::HighRisk => 60,
179        }
180    }
181
182    /// Get probability of payment on time.
183    pub fn on_time_probability(&self) -> f64 {
184        match self {
185            Self::Excellent | Self::EarlyPayer => 0.98,
186            Self::Good | Self::OnTime => 0.90,
187            Self::Fair | Self::SlightlyLate => 0.70,
188            Self::Poor | Self::OftenLate => 0.40,
189            Self::VeryPoor | Self::HighRisk => 0.20,
190        }
191    }
192
193    /// Get probability of taking early payment discount.
194    pub fn discount_probability(&self) -> f64 {
195        match self {
196            Self::Excellent | Self::EarlyPayer => 0.80,
197            Self::Good | Self::OnTime => 0.50,
198            Self::Fair | Self::SlightlyLate => 0.20,
199            Self::Poor | Self::OftenLate => 0.05,
200            Self::VeryPoor | Self::HighRisk => 0.01,
201        }
202    }
203}
204
205/// Customer credit rating.
206#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
207#[serde(rename_all = "snake_case")]
208pub enum CreditRating {
209    /// Excellent credit
210    AAA,
211    /// Very good credit
212    AA,
213    /// Good credit
214    #[default]
215    A,
216    /// Satisfactory credit
217    BBB,
218    /// Fair credit
219    BB,
220    /// Marginal credit
221    B,
222    /// Poor credit
223    CCC,
224    /// Very poor credit
225    CC,
226    /// Extremely poor credit
227    C,
228    /// Default/no credit
229    D,
230}
231
232impl CreditRating {
233    /// Get credit limit multiplier for this rating.
234    pub fn credit_limit_multiplier(&self) -> Decimal {
235        match self {
236            Self::AAA => Decimal::from(5),
237            Self::AA => Decimal::from(4),
238            Self::A => Decimal::from(3),
239            Self::BBB => Decimal::from(2),
240            Self::BB => Decimal::from_str_exact("1.5").unwrap_or(Decimal::from(1)),
241            Self::B => Decimal::from(1),
242            Self::CCC => Decimal::from_str_exact("0.5").unwrap_or(Decimal::from(1)),
243            Self::CC => Decimal::from_str_exact("0.25").unwrap_or(Decimal::from(0)),
244            Self::C => Decimal::from_str_exact("0.1").unwrap_or(Decimal::from(0)),
245            Self::D => Decimal::ZERO,
246        }
247    }
248
249    /// Check if credit should be blocked.
250    pub fn is_credit_blocked(&self) -> bool {
251        matches!(self, Self::D)
252    }
253}
254
255/// Bank account information for payments.
256#[derive(Debug, Clone, Serialize, Deserialize)]
257pub struct BankAccount {
258    /// Bank name
259    pub bank_name: String,
260    /// Bank country
261    pub bank_country: String,
262    /// Account number or IBAN
263    pub account_number: String,
264    /// Routing number / BIC / SWIFT
265    pub routing_code: String,
266    /// Account holder name
267    pub holder_name: String,
268    /// Is this the primary account?
269    pub is_primary: bool,
270}
271
272impl BankAccount {
273    /// Create a new bank account.
274    pub fn new(
275        bank_name: impl Into<String>,
276        account_number: impl Into<String>,
277        routing_code: impl Into<String>,
278        holder_name: impl Into<String>,
279    ) -> Self {
280        Self {
281            bank_name: bank_name.into(),
282            bank_country: "US".to_string(),
283            account_number: account_number.into(),
284            routing_code: routing_code.into(),
285            holder_name: holder_name.into(),
286            is_primary: true,
287        }
288    }
289}
290
291/// Type of vendor relationship.
292#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
293#[serde(rename_all = "snake_case")]
294pub enum VendorType {
295    /// General supplier of goods
296    #[default]
297    Supplier,
298    /// Service provider
299    ServiceProvider,
300    /// Utility company
301    Utility,
302    /// Professional services (legal, accounting, consulting)
303    ProfessionalServices,
304    /// Technology/software vendor
305    Technology,
306    /// Logistics/shipping
307    Logistics,
308    /// Contractor/freelancer
309    Contractor,
310    /// Landlord/property management
311    RealEstate,
312    /// Financial services
313    Financial,
314    /// Employee expense reimbursement
315    EmployeeReimbursement,
316}
317
318impl VendorType {
319    /// Get typical expense categories for this vendor type.
320    pub fn typical_expense_categories(&self) -> &'static [&'static str] {
321        match self {
322            Self::Supplier => &["Materials", "Inventory", "Office Supplies", "Equipment"],
323            Self::ServiceProvider => &["Services", "Maintenance", "Support"],
324            Self::Utility => &["Electricity", "Gas", "Water", "Telecommunications"],
325            Self::ProfessionalServices => &["Legal", "Audit", "Consulting", "Tax Services"],
326            Self::Technology => &["Software", "Licenses", "Cloud Services", "IT Support"],
327            Self::Logistics => &["Freight", "Shipping", "Warehousing", "Customs"],
328            Self::Contractor => &["Contract Labor", "Professional Fees", "Consulting"],
329            Self::RealEstate => &["Rent", "Property Management", "Facilities"],
330            Self::Financial => &["Bank Fees", "Interest", "Insurance", "Financing Costs"],
331            Self::EmployeeReimbursement => &["Travel", "Meals", "Entertainment", "Expenses"],
332        }
333    }
334}
335
336/// Vendor master data.
337#[derive(Debug, Clone, Serialize, Deserialize)]
338pub struct Vendor {
339    /// Vendor ID (e.g., "V-001234")
340    pub vendor_id: String,
341
342    /// Vendor name
343    pub name: String,
344
345    /// Type of vendor
346    pub vendor_type: VendorType,
347
348    /// Country code (ISO 3166-1 alpha-2)
349    pub country: String,
350
351    /// Payment terms (structured)
352    pub payment_terms: PaymentTerms,
353
354    /// Payment terms in days (legacy, computed from payment_terms)
355    pub payment_terms_days: u8,
356
357    /// Typical invoice amount range (min, max)
358    pub typical_amount_range: (Decimal, Decimal),
359
360    /// Is this vendor active
361    pub is_active: bool,
362
363    /// Vendor account number in sub-ledger
364    pub account_number: Option<String>,
365
366    /// Tax ID / VAT number
367    pub tax_id: Option<String>,
368
369    /// Bank accounts for payment
370    pub bank_accounts: Vec<BankAccount>,
371
372    /// Is this an intercompany vendor?
373    pub is_intercompany: bool,
374
375    /// Related company code (if intercompany)
376    pub intercompany_code: Option<String>,
377
378    /// Vendor behavior for payment follow-up
379    pub behavior: VendorBehavior,
380
381    /// Currency for transactions
382    pub currency: String,
383
384    /// Reconciliation account in GL
385    pub reconciliation_account: Option<String>,
386
387    /// Withholding tax applicable
388    pub withholding_tax_applicable: bool,
389
390    /// Withholding tax rate
391    pub withholding_tax_rate: Option<Decimal>,
392
393    /// One-time vendor (no master data)
394    pub is_one_time: bool,
395
396    /// Purchasing organization
397    pub purchasing_org: Option<String>,
398}
399
400impl Vendor {
401    /// Create a new vendor.
402    pub fn new(vendor_id: &str, name: &str, vendor_type: VendorType) -> Self {
403        Self {
404            vendor_id: vendor_id.to_string(),
405            name: name.to_string(),
406            vendor_type,
407            country: "US".to_string(),
408            payment_terms: PaymentTerms::Net30,
409            payment_terms_days: 30,
410            typical_amount_range: (Decimal::from(100), Decimal::from(10000)),
411            is_active: true,
412            account_number: None,
413            tax_id: None,
414            bank_accounts: Vec::new(),
415            is_intercompany: false,
416            intercompany_code: None,
417            behavior: VendorBehavior::default(),
418            currency: "USD".to_string(),
419            reconciliation_account: None,
420            withholding_tax_applicable: false,
421            withholding_tax_rate: None,
422            is_one_time: false,
423            purchasing_org: None,
424        }
425    }
426
427    /// Create an intercompany vendor.
428    pub fn new_intercompany(vendor_id: &str, name: &str, related_company_code: &str) -> Self {
429        Self::new(vendor_id, name, VendorType::Supplier).with_intercompany(related_company_code)
430    }
431
432    /// Set country.
433    pub fn with_country(mut self, country: &str) -> Self {
434        self.country = country.to_string();
435        self
436    }
437
438    /// Set structured payment terms.
439    pub fn with_payment_terms_structured(mut self, terms: PaymentTerms) -> Self {
440        self.payment_terms = terms;
441        self.payment_terms_days = terms.due_days() as u8;
442        self
443    }
444
445    /// Set payment terms (legacy, by days).
446    pub fn with_payment_terms(mut self, days: u8) -> Self {
447        self.payment_terms_days = days;
448        // Map to closest structured terms
449        self.payment_terms = match days {
450            0 => PaymentTerms::Immediate,
451            1..=15 => PaymentTerms::Net15,
452            16..=35 => PaymentTerms::Net30,
453            36..=50 => PaymentTerms::Net45,
454            51..=70 => PaymentTerms::Net60,
455            _ => PaymentTerms::Net90,
456        };
457        self
458    }
459
460    /// Set amount range.
461    pub fn with_amount_range(mut self, min: Decimal, max: Decimal) -> Self {
462        self.typical_amount_range = (min, max);
463        self
464    }
465
466    /// Set as intercompany vendor.
467    pub fn with_intercompany(mut self, related_company_code: &str) -> Self {
468        self.is_intercompany = true;
469        self.intercompany_code = Some(related_company_code.to_string());
470        self
471    }
472
473    /// Add a bank account.
474    pub fn with_bank_account(mut self, account: BankAccount) -> Self {
475        self.bank_accounts.push(account);
476        self
477    }
478
479    /// Set vendor behavior.
480    pub fn with_behavior(mut self, behavior: VendorBehavior) -> Self {
481        self.behavior = behavior;
482        self
483    }
484
485    /// Set currency.
486    pub fn with_currency(mut self, currency: &str) -> Self {
487        self.currency = currency.to_string();
488        self
489    }
490
491    /// Set reconciliation account.
492    pub fn with_reconciliation_account(mut self, account: &str) -> Self {
493        self.reconciliation_account = Some(account.to_string());
494        self
495    }
496
497    /// Set withholding tax.
498    pub fn with_withholding_tax(mut self, rate: Decimal) -> Self {
499        self.withholding_tax_applicable = true;
500        self.withholding_tax_rate = Some(rate);
501        self
502    }
503
504    /// Get the primary bank account.
505    pub fn primary_bank_account(&self) -> Option<&BankAccount> {
506        self.bank_accounts
507            .iter()
508            .find(|a| a.is_primary)
509            .or_else(|| self.bank_accounts.first())
510    }
511
512    /// Generate a random amount within the typical range.
513    pub fn generate_amount(&self, rng: &mut impl Rng) -> Decimal {
514        let (min, max) = self.typical_amount_range;
515        let range = max - min;
516        let random_fraction = Decimal::from_f64_retain(rng.gen::<f64>()).unwrap_or(Decimal::ZERO);
517        min + range * random_fraction
518    }
519
520    /// Calculate due date for an invoice.
521    pub fn calculate_due_date(&self, invoice_date: chrono::NaiveDate) -> chrono::NaiveDate {
522        invoice_date + chrono::Duration::days(self.payment_terms.due_days() as i64)
523    }
524}
525
526/// Type of customer relationship.
527#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
528#[serde(rename_all = "snake_case")]
529pub enum CustomerType {
530    /// Business-to-business customer
531    #[default]
532    Corporate,
533    /// Small/medium business
534    SmallBusiness,
535    /// Individual consumer
536    Consumer,
537    /// Government entity
538    Government,
539    /// Non-profit organization
540    NonProfit,
541    /// Intercompany (related party)
542    Intercompany,
543    /// Distributor/reseller
544    Distributor,
545}
546
547/// Customer master data.
548#[derive(Debug, Clone, Serialize, Deserialize)]
549pub struct Customer {
550    /// Customer ID (e.g., "C-001234")
551    pub customer_id: String,
552
553    /// Customer name
554    pub name: String,
555
556    /// Type of customer
557    pub customer_type: CustomerType,
558
559    /// Country code (ISO 3166-1 alpha-2)
560    pub country: String,
561
562    /// Credit rating
563    pub credit_rating: CreditRating,
564
565    /// Credit limit
566    #[serde(with = "rust_decimal::serde::str")]
567    pub credit_limit: Decimal,
568
569    /// Current credit exposure (outstanding AR)
570    #[serde(with = "rust_decimal::serde::str")]
571    pub credit_exposure: Decimal,
572
573    /// Payment terms (structured)
574    pub payment_terms: PaymentTerms,
575
576    /// Payment terms in days (legacy)
577    pub payment_terms_days: u8,
578
579    /// Payment behavior pattern
580    pub payment_behavior: CustomerPaymentBehavior,
581
582    /// Is this customer active
583    pub is_active: bool,
584
585    /// Customer account number in sub-ledger
586    pub account_number: Option<String>,
587
588    /// Typical order amount range (min, max)
589    pub typical_order_range: (Decimal, Decimal),
590
591    /// Is this an intercompany customer?
592    pub is_intercompany: bool,
593
594    /// Related company code (if intercompany)
595    pub intercompany_code: Option<String>,
596
597    /// Currency for transactions
598    pub currency: String,
599
600    /// Reconciliation account in GL
601    pub reconciliation_account: Option<String>,
602
603    /// Sales organization
604    pub sales_org: Option<String>,
605
606    /// Distribution channel
607    pub distribution_channel: Option<String>,
608
609    /// Tax ID / VAT number
610    pub tax_id: Option<String>,
611
612    /// Is credit blocked?
613    pub credit_blocked: bool,
614
615    /// Credit block reason
616    pub credit_block_reason: Option<String>,
617
618    /// Dunning procedure
619    pub dunning_procedure: Option<String>,
620
621    /// Last dunning date
622    pub last_dunning_date: Option<chrono::NaiveDate>,
623
624    /// Dunning level (0-4)
625    pub dunning_level: u8,
626}
627
628impl Customer {
629    /// Create a new customer.
630    pub fn new(customer_id: &str, name: &str, customer_type: CustomerType) -> Self {
631        Self {
632            customer_id: customer_id.to_string(),
633            name: name.to_string(),
634            customer_type,
635            country: "US".to_string(),
636            credit_rating: CreditRating::default(),
637            credit_limit: Decimal::from(100000),
638            credit_exposure: Decimal::ZERO,
639            payment_terms: PaymentTerms::Net30,
640            payment_terms_days: 30,
641            payment_behavior: CustomerPaymentBehavior::default(),
642            is_active: true,
643            account_number: None,
644            typical_order_range: (Decimal::from(500), Decimal::from(50000)),
645            is_intercompany: false,
646            intercompany_code: None,
647            currency: "USD".to_string(),
648            reconciliation_account: None,
649            sales_org: None,
650            distribution_channel: None,
651            tax_id: None,
652            credit_blocked: false,
653            credit_block_reason: None,
654            dunning_procedure: None,
655            last_dunning_date: None,
656            dunning_level: 0,
657        }
658    }
659
660    /// Create an intercompany customer.
661    pub fn new_intercompany(customer_id: &str, name: &str, related_company_code: &str) -> Self {
662        Self::new(customer_id, name, CustomerType::Intercompany)
663            .with_intercompany(related_company_code)
664    }
665
666    /// Set country.
667    pub fn with_country(mut self, country: &str) -> Self {
668        self.country = country.to_string();
669        self
670    }
671
672    /// Set credit rating.
673    pub fn with_credit_rating(mut self, rating: CreditRating) -> Self {
674        self.credit_rating = rating;
675        // Adjust credit limit based on rating
676        self.credit_limit *= rating.credit_limit_multiplier();
677        if rating.is_credit_blocked() {
678            self.credit_blocked = true;
679            self.credit_block_reason = Some("Credit rating D".to_string());
680        }
681        self
682    }
683
684    /// Set credit limit.
685    pub fn with_credit_limit(mut self, limit: Decimal) -> Self {
686        self.credit_limit = limit;
687        self
688    }
689
690    /// Set structured payment terms.
691    pub fn with_payment_terms_structured(mut self, terms: PaymentTerms) -> Self {
692        self.payment_terms = terms;
693        self.payment_terms_days = terms.due_days() as u8;
694        self
695    }
696
697    /// Set payment terms (legacy, by days).
698    pub fn with_payment_terms(mut self, days: u8) -> Self {
699        self.payment_terms_days = days;
700        self.payment_terms = match days {
701            0 => PaymentTerms::Immediate,
702            1..=15 => PaymentTerms::Net15,
703            16..=35 => PaymentTerms::Net30,
704            36..=50 => PaymentTerms::Net45,
705            51..=70 => PaymentTerms::Net60,
706            _ => PaymentTerms::Net90,
707        };
708        self
709    }
710
711    /// Set payment behavior.
712    pub fn with_payment_behavior(mut self, behavior: CustomerPaymentBehavior) -> Self {
713        self.payment_behavior = behavior;
714        self
715    }
716
717    /// Set as intercompany customer.
718    pub fn with_intercompany(mut self, related_company_code: &str) -> Self {
719        self.is_intercompany = true;
720        self.intercompany_code = Some(related_company_code.to_string());
721        self.customer_type = CustomerType::Intercompany;
722        // Intercompany customers typically have excellent credit
723        self.credit_rating = CreditRating::AAA;
724        self.payment_behavior = CustomerPaymentBehavior::Excellent;
725        self
726    }
727
728    /// Set currency.
729    pub fn with_currency(mut self, currency: &str) -> Self {
730        self.currency = currency.to_string();
731        self
732    }
733
734    /// Set sales organization.
735    pub fn with_sales_org(mut self, org: &str) -> Self {
736        self.sales_org = Some(org.to_string());
737        self
738    }
739
740    /// Block credit.
741    pub fn block_credit(&mut self, reason: &str) {
742        self.credit_blocked = true;
743        self.credit_block_reason = Some(reason.to_string());
744    }
745
746    /// Unblock credit.
747    pub fn unblock_credit(&mut self) {
748        self.credit_blocked = false;
749        self.credit_block_reason = None;
750    }
751
752    /// Check if order can be placed (credit check).
753    pub fn can_place_order(&self, order_amount: Decimal) -> bool {
754        if self.credit_blocked {
755            return false;
756        }
757        if !self.is_active {
758            return false;
759        }
760        // Check credit limit
761        self.credit_exposure + order_amount <= self.credit_limit
762    }
763
764    /// Available credit.
765    pub fn available_credit(&self) -> Decimal {
766        if self.credit_blocked {
767            Decimal::ZERO
768        } else {
769            (self.credit_limit - self.credit_exposure).max(Decimal::ZERO)
770        }
771    }
772
773    /// Update credit exposure.
774    pub fn add_credit_exposure(&mut self, amount: Decimal) {
775        self.credit_exposure += amount;
776    }
777
778    /// Reduce credit exposure (payment received).
779    pub fn reduce_credit_exposure(&mut self, amount: Decimal) {
780        self.credit_exposure = (self.credit_exposure - amount).max(Decimal::ZERO);
781    }
782
783    /// Generate a random order amount within typical range.
784    pub fn generate_order_amount(&self, rng: &mut impl Rng) -> Decimal {
785        let (min, max) = self.typical_order_range;
786        let range = max - min;
787        let random_fraction = Decimal::from_f64_retain(rng.gen::<f64>()).unwrap_or(Decimal::ZERO);
788        min + range * random_fraction
789    }
790
791    /// Calculate due date for an invoice.
792    pub fn calculate_due_date(&self, invoice_date: chrono::NaiveDate) -> chrono::NaiveDate {
793        invoice_date + chrono::Duration::days(self.payment_terms.due_days() as i64)
794    }
795
796    /// Simulate payment date based on payment behavior.
797    pub fn simulate_payment_date(
798        &self,
799        due_date: chrono::NaiveDate,
800        rng: &mut impl Rng,
801    ) -> chrono::NaiveDate {
802        let days_offset = self.payment_behavior.average_days_past_due();
803        // Add some random variation
804        let variation: i16 = rng.gen_range(-5..=10);
805        let total_offset = days_offset + variation;
806        due_date + chrono::Duration::days(total_offset as i64)
807    }
808}
809
810/// Pool of vendors for transaction generation.
811#[derive(Debug, Clone, Default)]
812pub struct VendorPool {
813    /// All vendors
814    pub vendors: Vec<Vendor>,
815    /// Index by vendor type
816    type_index: HashMap<VendorType, Vec<usize>>,
817}
818
819impl VendorPool {
820    /// Create a new empty vendor pool.
821    pub fn new() -> Self {
822        Self {
823            vendors: Vec::new(),
824            type_index: HashMap::new(),
825        }
826    }
827
828    /// Create a vendor pool from a vector of vendors.
829    ///
830    /// This is the preferred way to create a pool from generated master data,
831    /// ensuring JEs reference real entities.
832    pub fn from_vendors(vendors: Vec<Vendor>) -> Self {
833        let mut pool = Self::new();
834        for vendor in vendors {
835            pool.add_vendor(vendor);
836        }
837        pool
838    }
839
840    /// Add a vendor to the pool.
841    pub fn add_vendor(&mut self, vendor: Vendor) {
842        let idx = self.vendors.len();
843        let vendor_type = vendor.vendor_type;
844        self.vendors.push(vendor);
845        self.type_index.entry(vendor_type).or_default().push(idx);
846    }
847
848    /// Get a random vendor.
849    pub fn random_vendor(&self, rng: &mut impl Rng) -> Option<&Vendor> {
850        self.vendors.choose(rng)
851    }
852
853    /// Get a random vendor of a specific type.
854    pub fn random_vendor_of_type(
855        &self,
856        vendor_type: VendorType,
857        rng: &mut impl Rng,
858    ) -> Option<&Vendor> {
859        self.type_index
860            .get(&vendor_type)
861            .and_then(|indices| indices.choose(rng))
862            .map(|&idx| &self.vendors[idx])
863    }
864
865    /// Rebuild the type index (call after deserialization).
866    pub fn rebuild_index(&mut self) {
867        self.type_index.clear();
868        for (idx, vendor) in self.vendors.iter().enumerate() {
869            self.type_index
870                .entry(vendor.vendor_type)
871                .or_default()
872                .push(idx);
873        }
874    }
875
876    /// Generate a standard vendor pool with realistic vendors.
877    pub fn standard() -> Self {
878        let mut pool = Self::new();
879
880        // Suppliers
881        let suppliers = [
882            ("V-000001", "Acme Supplies Inc", VendorType::Supplier),
883            ("V-000002", "Global Materials Corp", VendorType::Supplier),
884            ("V-000003", "Office Depot Business", VendorType::Supplier),
885            ("V-000004", "Industrial Parts Co", VendorType::Supplier),
886            ("V-000005", "Premium Components Ltd", VendorType::Supplier),
887        ];
888
889        // Service providers
890        let services = [
891            ("V-000010", "CleanCo Services", VendorType::ServiceProvider),
892            (
893                "V-000011",
894                "Building Maintenance Inc",
895                VendorType::ServiceProvider,
896            ),
897            (
898                "V-000012",
899                "Security Solutions LLC",
900                VendorType::ServiceProvider,
901            ),
902        ];
903
904        // Utilities
905        let utilities = [
906            ("V-000020", "City Electric Utility", VendorType::Utility),
907            ("V-000021", "Natural Gas Co", VendorType::Utility),
908            ("V-000022", "Metro Water Authority", VendorType::Utility),
909            ("V-000023", "Telecom Network Inc", VendorType::Utility),
910        ];
911
912        // Professional services
913        let professional = [
914            (
915                "V-000030",
916                "Baker & Associates LLP",
917                VendorType::ProfessionalServices,
918            ),
919            (
920                "V-000031",
921                "PricewaterhouseCoopers",
922                VendorType::ProfessionalServices,
923            ),
924            (
925                "V-000032",
926                "McKinsey & Company",
927                VendorType::ProfessionalServices,
928            ),
929            (
930                "V-000033",
931                "Deloitte Consulting",
932                VendorType::ProfessionalServices,
933            ),
934        ];
935
936        // Technology
937        let technology = [
938            ("V-000040", "Microsoft Corporation", VendorType::Technology),
939            ("V-000041", "Amazon Web Services", VendorType::Technology),
940            ("V-000042", "Salesforce Inc", VendorType::Technology),
941            ("V-000043", "SAP America Inc", VendorType::Technology),
942            ("V-000044", "Oracle Corporation", VendorType::Technology),
943            ("V-000045", "Adobe Systems", VendorType::Technology),
944        ];
945
946        // Logistics
947        let logistics = [
948            ("V-000050", "FedEx Corporation", VendorType::Logistics),
949            ("V-000051", "UPS Shipping", VendorType::Logistics),
950            ("V-000052", "DHL Express", VendorType::Logistics),
951        ];
952
953        // Real estate
954        let real_estate = [
955            (
956                "V-000060",
957                "Commercial Properties LLC",
958                VendorType::RealEstate,
959            ),
960            ("V-000061", "CBRE Group", VendorType::RealEstate),
961        ];
962
963        // Add all vendors
964        for (id, name, vtype) in suppliers {
965            pool.add_vendor(
966                Vendor::new(id, name, vtype)
967                    .with_amount_range(Decimal::from(500), Decimal::from(50000)),
968            );
969        }
970
971        for (id, name, vtype) in services {
972            pool.add_vendor(
973                Vendor::new(id, name, vtype)
974                    .with_amount_range(Decimal::from(200), Decimal::from(5000)),
975            );
976        }
977
978        for (id, name, vtype) in utilities {
979            pool.add_vendor(
980                Vendor::new(id, name, vtype)
981                    .with_amount_range(Decimal::from(500), Decimal::from(20000)),
982            );
983        }
984
985        for (id, name, vtype) in professional {
986            pool.add_vendor(
987                Vendor::new(id, name, vtype)
988                    .with_amount_range(Decimal::from(5000), Decimal::from(500000)),
989            );
990        }
991
992        for (id, name, vtype) in technology {
993            pool.add_vendor(
994                Vendor::new(id, name, vtype)
995                    .with_amount_range(Decimal::from(100), Decimal::from(100000)),
996            );
997        }
998
999        for (id, name, vtype) in logistics {
1000            pool.add_vendor(
1001                Vendor::new(id, name, vtype)
1002                    .with_amount_range(Decimal::from(50), Decimal::from(10000)),
1003            );
1004        }
1005
1006        for (id, name, vtype) in real_estate {
1007            pool.add_vendor(
1008                Vendor::new(id, name, vtype)
1009                    .with_amount_range(Decimal::from(5000), Decimal::from(100000)),
1010            );
1011        }
1012
1013        pool
1014    }
1015}
1016
1017/// Pool of customers for transaction generation.
1018#[derive(Debug, Clone, Default)]
1019pub struct CustomerPool {
1020    /// All customers
1021    pub customers: Vec<Customer>,
1022    /// Index by customer type
1023    type_index: HashMap<CustomerType, Vec<usize>>,
1024}
1025
1026impl CustomerPool {
1027    /// Create a new empty customer pool.
1028    pub fn new() -> Self {
1029        Self {
1030            customers: Vec::new(),
1031            type_index: HashMap::new(),
1032        }
1033    }
1034
1035    /// Create a customer pool from a vector of customers.
1036    ///
1037    /// This is the preferred way to create a pool from generated master data,
1038    /// ensuring JEs reference real entities.
1039    pub fn from_customers(customers: Vec<Customer>) -> Self {
1040        let mut pool = Self::new();
1041        for customer in customers {
1042            pool.add_customer(customer);
1043        }
1044        pool
1045    }
1046
1047    /// Add a customer to the pool.
1048    pub fn add_customer(&mut self, customer: Customer) {
1049        let idx = self.customers.len();
1050        let customer_type = customer.customer_type;
1051        self.customers.push(customer);
1052        self.type_index.entry(customer_type).or_default().push(idx);
1053    }
1054
1055    /// Get a random customer.
1056    pub fn random_customer(&self, rng: &mut impl Rng) -> Option<&Customer> {
1057        self.customers.choose(rng)
1058    }
1059
1060    /// Get a random customer of a specific type.
1061    pub fn random_customer_of_type(
1062        &self,
1063        customer_type: CustomerType,
1064        rng: &mut impl Rng,
1065    ) -> Option<&Customer> {
1066        self.type_index
1067            .get(&customer_type)
1068            .and_then(|indices| indices.choose(rng))
1069            .map(|&idx| &self.customers[idx])
1070    }
1071
1072    /// Rebuild the type index.
1073    pub fn rebuild_index(&mut self) {
1074        self.type_index.clear();
1075        for (idx, customer) in self.customers.iter().enumerate() {
1076            self.type_index
1077                .entry(customer.customer_type)
1078                .or_default()
1079                .push(idx);
1080        }
1081    }
1082
1083    /// Generate a standard customer pool.
1084    pub fn standard() -> Self {
1085        let mut pool = Self::new();
1086
1087        // Corporate customers
1088        let corporate = [
1089            ("C-000001", "Northwind Traders", CustomerType::Corporate),
1090            ("C-000002", "Contoso Corporation", CustomerType::Corporate),
1091            ("C-000003", "Adventure Works", CustomerType::Corporate),
1092            ("C-000004", "Fabrikam Industries", CustomerType::Corporate),
1093            ("C-000005", "Wide World Importers", CustomerType::Corporate),
1094            ("C-000006", "Tailspin Toys", CustomerType::Corporate),
1095            ("C-000007", "Proseware Inc", CustomerType::Corporate),
1096            ("C-000008", "Coho Vineyard", CustomerType::Corporate),
1097            ("C-000009", "Alpine Ski House", CustomerType::Corporate),
1098            ("C-000010", "VanArsdel Ltd", CustomerType::Corporate),
1099        ];
1100
1101        // Small business
1102        let small_business = [
1103            ("C-000020", "Smith & Co LLC", CustomerType::SmallBusiness),
1104            (
1105                "C-000021",
1106                "Johnson Enterprises",
1107                CustomerType::SmallBusiness,
1108            ),
1109            (
1110                "C-000022",
1111                "Williams Consulting",
1112                CustomerType::SmallBusiness,
1113            ),
1114            (
1115                "C-000023",
1116                "Brown Brothers Shop",
1117                CustomerType::SmallBusiness,
1118            ),
1119            (
1120                "C-000024",
1121                "Davis Family Business",
1122                CustomerType::SmallBusiness,
1123            ),
1124        ];
1125
1126        // Government
1127        let government = [
1128            (
1129                "C-000030",
1130                "US Federal Government",
1131                CustomerType::Government,
1132            ),
1133            ("C-000031", "State of California", CustomerType::Government),
1134            ("C-000032", "City of New York", CustomerType::Government),
1135        ];
1136
1137        // Distributors
1138        let distributors = [
1139            (
1140                "C-000040",
1141                "National Distribution Co",
1142                CustomerType::Distributor,
1143            ),
1144            (
1145                "C-000041",
1146                "Regional Wholesale Inc",
1147                CustomerType::Distributor,
1148            ),
1149            (
1150                "C-000042",
1151                "Pacific Distributors",
1152                CustomerType::Distributor,
1153            ),
1154        ];
1155
1156        for (id, name, ctype) in corporate {
1157            pool.add_customer(
1158                Customer::new(id, name, ctype).with_credit_limit(Decimal::from(500000)),
1159            );
1160        }
1161
1162        for (id, name, ctype) in small_business {
1163            pool.add_customer(
1164                Customer::new(id, name, ctype).with_credit_limit(Decimal::from(50000)),
1165            );
1166        }
1167
1168        for (id, name, ctype) in government {
1169            pool.add_customer(
1170                Customer::new(id, name, ctype)
1171                    .with_credit_limit(Decimal::from(1000000))
1172                    .with_payment_terms(45),
1173            );
1174        }
1175
1176        for (id, name, ctype) in distributors {
1177            pool.add_customer(
1178                Customer::new(id, name, ctype).with_credit_limit(Decimal::from(250000)),
1179            );
1180        }
1181
1182        pool
1183    }
1184}
1185
1186#[cfg(test)]
1187mod tests {
1188    use super::*;
1189    use rand::SeedableRng;
1190    use rand_chacha::ChaCha8Rng;
1191
1192    #[test]
1193    fn test_vendor_creation() {
1194        let vendor = Vendor::new("V-001", "Test Vendor", VendorType::Supplier)
1195            .with_country("DE")
1196            .with_payment_terms(45);
1197
1198        assert_eq!(vendor.vendor_id, "V-001");
1199        assert_eq!(vendor.country, "DE");
1200        assert_eq!(vendor.payment_terms_days, 45);
1201    }
1202
1203    #[test]
1204    fn test_vendor_pool() {
1205        let pool = VendorPool::standard();
1206
1207        assert!(!pool.vendors.is_empty());
1208
1209        let mut rng = ChaCha8Rng::seed_from_u64(42);
1210        let vendor = pool.random_vendor(&mut rng);
1211        assert!(vendor.is_some());
1212
1213        let tech_vendor = pool.random_vendor_of_type(VendorType::Technology, &mut rng);
1214        assert!(tech_vendor.is_some());
1215    }
1216
1217    #[test]
1218    fn test_customer_pool() {
1219        let pool = CustomerPool::standard();
1220
1221        assert!(!pool.customers.is_empty());
1222
1223        let mut rng = ChaCha8Rng::seed_from_u64(42);
1224        let customer = pool.random_customer(&mut rng);
1225        assert!(customer.is_some());
1226    }
1227
1228    #[test]
1229    fn test_amount_generation() {
1230        let mut rng = ChaCha8Rng::seed_from_u64(42);
1231        let vendor = Vendor::new("V-001", "Test", VendorType::Supplier)
1232            .with_amount_range(Decimal::from(100), Decimal::from(1000));
1233
1234        let amount = vendor.generate_amount(&mut rng);
1235        assert!(amount >= Decimal::from(100));
1236        assert!(amount <= Decimal::from(1000));
1237    }
1238
1239    #[test]
1240    fn test_payment_terms() {
1241        assert_eq!(PaymentTerms::Net30.due_days(), 30);
1242        assert_eq!(PaymentTerms::Net60.due_days(), 60);
1243        assert!(PaymentTerms::Prepayment.requires_prepayment());
1244
1245        let discount = PaymentTerms::TwoTenNet30.early_payment_discount();
1246        assert!(discount.is_some());
1247        let (days, percent) = discount.unwrap();
1248        assert_eq!(days, 10);
1249        assert_eq!(percent, Decimal::from(2));
1250    }
1251
1252    #[test]
1253    fn test_credit_rating() {
1254        assert!(
1255            CreditRating::AAA.credit_limit_multiplier() > CreditRating::B.credit_limit_multiplier()
1256        );
1257        assert!(CreditRating::D.is_credit_blocked());
1258        assert!(!CreditRating::A.is_credit_blocked());
1259    }
1260
1261    #[test]
1262    fn test_customer_credit_check() {
1263        let mut customer = Customer::new("C-001", "Test", CustomerType::Corporate)
1264            .with_credit_limit(Decimal::from(10000));
1265
1266        // Should be able to place order within limit
1267        assert!(customer.can_place_order(Decimal::from(5000)));
1268
1269        // Add some exposure
1270        customer.add_credit_exposure(Decimal::from(8000));
1271
1272        // Now should fail for large order
1273        assert!(!customer.can_place_order(Decimal::from(5000)));
1274
1275        // But small order should work
1276        assert!(customer.can_place_order(Decimal::from(2000)));
1277
1278        // Block credit
1279        customer.block_credit("Testing");
1280        assert!(!customer.can_place_order(Decimal::from(100)));
1281    }
1282
1283    #[test]
1284    fn test_intercompany_vendor() {
1285        let vendor = Vendor::new_intercompany("V-IC-001", "Subsidiary Co", "2000");
1286
1287        assert!(vendor.is_intercompany);
1288        assert_eq!(vendor.intercompany_code, Some("2000".to_string()));
1289    }
1290
1291    #[test]
1292    fn test_intercompany_customer() {
1293        let customer = Customer::new_intercompany("C-IC-001", "Parent Co", "1000");
1294
1295        assert!(customer.is_intercompany);
1296        assert_eq!(customer.customer_type, CustomerType::Intercompany);
1297        assert_eq!(customer.credit_rating, CreditRating::AAA);
1298    }
1299
1300    #[test]
1301    fn test_payment_behavior() {
1302        assert!(CustomerPaymentBehavior::Excellent.on_time_probability() > 0.95);
1303        assert!(CustomerPaymentBehavior::VeryPoor.on_time_probability() < 0.25);
1304        assert!(CustomerPaymentBehavior::Excellent.average_days_past_due() < 0);
1305        // Pays early
1306    }
1307
1308    #[test]
1309    fn test_vendor_due_date() {
1310        let vendor = Vendor::new("V-001", "Test", VendorType::Supplier)
1311            .with_payment_terms_structured(PaymentTerms::Net30);
1312
1313        let invoice_date = chrono::NaiveDate::from_ymd_opt(2024, 1, 15).unwrap();
1314        let due_date = vendor.calculate_due_date(invoice_date);
1315
1316        assert_eq!(
1317            due_date,
1318            chrono::NaiveDate::from_ymd_opt(2024, 2, 14).unwrap()
1319        );
1320    }
1321}