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 #[serde(default, skip_serializing_if = "Option::is_none")]
390 pub auxiliary_gl_account: Option<String>,
391
392 pub withholding_tax_applicable: bool,
394
395 pub withholding_tax_rate: Option<Decimal>,
397
398 pub is_one_time: bool,
400
401 pub purchasing_org: Option<String>,
403}
404
405impl Vendor {
406 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 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 pub fn with_country(mut self, country: &str) -> Self {
440 self.country = country.to_string();
441 self
442 }
443
444 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 pub fn with_payment_terms(mut self, days: u8) -> Self {
453 self.payment_terms_days = days;
454 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 pub fn with_amount_range(mut self, min: Decimal, max: Decimal) -> Self {
468 self.typical_amount_range = (min, max);
469 self
470 }
471
472 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 pub fn with_bank_account(mut self, account: BankAccount) -> Self {
481 self.bank_accounts.push(account);
482 self
483 }
484
485 pub fn with_behavior(mut self, behavior: VendorBehavior) -> Self {
487 self.behavior = behavior;
488 self
489 }
490
491 pub fn with_currency(mut self, currency: &str) -> Self {
493 self.currency = currency.to_string();
494 self
495 }
496
497 pub fn with_reconciliation_account(mut self, account: &str) -> Self {
499 self.reconciliation_account = Some(account.to_string());
500 self
501 }
502
503 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 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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
535#[serde(rename_all = "snake_case")]
536pub enum CustomerType {
537 #[default]
539 Corporate,
540 SmallBusiness,
542 Consumer,
544 Government,
546 NonProfit,
548 Intercompany,
550 Distributor,
552}
553
554#[derive(Debug, Clone, Serialize, Deserialize)]
556pub struct Customer {
557 pub customer_id: String,
559
560 pub name: String,
562
563 pub customer_type: CustomerType,
565
566 pub country: String,
568
569 pub credit_rating: CreditRating,
571
572 #[serde(with = "rust_decimal::serde::str")]
574 pub credit_limit: Decimal,
575
576 #[serde(with = "rust_decimal::serde::str")]
578 pub credit_exposure: Decimal,
579
580 pub payment_terms: PaymentTerms,
582
583 pub payment_terms_days: u8,
585
586 pub payment_behavior: CustomerPaymentBehavior,
588
589 pub is_active: bool,
591
592 pub account_number: Option<String>,
594
595 pub typical_order_range: (Decimal, Decimal),
597
598 pub is_intercompany: bool,
600
601 pub intercompany_code: Option<String>,
603
604 pub currency: String,
606
607 pub reconciliation_account: Option<String>,
609
610 #[serde(default, skip_serializing_if = "Option::is_none")]
613 pub auxiliary_gl_account: Option<String>,
614
615 pub sales_org: Option<String>,
617
618 pub distribution_channel: Option<String>,
620
621 pub tax_id: Option<String>,
623
624 pub credit_blocked: bool,
626
627 pub credit_block_reason: Option<String>,
629
630 pub dunning_procedure: Option<String>,
632
633 pub last_dunning_date: Option<chrono::NaiveDate>,
635
636 pub dunning_level: u8,
638}
639
640impl Customer {
641 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 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 pub fn with_country(mut self, country: &str) -> Self {
681 self.country = country.to_string();
682 self
683 }
684
685 pub fn with_credit_rating(mut self, rating: CreditRating) -> Self {
687 self.credit_rating = rating;
688 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 pub fn with_credit_limit(mut self, limit: Decimal) -> Self {
699 self.credit_limit = limit;
700 self
701 }
702
703 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 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 pub fn with_payment_behavior(mut self, behavior: CustomerPaymentBehavior) -> Self {
726 self.payment_behavior = behavior;
727 self
728 }
729
730 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 self.credit_rating = CreditRating::AAA;
737 self.payment_behavior = CustomerPaymentBehavior::Excellent;
738 self
739 }
740
741 pub fn with_currency(mut self, currency: &str) -> Self {
743 self.currency = currency.to_string();
744 self
745 }
746
747 pub fn with_sales_org(mut self, org: &str) -> Self {
749 self.sales_org = Some(org.to_string());
750 self
751 }
752
753 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 pub fn unblock_credit(&mut self) {
761 self.credit_blocked = false;
762 self.credit_block_reason = None;
763 }
764
765 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 self.credit_exposure + order_amount <= self.credit_limit
775 }
776
777 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 pub fn add_credit_exposure(&mut self, amount: Decimal) {
788 self.credit_exposure += amount;
789 }
790
791 pub fn reduce_credit_exposure(&mut self, amount: Decimal) {
793 self.credit_exposure = (self.credit_exposure - amount).max(Decimal::ZERO);
794 }
795
796 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 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 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 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#[derive(Debug, Clone, Default)]
826pub struct VendorPool {
827 pub vendors: Vec<Vendor>,
829 type_index: HashMap<VendorType, Vec<usize>>,
831}
832
833impl VendorPool {
834 pub fn new() -> Self {
836 Self {
837 vendors: Vec::new(),
838 type_index: HashMap::new(),
839 }
840 }
841
842 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 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 pub fn random_vendor(&self, rng: &mut impl Rng) -> Option<&Vendor> {
864 self.vendors.choose(rng)
865 }
866
867 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 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 pub fn standard() -> Self {
892 let mut pool = Self::new();
893
894 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 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 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 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 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 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 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 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#[derive(Debug, Clone, Default)]
1033pub struct CustomerPool {
1034 pub customers: Vec<Customer>,
1036 type_index: HashMap<CustomerType, Vec<usize>>,
1038}
1039
1040impl CustomerPool {
1041 pub fn new() -> Self {
1043 Self {
1044 customers: Vec::new(),
1045 type_index: HashMap::new(),
1046 }
1047 }
1048
1049 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 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 pub fn random_customer(&self, rng: &mut impl Rng) -> Option<&Customer> {
1071 self.customers.choose(rng)
1072 }
1073
1074 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 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 pub fn standard() -> Self {
1099 let mut pool = Self::new();
1100
1101 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 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 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 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 assert!(customer.can_place_order(Decimal::from(5000)));
1283
1284 customer.add_credit_exposure(Decimal::from(8000));
1286
1287 assert!(!customer.can_place_order(Decimal::from(5000)));
1289
1290 assert!(customer.can_place_order(Decimal::from(2000)));
1292
1293 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 }
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}