1use rand::seq::IndexedRandom;
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 =
517 Decimal::from_f64_retain(rng.random::<f64>()).unwrap_or(Decimal::ZERO);
518 min + range * random_fraction
519 }
520
521 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
529#[serde(rename_all = "snake_case")]
530pub enum CustomerType {
531 #[default]
533 Corporate,
534 SmallBusiness,
536 Consumer,
538 Government,
540 NonProfit,
542 Intercompany,
544 Distributor,
546}
547
548#[derive(Debug, Clone, Serialize, Deserialize)]
550pub struct Customer {
551 pub customer_id: String,
553
554 pub name: String,
556
557 pub customer_type: CustomerType,
559
560 pub country: String,
562
563 pub credit_rating: CreditRating,
565
566 #[serde(with = "rust_decimal::serde::str")]
568 pub credit_limit: Decimal,
569
570 #[serde(with = "rust_decimal::serde::str")]
572 pub credit_exposure: Decimal,
573
574 pub payment_terms: PaymentTerms,
576
577 pub payment_terms_days: u8,
579
580 pub payment_behavior: CustomerPaymentBehavior,
582
583 pub is_active: bool,
585
586 pub account_number: Option<String>,
588
589 pub typical_order_range: (Decimal, Decimal),
591
592 pub is_intercompany: bool,
594
595 pub intercompany_code: Option<String>,
597
598 pub currency: String,
600
601 pub reconciliation_account: Option<String>,
603
604 pub sales_org: Option<String>,
606
607 pub distribution_channel: Option<String>,
609
610 pub tax_id: Option<String>,
612
613 pub credit_blocked: bool,
615
616 pub credit_block_reason: Option<String>,
618
619 pub dunning_procedure: Option<String>,
621
622 pub last_dunning_date: Option<chrono::NaiveDate>,
624
625 pub dunning_level: u8,
627}
628
629impl Customer {
630 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 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 pub fn with_country(mut self, country: &str) -> Self {
669 self.country = country.to_string();
670 self
671 }
672
673 pub fn with_credit_rating(mut self, rating: CreditRating) -> Self {
675 self.credit_rating = rating;
676 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 pub fn with_credit_limit(mut self, limit: Decimal) -> Self {
687 self.credit_limit = limit;
688 self
689 }
690
691 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 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 pub fn with_payment_behavior(mut self, behavior: CustomerPaymentBehavior) -> Self {
714 self.payment_behavior = behavior;
715 self
716 }
717
718 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 self.credit_rating = CreditRating::AAA;
725 self.payment_behavior = CustomerPaymentBehavior::Excellent;
726 self
727 }
728
729 pub fn with_currency(mut self, currency: &str) -> Self {
731 self.currency = currency.to_string();
732 self
733 }
734
735 pub fn with_sales_org(mut self, org: &str) -> Self {
737 self.sales_org = Some(org.to_string());
738 self
739 }
740
741 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 pub fn unblock_credit(&mut self) {
749 self.credit_blocked = false;
750 self.credit_block_reason = None;
751 }
752
753 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 self.credit_exposure + order_amount <= self.credit_limit
763 }
764
765 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 pub fn add_credit_exposure(&mut self, amount: Decimal) {
776 self.credit_exposure += amount;
777 }
778
779 pub fn reduce_credit_exposure(&mut self, amount: Decimal) {
781 self.credit_exposure = (self.credit_exposure - amount).max(Decimal::ZERO);
782 }
783
784 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 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 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 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#[derive(Debug, Clone, Default)]
814pub struct VendorPool {
815 pub vendors: Vec<Vendor>,
817 type_index: HashMap<VendorType, Vec<usize>>,
819}
820
821impl VendorPool {
822 pub fn new() -> Self {
824 Self {
825 vendors: Vec::new(),
826 type_index: HashMap::new(),
827 }
828 }
829
830 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 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 pub fn random_vendor(&self, rng: &mut impl Rng) -> Option<&Vendor> {
852 self.vendors.choose(rng)
853 }
854
855 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 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 pub fn standard() -> Self {
880 let mut pool = Self::new();
881
882 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 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 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 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 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 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 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 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#[derive(Debug, Clone, Default)]
1021pub struct CustomerPool {
1022 pub customers: Vec<Customer>,
1024 type_index: HashMap<CustomerType, Vec<usize>>,
1026}
1027
1028impl CustomerPool {
1029 pub fn new() -> Self {
1031 Self {
1032 customers: Vec::new(),
1033 type_index: HashMap::new(),
1034 }
1035 }
1036
1037 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 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 pub fn random_customer(&self, rng: &mut impl Rng) -> Option<&Customer> {
1059 self.customers.choose(rng)
1060 }
1061
1062 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 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 pub fn standard() -> Self {
1087 let mut pool = Self::new();
1088
1089 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 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 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 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 assert!(customer.can_place_order(Decimal::from(5000)));
1271
1272 customer.add_credit_exposure(Decimal::from(8000));
1274
1275 assert!(!customer.can_place_order(Decimal::from(5000)));
1277
1278 assert!(customer.can_place_order(Decimal::from(2000)));
1280
1281 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 }
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}