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