1use rand::seq::SliceRandom;
8use rand::Rng;
9use rust_decimal::Decimal;
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
15#[serde(rename_all = "snake_case")]
16pub enum PaymentTerms {
17 Immediate,
19 Net10,
21 Net15,
23 #[default]
25 Net30,
26 Net45,
28 Net60,
30 Net90,
32 TwoTenNet30,
34 OneTenNet30,
36 TwoFifteenNet45,
38 EndOfMonth,
40 EndOfMonthPlus30,
42 CashOnDelivery,
44 Prepayment,
46}
47
48impl PaymentTerms {
49 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, 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, Self::EndOfMonthPlus30 => 60, }
63 }
64
65 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 pub fn requires_prepayment(&self) -> bool {
77 matches!(self, Self::Prepayment | Self::CashOnDelivery)
78 }
79
80 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 pub fn net_days(&self) -> u16 {
102 self.due_days()
103 }
104
105 pub fn discount_days(&self) -> Option<u16> {
107 self.early_payment_discount().map(|(days, _)| days)
108 }
109
110 pub fn discount_percent(&self) -> Option<Decimal> {
112 self.early_payment_discount().map(|(_, percent)| percent)
113 }
114}
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
118#[serde(rename_all = "snake_case")]
119pub enum VendorBehavior {
120 Strict,
122 #[default]
124 Flexible,
125 VeryFlexible,
127 Aggressive,
129}
130
131impl VendorBehavior {
132 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
145#[serde(rename_all = "snake_case")]
146pub enum CustomerPaymentBehavior {
147 Excellent,
149 EarlyPayer,
151 #[default]
153 Good,
154 OnTime,
156 Fair,
158 SlightlyLate,
160 Poor,
162 OftenLate,
164 VeryPoor,
166 HighRisk,
168}
169
170impl CustomerPaymentBehavior {
171 pub fn average_days_past_due(&self) -> i16 {
173 match self {
174 Self::Excellent | Self::EarlyPayer => -5, 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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
207#[serde(rename_all = "snake_case")]
208pub enum CreditRating {
209 AAA,
211 AA,
213 #[default]
215 A,
216 BBB,
218 BB,
220 B,
222 CCC,
224 CC,
226 C,
228 D,
230}
231
232impl CreditRating {
233 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 pub fn is_credit_blocked(&self) -> bool {
251 matches!(self, Self::D)
252 }
253}
254
255#[derive(Debug, Clone, Serialize, Deserialize)]
257pub struct BankAccount {
258 pub bank_name: String,
260 pub bank_country: String,
262 pub account_number: String,
264 pub routing_code: String,
266 pub holder_name: String,
268 pub is_primary: bool,
270}
271
272impl BankAccount {
273 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
293#[serde(rename_all = "snake_case")]
294pub enum VendorType {
295 #[default]
297 Supplier,
298 ServiceProvider,
300 Utility,
302 ProfessionalServices,
304 Technology,
306 Logistics,
308 Contractor,
310 RealEstate,
312 Financial,
314 EmployeeReimbursement,
316}
317
318impl VendorType {
319 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#[derive(Debug, Clone, Serialize, Deserialize)]
338pub struct Vendor {
339 pub vendor_id: String,
341
342 pub name: String,
344
345 pub vendor_type: VendorType,
347
348 pub country: String,
350
351 pub payment_terms: PaymentTerms,
353
354 pub payment_terms_days: u8,
356
357 pub typical_amount_range: (Decimal, Decimal),
359
360 pub is_active: bool,
362
363 pub account_number: Option<String>,
365
366 pub tax_id: Option<String>,
368
369 pub bank_accounts: Vec<BankAccount>,
371
372 pub is_intercompany: bool,
374
375 pub intercompany_code: Option<String>,
377
378 pub behavior: VendorBehavior,
380
381 pub currency: String,
383
384 pub reconciliation_account: Option<String>,
386
387 pub withholding_tax_applicable: bool,
389
390 pub withholding_tax_rate: Option<Decimal>,
392
393 pub is_one_time: bool,
395
396 pub purchasing_org: Option<String>,
398}
399
400impl Vendor {
401 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 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 pub fn with_country(mut self, country: &str) -> Self {
434 self.country = country.to_string();
435 self
436 }
437
438 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 pub fn with_payment_terms(mut self, days: u8) -> Self {
447 self.payment_terms_days = days;
448 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 pub fn with_amount_range(mut self, min: Decimal, max: Decimal) -> Self {
462 self.typical_amount_range = (min, max);
463 self
464 }
465
466 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 pub fn with_bank_account(mut self, account: BankAccount) -> Self {
475 self.bank_accounts.push(account);
476 self
477 }
478
479 pub fn with_behavior(mut self, behavior: VendorBehavior) -> Self {
481 self.behavior = behavior;
482 self
483 }
484
485 pub fn with_currency(mut self, currency: &str) -> Self {
487 self.currency = currency.to_string();
488 self
489 }
490
491 pub fn with_reconciliation_account(mut self, account: &str) -> Self {
493 self.reconciliation_account = Some(account.to_string());
494 self
495 }
496
497 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 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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
528#[serde(rename_all = "snake_case")]
529pub enum CustomerType {
530 #[default]
532 Corporate,
533 SmallBusiness,
535 Consumer,
537 Government,
539 NonProfit,
541 Intercompany,
543 Distributor,
545}
546
547#[derive(Debug, Clone, Serialize, Deserialize)]
549pub struct Customer {
550 pub customer_id: String,
552
553 pub name: String,
555
556 pub customer_type: CustomerType,
558
559 pub country: String,
561
562 pub credit_rating: CreditRating,
564
565 #[serde(with = "rust_decimal::serde::str")]
567 pub credit_limit: Decimal,
568
569 #[serde(with = "rust_decimal::serde::str")]
571 pub credit_exposure: Decimal,
572
573 pub payment_terms: PaymentTerms,
575
576 pub payment_terms_days: u8,
578
579 pub payment_behavior: CustomerPaymentBehavior,
581
582 pub is_active: bool,
584
585 pub account_number: Option<String>,
587
588 pub typical_order_range: (Decimal, Decimal),
590
591 pub is_intercompany: bool,
593
594 pub intercompany_code: Option<String>,
596
597 pub currency: String,
599
600 pub reconciliation_account: Option<String>,
602
603 pub sales_org: Option<String>,
605
606 pub distribution_channel: Option<String>,
608
609 pub tax_id: Option<String>,
611
612 pub credit_blocked: bool,
614
615 pub credit_block_reason: Option<String>,
617
618 pub dunning_procedure: Option<String>,
620
621 pub last_dunning_date: Option<chrono::NaiveDate>,
623
624 pub dunning_level: u8,
626}
627
628impl Customer {
629 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 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 pub fn with_country(mut self, country: &str) -> Self {
668 self.country = country.to_string();
669 self
670 }
671
672 pub fn with_credit_rating(mut self, rating: CreditRating) -> Self {
674 self.credit_rating = rating;
675 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 pub fn with_credit_limit(mut self, limit: Decimal) -> Self {
686 self.credit_limit = limit;
687 self
688 }
689
690 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 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 pub fn with_payment_behavior(mut self, behavior: CustomerPaymentBehavior) -> Self {
713 self.payment_behavior = behavior;
714 self
715 }
716
717 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 self.credit_rating = CreditRating::AAA;
724 self.payment_behavior = CustomerPaymentBehavior::Excellent;
725 self
726 }
727
728 pub fn with_currency(mut self, currency: &str) -> Self {
730 self.currency = currency.to_string();
731 self
732 }
733
734 pub fn with_sales_org(mut self, org: &str) -> Self {
736 self.sales_org = Some(org.to_string());
737 self
738 }
739
740 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 pub fn unblock_credit(&mut self) {
748 self.credit_blocked = false;
749 self.credit_block_reason = None;
750 }
751
752 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 self.credit_exposure + order_amount <= self.credit_limit
762 }
763
764 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 pub fn add_credit_exposure(&mut self, amount: Decimal) {
775 self.credit_exposure += amount;
776 }
777
778 pub fn reduce_credit_exposure(&mut self, amount: Decimal) {
780 self.credit_exposure = (self.credit_exposure - amount).max(Decimal::ZERO);
781 }
782
783 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 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 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 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#[derive(Debug, Clone, Default)]
812pub struct VendorPool {
813 pub vendors: Vec<Vendor>,
815 type_index: HashMap<VendorType, Vec<usize>>,
817}
818
819impl VendorPool {
820 pub fn new() -> Self {
822 Self {
823 vendors: Vec::new(),
824 type_index: HashMap::new(),
825 }
826 }
827
828 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 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 pub fn random_vendor(&self, rng: &mut impl Rng) -> Option<&Vendor> {
850 self.vendors.choose(rng)
851 }
852
853 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 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 pub fn standard() -> Self {
878 let mut pool = Self::new();
879
880 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 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 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 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 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 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 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 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#[derive(Debug, Clone, Default)]
1019pub struct CustomerPool {
1020 pub customers: Vec<Customer>,
1022 type_index: HashMap<CustomerType, Vec<usize>>,
1024}
1025
1026impl CustomerPool {
1027 pub fn new() -> Self {
1029 Self {
1030 customers: Vec::new(),
1031 type_index: HashMap::new(),
1032 }
1033 }
1034
1035 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 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 pub fn random_customer(&self, rng: &mut impl Rng) -> Option<&Customer> {
1057 self.customers.choose(rng)
1058 }
1059
1060 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 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 pub fn standard() -> Self {
1085 let mut pool = Self::new();
1086
1087 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 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 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 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 assert!(customer.can_place_order(Decimal::from(5000)));
1268
1269 customer.add_credit_exposure(Decimal::from(8000));
1271
1272 assert!(!customer.can_place_order(Decimal::from(5000)));
1274
1275 assert!(customer.can_place_order(Decimal::from(2000)));
1277
1278 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 }
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}