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::IndexedRandom;
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 =
517            Decimal::from_f64_retain(rng.random::<f64>()).unwrap_or(Decimal::ZERO);
518        min + range * random_fraction
519    }
520
521    /// Calculate due date for an invoice.
522    pub fn calculate_due_date(&self, invoice_date: chrono::NaiveDate) -> chrono::NaiveDate {
523        invoice_date + chrono::Duration::days(self.payment_terms.due_days() as i64)
524    }
525}
526
527/// Type of customer relationship.
528#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
529#[serde(rename_all = "snake_case")]
530pub enum CustomerType {
531    /// Business-to-business customer
532    #[default]
533    Corporate,
534    /// Small/medium business
535    SmallBusiness,
536    /// Individual consumer
537    Consumer,
538    /// Government entity
539    Government,
540    /// Non-profit organization
541    NonProfit,
542    /// Intercompany (related party)
543    Intercompany,
544    /// Distributor/reseller
545    Distributor,
546}
547
548/// Customer master data.
549#[derive(Debug, Clone, Serialize, Deserialize)]
550pub struct Customer {
551    /// Customer ID (e.g., "C-001234")
552    pub customer_id: String,
553
554    /// Customer name
555    pub name: String,
556
557    /// Type of customer
558    pub customer_type: CustomerType,
559
560    /// Country code (ISO 3166-1 alpha-2)
561    pub country: String,
562
563    /// Credit rating
564    pub credit_rating: CreditRating,
565
566    /// Credit limit
567    #[serde(with = "rust_decimal::serde::str")]
568    pub credit_limit: Decimal,
569
570    /// Current credit exposure (outstanding AR)
571    #[serde(with = "rust_decimal::serde::str")]
572    pub credit_exposure: Decimal,
573
574    /// Payment terms (structured)
575    pub payment_terms: PaymentTerms,
576
577    /// Payment terms in days (legacy)
578    pub payment_terms_days: u8,
579
580    /// Payment behavior pattern
581    pub payment_behavior: CustomerPaymentBehavior,
582
583    /// Is this customer active
584    pub is_active: bool,
585
586    /// Customer account number in sub-ledger
587    pub account_number: Option<String>,
588
589    /// Typical order amount range (min, max)
590    pub typical_order_range: (Decimal, Decimal),
591
592    /// Is this an intercompany customer?
593    pub is_intercompany: bool,
594
595    /// Related company code (if intercompany)
596    pub intercompany_code: Option<String>,
597
598    /// Currency for transactions
599    pub currency: String,
600
601    /// Reconciliation account in GL
602    pub reconciliation_account: Option<String>,
603
604    /// Sales organization
605    pub sales_org: Option<String>,
606
607    /// Distribution channel
608    pub distribution_channel: Option<String>,
609
610    /// Tax ID / VAT number
611    pub tax_id: Option<String>,
612
613    /// Is credit blocked?
614    pub credit_blocked: bool,
615
616    /// Credit block reason
617    pub credit_block_reason: Option<String>,
618
619    /// Dunning procedure
620    pub dunning_procedure: Option<String>,
621
622    /// Last dunning date
623    pub last_dunning_date: Option<chrono::NaiveDate>,
624
625    /// Dunning level (0-4)
626    pub dunning_level: u8,
627}
628
629impl Customer {
630    /// Create a new customer.
631    pub fn new(customer_id: &str, name: &str, customer_type: CustomerType) -> Self {
632        Self {
633            customer_id: customer_id.to_string(),
634            name: name.to_string(),
635            customer_type,
636            country: "US".to_string(),
637            credit_rating: CreditRating::default(),
638            credit_limit: Decimal::from(100000),
639            credit_exposure: Decimal::ZERO,
640            payment_terms: PaymentTerms::Net30,
641            payment_terms_days: 30,
642            payment_behavior: CustomerPaymentBehavior::default(),
643            is_active: true,
644            account_number: None,
645            typical_order_range: (Decimal::from(500), Decimal::from(50000)),
646            is_intercompany: false,
647            intercompany_code: None,
648            currency: "USD".to_string(),
649            reconciliation_account: None,
650            sales_org: None,
651            distribution_channel: None,
652            tax_id: None,
653            credit_blocked: false,
654            credit_block_reason: None,
655            dunning_procedure: None,
656            last_dunning_date: None,
657            dunning_level: 0,
658        }
659    }
660
661    /// Create an intercompany customer.
662    pub fn new_intercompany(customer_id: &str, name: &str, related_company_code: &str) -> Self {
663        Self::new(customer_id, name, CustomerType::Intercompany)
664            .with_intercompany(related_company_code)
665    }
666
667    /// Set country.
668    pub fn with_country(mut self, country: &str) -> Self {
669        self.country = country.to_string();
670        self
671    }
672
673    /// Set credit rating.
674    pub fn with_credit_rating(mut self, rating: CreditRating) -> Self {
675        self.credit_rating = rating;
676        // Adjust credit limit based on rating
677        self.credit_limit *= rating.credit_limit_multiplier();
678        if rating.is_credit_blocked() {
679            self.credit_blocked = true;
680            self.credit_block_reason = Some("Credit rating D".to_string());
681        }
682        self
683    }
684
685    /// Set credit limit.
686    pub fn with_credit_limit(mut self, limit: Decimal) -> Self {
687        self.credit_limit = limit;
688        self
689    }
690
691    /// Set structured payment terms.
692    pub fn with_payment_terms_structured(mut self, terms: PaymentTerms) -> Self {
693        self.payment_terms = terms;
694        self.payment_terms_days = terms.due_days() as u8;
695        self
696    }
697
698    /// Set payment terms (legacy, by days).
699    pub fn with_payment_terms(mut self, days: u8) -> Self {
700        self.payment_terms_days = days;
701        self.payment_terms = match days {
702            0 => PaymentTerms::Immediate,
703            1..=15 => PaymentTerms::Net15,
704            16..=35 => PaymentTerms::Net30,
705            36..=50 => PaymentTerms::Net45,
706            51..=70 => PaymentTerms::Net60,
707            _ => PaymentTerms::Net90,
708        };
709        self
710    }
711
712    /// Set payment behavior.
713    pub fn with_payment_behavior(mut self, behavior: CustomerPaymentBehavior) -> Self {
714        self.payment_behavior = behavior;
715        self
716    }
717
718    /// Set as intercompany customer.
719    pub fn with_intercompany(mut self, related_company_code: &str) -> Self {
720        self.is_intercompany = true;
721        self.intercompany_code = Some(related_company_code.to_string());
722        self.customer_type = CustomerType::Intercompany;
723        // Intercompany customers typically have excellent credit
724        self.credit_rating = CreditRating::AAA;
725        self.payment_behavior = CustomerPaymentBehavior::Excellent;
726        self
727    }
728
729    /// Set currency.
730    pub fn with_currency(mut self, currency: &str) -> Self {
731        self.currency = currency.to_string();
732        self
733    }
734
735    /// Set sales organization.
736    pub fn with_sales_org(mut self, org: &str) -> Self {
737        self.sales_org = Some(org.to_string());
738        self
739    }
740
741    /// Block credit.
742    pub fn block_credit(&mut self, reason: &str) {
743        self.credit_blocked = true;
744        self.credit_block_reason = Some(reason.to_string());
745    }
746
747    /// Unblock credit.
748    pub fn unblock_credit(&mut self) {
749        self.credit_blocked = false;
750        self.credit_block_reason = None;
751    }
752
753    /// Check if order can be placed (credit check).
754    pub fn can_place_order(&self, order_amount: Decimal) -> bool {
755        if self.credit_blocked {
756            return false;
757        }
758        if !self.is_active {
759            return false;
760        }
761        // Check credit limit
762        self.credit_exposure + order_amount <= self.credit_limit
763    }
764
765    /// Available credit.
766    pub fn available_credit(&self) -> Decimal {
767        if self.credit_blocked {
768            Decimal::ZERO
769        } else {
770            (self.credit_limit - self.credit_exposure).max(Decimal::ZERO)
771        }
772    }
773
774    /// Update credit exposure.
775    pub fn add_credit_exposure(&mut self, amount: Decimal) {
776        self.credit_exposure += amount;
777    }
778
779    /// Reduce credit exposure (payment received).
780    pub fn reduce_credit_exposure(&mut self, amount: Decimal) {
781        self.credit_exposure = (self.credit_exposure - amount).max(Decimal::ZERO);
782    }
783
784    /// Generate a random order amount within typical range.
785    pub fn generate_order_amount(&self, rng: &mut impl Rng) -> Decimal {
786        let (min, max) = self.typical_order_range;
787        let range = max - min;
788        let random_fraction =
789            Decimal::from_f64_retain(rng.random::<f64>()).unwrap_or(Decimal::ZERO);
790        min + range * random_fraction
791    }
792
793    /// Calculate due date for an invoice.
794    pub fn calculate_due_date(&self, invoice_date: chrono::NaiveDate) -> chrono::NaiveDate {
795        invoice_date + chrono::Duration::days(self.payment_terms.due_days() as i64)
796    }
797
798    /// Simulate payment date based on payment behavior.
799    pub fn simulate_payment_date(
800        &self,
801        due_date: chrono::NaiveDate,
802        rng: &mut impl Rng,
803    ) -> chrono::NaiveDate {
804        let days_offset = self.payment_behavior.average_days_past_due();
805        // Add some random variation
806        let variation: i16 = rng.random_range(-5..=10);
807        let total_offset = days_offset + variation;
808        due_date + chrono::Duration::days(total_offset as i64)
809    }
810}
811
812/// Pool of vendors for transaction generation.
813#[derive(Debug, Clone, Default)]
814pub struct VendorPool {
815    /// All vendors
816    pub vendors: Vec<Vendor>,
817    /// Index by vendor type
818    type_index: HashMap<VendorType, Vec<usize>>,
819}
820
821impl VendorPool {
822    /// Create a new empty vendor pool.
823    pub fn new() -> Self {
824        Self {
825            vendors: Vec::new(),
826            type_index: HashMap::new(),
827        }
828    }
829
830    /// Create a vendor pool from a vector of vendors.
831    ///
832    /// This is the preferred way to create a pool from generated master data,
833    /// ensuring JEs reference real entities.
834    pub fn from_vendors(vendors: Vec<Vendor>) -> Self {
835        let mut pool = Self::new();
836        for vendor in vendors {
837            pool.add_vendor(vendor);
838        }
839        pool
840    }
841
842    /// Add a vendor to the pool.
843    pub fn add_vendor(&mut self, vendor: Vendor) {
844        let idx = self.vendors.len();
845        let vendor_type = vendor.vendor_type;
846        self.vendors.push(vendor);
847        self.type_index.entry(vendor_type).or_default().push(idx);
848    }
849
850    /// Get a random vendor.
851    pub fn random_vendor(&self, rng: &mut impl Rng) -> Option<&Vendor> {
852        self.vendors.choose(rng)
853    }
854
855    /// Get a random vendor of a specific type.
856    pub fn random_vendor_of_type(
857        &self,
858        vendor_type: VendorType,
859        rng: &mut impl Rng,
860    ) -> Option<&Vendor> {
861        self.type_index
862            .get(&vendor_type)
863            .and_then(|indices| indices.choose(rng))
864            .map(|&idx| &self.vendors[idx])
865    }
866
867    /// Rebuild the type index (call after deserialization).
868    pub fn rebuild_index(&mut self) {
869        self.type_index.clear();
870        for (idx, vendor) in self.vendors.iter().enumerate() {
871            self.type_index
872                .entry(vendor.vendor_type)
873                .or_default()
874                .push(idx);
875        }
876    }
877
878    /// Generate a standard vendor pool with realistic vendors.
879    pub fn standard() -> Self {
880        let mut pool = Self::new();
881
882        // Suppliers
883        let suppliers = [
884            ("V-000001", "Acme Supplies Inc", VendorType::Supplier),
885            ("V-000002", "Global Materials Corp", VendorType::Supplier),
886            ("V-000003", "Office Depot Business", VendorType::Supplier),
887            ("V-000004", "Industrial Parts Co", VendorType::Supplier),
888            ("V-000005", "Premium Components Ltd", VendorType::Supplier),
889        ];
890
891        // Service providers
892        let services = [
893            ("V-000010", "CleanCo Services", VendorType::ServiceProvider),
894            (
895                "V-000011",
896                "Building Maintenance Inc",
897                VendorType::ServiceProvider,
898            ),
899            (
900                "V-000012",
901                "Security Solutions LLC",
902                VendorType::ServiceProvider,
903            ),
904        ];
905
906        // Utilities
907        let utilities = [
908            ("V-000020", "City Electric Utility", VendorType::Utility),
909            ("V-000021", "Natural Gas Co", VendorType::Utility),
910            ("V-000022", "Metro Water Authority", VendorType::Utility),
911            ("V-000023", "Telecom Network Inc", VendorType::Utility),
912        ];
913
914        // Professional services
915        let professional = [
916            (
917                "V-000030",
918                "Baker & Associates LLP",
919                VendorType::ProfessionalServices,
920            ),
921            (
922                "V-000031",
923                "PricewaterhouseCoopers",
924                VendorType::ProfessionalServices,
925            ),
926            (
927                "V-000032",
928                "McKinsey & Company",
929                VendorType::ProfessionalServices,
930            ),
931            (
932                "V-000033",
933                "Deloitte Consulting",
934                VendorType::ProfessionalServices,
935            ),
936        ];
937
938        // Technology
939        let technology = [
940            ("V-000040", "Microsoft Corporation", VendorType::Technology),
941            ("V-000041", "Amazon Web Services", VendorType::Technology),
942            ("V-000042", "Salesforce Inc", VendorType::Technology),
943            ("V-000043", "SAP America Inc", VendorType::Technology),
944            ("V-000044", "Oracle Corporation", VendorType::Technology),
945            ("V-000045", "Adobe Systems", VendorType::Technology),
946        ];
947
948        // Logistics
949        let logistics = [
950            ("V-000050", "FedEx Corporation", VendorType::Logistics),
951            ("V-000051", "UPS Shipping", VendorType::Logistics),
952            ("V-000052", "DHL Express", VendorType::Logistics),
953        ];
954
955        // Real estate
956        let real_estate = [
957            (
958                "V-000060",
959                "Commercial Properties LLC",
960                VendorType::RealEstate,
961            ),
962            ("V-000061", "CBRE Group", VendorType::RealEstate),
963        ];
964
965        // Add all vendors
966        for (id, name, vtype) in suppliers {
967            pool.add_vendor(
968                Vendor::new(id, name, vtype)
969                    .with_amount_range(Decimal::from(500), Decimal::from(50000)),
970            );
971        }
972
973        for (id, name, vtype) in services {
974            pool.add_vendor(
975                Vendor::new(id, name, vtype)
976                    .with_amount_range(Decimal::from(200), Decimal::from(5000)),
977            );
978        }
979
980        for (id, name, vtype) in utilities {
981            pool.add_vendor(
982                Vendor::new(id, name, vtype)
983                    .with_amount_range(Decimal::from(500), Decimal::from(20000)),
984            );
985        }
986
987        for (id, name, vtype) in professional {
988            pool.add_vendor(
989                Vendor::new(id, name, vtype)
990                    .with_amount_range(Decimal::from(5000), Decimal::from(500000)),
991            );
992        }
993
994        for (id, name, vtype) in technology {
995            pool.add_vendor(
996                Vendor::new(id, name, vtype)
997                    .with_amount_range(Decimal::from(100), Decimal::from(100000)),
998            );
999        }
1000
1001        for (id, name, vtype) in logistics {
1002            pool.add_vendor(
1003                Vendor::new(id, name, vtype)
1004                    .with_amount_range(Decimal::from(50), Decimal::from(10000)),
1005            );
1006        }
1007
1008        for (id, name, vtype) in real_estate {
1009            pool.add_vendor(
1010                Vendor::new(id, name, vtype)
1011                    .with_amount_range(Decimal::from(5000), Decimal::from(100000)),
1012            );
1013        }
1014
1015        pool
1016    }
1017}
1018
1019/// Pool of customers for transaction generation.
1020#[derive(Debug, Clone, Default)]
1021pub struct CustomerPool {
1022    /// All customers
1023    pub customers: Vec<Customer>,
1024    /// Index by customer type
1025    type_index: HashMap<CustomerType, Vec<usize>>,
1026}
1027
1028impl CustomerPool {
1029    /// Create a new empty customer pool.
1030    pub fn new() -> Self {
1031        Self {
1032            customers: Vec::new(),
1033            type_index: HashMap::new(),
1034        }
1035    }
1036
1037    /// Create a customer pool from a vector of customers.
1038    ///
1039    /// This is the preferred way to create a pool from generated master data,
1040    /// ensuring JEs reference real entities.
1041    pub fn from_customers(customers: Vec<Customer>) -> Self {
1042        let mut pool = Self::new();
1043        for customer in customers {
1044            pool.add_customer(customer);
1045        }
1046        pool
1047    }
1048
1049    /// Add a customer to the pool.
1050    pub fn add_customer(&mut self, customer: Customer) {
1051        let idx = self.customers.len();
1052        let customer_type = customer.customer_type;
1053        self.customers.push(customer);
1054        self.type_index.entry(customer_type).or_default().push(idx);
1055    }
1056
1057    /// Get a random customer.
1058    pub fn random_customer(&self, rng: &mut impl Rng) -> Option<&Customer> {
1059        self.customers.choose(rng)
1060    }
1061
1062    /// Get a random customer of a specific type.
1063    pub fn random_customer_of_type(
1064        &self,
1065        customer_type: CustomerType,
1066        rng: &mut impl Rng,
1067    ) -> Option<&Customer> {
1068        self.type_index
1069            .get(&customer_type)
1070            .and_then(|indices| indices.choose(rng))
1071            .map(|&idx| &self.customers[idx])
1072    }
1073
1074    /// Rebuild the type index.
1075    pub fn rebuild_index(&mut self) {
1076        self.type_index.clear();
1077        for (idx, customer) in self.customers.iter().enumerate() {
1078            self.type_index
1079                .entry(customer.customer_type)
1080                .or_default()
1081                .push(idx);
1082        }
1083    }
1084
1085    /// Generate a standard customer pool.
1086    pub fn standard() -> Self {
1087        let mut pool = Self::new();
1088
1089        // Corporate customers
1090        let corporate = [
1091            ("C-000001", "Northwind Traders", CustomerType::Corporate),
1092            ("C-000002", "Contoso Corporation", CustomerType::Corporate),
1093            ("C-000003", "Adventure Works", CustomerType::Corporate),
1094            ("C-000004", "Fabrikam Industries", CustomerType::Corporate),
1095            ("C-000005", "Wide World Importers", CustomerType::Corporate),
1096            ("C-000006", "Tailspin Toys", CustomerType::Corporate),
1097            ("C-000007", "Proseware Inc", CustomerType::Corporate),
1098            ("C-000008", "Coho Vineyard", CustomerType::Corporate),
1099            ("C-000009", "Alpine Ski House", CustomerType::Corporate),
1100            ("C-000010", "VanArsdel Ltd", CustomerType::Corporate),
1101        ];
1102
1103        // Small business
1104        let small_business = [
1105            ("C-000020", "Smith & Co LLC", CustomerType::SmallBusiness),
1106            (
1107                "C-000021",
1108                "Johnson Enterprises",
1109                CustomerType::SmallBusiness,
1110            ),
1111            (
1112                "C-000022",
1113                "Williams Consulting",
1114                CustomerType::SmallBusiness,
1115            ),
1116            (
1117                "C-000023",
1118                "Brown Brothers Shop",
1119                CustomerType::SmallBusiness,
1120            ),
1121            (
1122                "C-000024",
1123                "Davis Family Business",
1124                CustomerType::SmallBusiness,
1125            ),
1126        ];
1127
1128        // Government
1129        let government = [
1130            (
1131                "C-000030",
1132                "US Federal Government",
1133                CustomerType::Government,
1134            ),
1135            ("C-000031", "State of California", CustomerType::Government),
1136            ("C-000032", "City of New York", CustomerType::Government),
1137        ];
1138
1139        // Distributors
1140        let distributors = [
1141            (
1142                "C-000040",
1143                "National Distribution Co",
1144                CustomerType::Distributor,
1145            ),
1146            (
1147                "C-000041",
1148                "Regional Wholesale Inc",
1149                CustomerType::Distributor,
1150            ),
1151            (
1152                "C-000042",
1153                "Pacific Distributors",
1154                CustomerType::Distributor,
1155            ),
1156        ];
1157
1158        for (id, name, ctype) in corporate {
1159            pool.add_customer(
1160                Customer::new(id, name, ctype).with_credit_limit(Decimal::from(500000)),
1161            );
1162        }
1163
1164        for (id, name, ctype) in small_business {
1165            pool.add_customer(
1166                Customer::new(id, name, ctype).with_credit_limit(Decimal::from(50000)),
1167            );
1168        }
1169
1170        for (id, name, ctype) in government {
1171            pool.add_customer(
1172                Customer::new(id, name, ctype)
1173                    .with_credit_limit(Decimal::from(1000000))
1174                    .with_payment_terms(45),
1175            );
1176        }
1177
1178        for (id, name, ctype) in distributors {
1179            pool.add_customer(
1180                Customer::new(id, name, ctype).with_credit_limit(Decimal::from(250000)),
1181            );
1182        }
1183
1184        pool
1185    }
1186}
1187
1188#[cfg(test)]
1189#[allow(clippy::unwrap_used)]
1190mod tests {
1191    use super::*;
1192    use rand::SeedableRng;
1193    use rand_chacha::ChaCha8Rng;
1194
1195    #[test]
1196    fn test_vendor_creation() {
1197        let vendor = Vendor::new("V-001", "Test Vendor", VendorType::Supplier)
1198            .with_country("DE")
1199            .with_payment_terms(45);
1200
1201        assert_eq!(vendor.vendor_id, "V-001");
1202        assert_eq!(vendor.country, "DE");
1203        assert_eq!(vendor.payment_terms_days, 45);
1204    }
1205
1206    #[test]
1207    fn test_vendor_pool() {
1208        let pool = VendorPool::standard();
1209
1210        assert!(!pool.vendors.is_empty());
1211
1212        let mut rng = ChaCha8Rng::seed_from_u64(42);
1213        let vendor = pool.random_vendor(&mut rng);
1214        assert!(vendor.is_some());
1215
1216        let tech_vendor = pool.random_vendor_of_type(VendorType::Technology, &mut rng);
1217        assert!(tech_vendor.is_some());
1218    }
1219
1220    #[test]
1221    fn test_customer_pool() {
1222        let pool = CustomerPool::standard();
1223
1224        assert!(!pool.customers.is_empty());
1225
1226        let mut rng = ChaCha8Rng::seed_from_u64(42);
1227        let customer = pool.random_customer(&mut rng);
1228        assert!(customer.is_some());
1229    }
1230
1231    #[test]
1232    fn test_amount_generation() {
1233        let mut rng = ChaCha8Rng::seed_from_u64(42);
1234        let vendor = Vendor::new("V-001", "Test", VendorType::Supplier)
1235            .with_amount_range(Decimal::from(100), Decimal::from(1000));
1236
1237        let amount = vendor.generate_amount(&mut rng);
1238        assert!(amount >= Decimal::from(100));
1239        assert!(amount <= Decimal::from(1000));
1240    }
1241
1242    #[test]
1243    fn test_payment_terms() {
1244        assert_eq!(PaymentTerms::Net30.due_days(), 30);
1245        assert_eq!(PaymentTerms::Net60.due_days(), 60);
1246        assert!(PaymentTerms::Prepayment.requires_prepayment());
1247
1248        let discount = PaymentTerms::TwoTenNet30.early_payment_discount();
1249        assert!(discount.is_some());
1250        let (days, percent) = discount.unwrap();
1251        assert_eq!(days, 10);
1252        assert_eq!(percent, Decimal::from(2));
1253    }
1254
1255    #[test]
1256    fn test_credit_rating() {
1257        assert!(
1258            CreditRating::AAA.credit_limit_multiplier() > CreditRating::B.credit_limit_multiplier()
1259        );
1260        assert!(CreditRating::D.is_credit_blocked());
1261        assert!(!CreditRating::A.is_credit_blocked());
1262    }
1263
1264    #[test]
1265    fn test_customer_credit_check() {
1266        let mut customer = Customer::new("C-001", "Test", CustomerType::Corporate)
1267            .with_credit_limit(Decimal::from(10000));
1268
1269        // Should be able to place order within limit
1270        assert!(customer.can_place_order(Decimal::from(5000)));
1271
1272        // Add some exposure
1273        customer.add_credit_exposure(Decimal::from(8000));
1274
1275        // Now should fail for large order
1276        assert!(!customer.can_place_order(Decimal::from(5000)));
1277
1278        // But small order should work
1279        assert!(customer.can_place_order(Decimal::from(2000)));
1280
1281        // Block credit
1282        customer.block_credit("Testing");
1283        assert!(!customer.can_place_order(Decimal::from(100)));
1284    }
1285
1286    #[test]
1287    fn test_intercompany_vendor() {
1288        let vendor = Vendor::new_intercompany("V-IC-001", "Subsidiary Co", "2000");
1289
1290        assert!(vendor.is_intercompany);
1291        assert_eq!(vendor.intercompany_code, Some("2000".to_string()));
1292    }
1293
1294    #[test]
1295    fn test_intercompany_customer() {
1296        let customer = Customer::new_intercompany("C-IC-001", "Parent Co", "1000");
1297
1298        assert!(customer.is_intercompany);
1299        assert_eq!(customer.customer_type, CustomerType::Intercompany);
1300        assert_eq!(customer.credit_rating, CreditRating::AAA);
1301    }
1302
1303    #[test]
1304    fn test_payment_behavior() {
1305        assert!(CustomerPaymentBehavior::Excellent.on_time_probability() > 0.95);
1306        assert!(CustomerPaymentBehavior::VeryPoor.on_time_probability() < 0.25);
1307        assert!(CustomerPaymentBehavior::Excellent.average_days_past_due() < 0);
1308        // Pays early
1309    }
1310
1311    #[test]
1312    fn test_vendor_due_date() {
1313        let vendor = Vendor::new("V-001", "Test", VendorType::Supplier)
1314            .with_payment_terms_structured(PaymentTerms::Net30);
1315
1316        let invoice_date = chrono::NaiveDate::from_ymd_opt(2024, 1, 15).unwrap();
1317        let due_date = vendor.calculate_due_date(invoice_date);
1318
1319        assert_eq!(
1320            due_date,
1321            chrono::NaiveDate::from_ymd_opt(2024, 2, 14).unwrap()
1322        );
1323    }
1324}