1use chrono::NaiveDate;
4use datasynth_core::models::{BankAccount, PaymentTerms, Vendor, VendorBehavior, VendorPool};
5use rand::prelude::*;
6use rand_chacha::ChaCha8Rng;
7
8#[derive(Debug, Clone)]
10pub struct VendorGeneratorConfig {
11 pub payment_terms_distribution: Vec<(PaymentTerms, f64)>,
13 pub behavior_distribution: Vec<(VendorBehavior, f64)>,
15 pub intercompany_rate: f64,
17 pub default_country: String,
19 pub default_currency: String,
21 pub generate_bank_accounts: bool,
23 pub multiple_bank_account_rate: f64,
25}
26
27impl Default for VendorGeneratorConfig {
28 fn default() -> Self {
29 Self {
30 payment_terms_distribution: vec![
31 (PaymentTerms::Net30, 0.40),
32 (PaymentTerms::Net60, 0.20),
33 (PaymentTerms::TwoTenNet30, 0.25),
34 (PaymentTerms::Net15, 0.10),
35 (PaymentTerms::Immediate, 0.05),
36 ],
37 behavior_distribution: vec![
38 (VendorBehavior::Flexible, 0.60),
39 (VendorBehavior::Strict, 0.25),
40 (VendorBehavior::VeryFlexible, 0.10),
41 (VendorBehavior::Aggressive, 0.05),
42 ],
43 intercompany_rate: 0.05,
44 default_country: "US".to_string(),
45 default_currency: "USD".to_string(),
46 generate_bank_accounts: true,
47 multiple_bank_account_rate: 0.20,
48 }
49 }
50}
51
52const VENDOR_NAME_TEMPLATES: &[(&str, &[&str])] = &[
54 (
55 "Manufacturing",
56 &[
57 "Global Manufacturing Solutions",
58 "Precision Parts Inc.",
59 "Industrial Components Ltd.",
60 "Advanced Materials Corp.",
61 "Quality Fabrication Services",
62 "Metalworks International",
63 "Polymer Technologies",
64 "Assembly Dynamics",
65 ],
66 ),
67 (
68 "Services",
69 &[
70 "Professional Services Group",
71 "Consulting Partners LLC",
72 "Business Solutions Inc.",
73 "Technical Services Corp.",
74 "Support Systems International",
75 "Managed Services Ltd.",
76 "Advisory Group Partners",
77 "Strategic Consulting Co.",
78 ],
79 ),
80 (
81 "Technology",
82 &[
83 "Tech Solutions Inc.",
84 "Digital Systems Corp.",
85 "Software Innovations LLC",
86 "Cloud Services Partners",
87 "IT Infrastructure Group",
88 "Data Systems International",
89 "Network Solutions Ltd.",
90 "Cyber Systems Corp.",
91 ],
92 ),
93 (
94 "Logistics",
95 &[
96 "Global Logistics Partners",
97 "Freight Solutions Inc.",
98 "Supply Chain Services",
99 "Distribution Networks LLC",
100 "Warehouse Solutions Corp.",
101 "Transportation Partners",
102 "Shipping Dynamics Ltd.",
103 "Fulfillment Services Inc.",
104 ],
105 ),
106 (
107 "Office",
108 &[
109 "Office Supplies Direct",
110 "Business Products Inc.",
111 "Stationery Solutions",
112 "Equipment Suppliers Ltd.",
113 "Furniture Systems Corp.",
114 "Workplace Supplies LLC",
115 "Office Essentials Inc.",
116 "Business Equipment Co.",
117 ],
118 ),
119 (
120 "Utilities",
121 &[
122 "Power Solutions Inc.",
123 "Energy Services Corp.",
124 "Utility Management LLC",
125 "Water Services Group",
126 "Telecom Solutions Ltd.",
127 "Communications Partners",
128 "Internet Services Inc.",
129 "Utility Systems Corp.",
130 ],
131 ),
132];
133
134const BANK_NAMES: &[&str] = &[
136 "First National Bank",
137 "Commerce Bank",
138 "United Banking Corp",
139 "Regional Trust Bank",
140 "Merchants Bank",
141 "Citizens Financial",
142 "Pacific Coast Bank",
143 "Atlantic Commerce Bank",
144 "Midwest Trust Company",
145 "Capital One Commercial",
146];
147
148pub struct VendorGenerator {
150 rng: ChaCha8Rng,
151 seed: u64,
152 config: VendorGeneratorConfig,
153 vendor_counter: usize,
154}
155
156impl VendorGenerator {
157 pub fn new(seed: u64) -> Self {
159 Self::with_config(seed, VendorGeneratorConfig::default())
160 }
161
162 pub fn with_config(seed: u64, config: VendorGeneratorConfig) -> Self {
164 Self {
165 rng: ChaCha8Rng::seed_from_u64(seed),
166 seed,
167 config,
168 vendor_counter: 0,
169 }
170 }
171
172 pub fn generate_vendor(&mut self, company_code: &str, _effective_date: NaiveDate) -> Vendor {
174 self.vendor_counter += 1;
175
176 let vendor_id = format!("V-{:06}", self.vendor_counter);
177 let (_category, name) = self.select_vendor_name();
178 let tax_id = self.generate_tax_id();
179
180 let mut vendor = Vendor::new(
181 &vendor_id,
182 name,
183 datasynth_core::models::VendorType::Supplier,
184 );
185 vendor.tax_id = Some(tax_id);
186 vendor.country = self.config.default_country.clone();
187 vendor.currency = self.config.default_currency.clone();
188 vendor.payment_terms = self.select_payment_terms();
192
193 vendor.behavior = self.select_vendor_behavior();
195
196 if self.rng.gen::<f64>() < self.config.intercompany_rate {
198 vendor.is_intercompany = true;
199 vendor.intercompany_code = Some(format!("IC-{}", company_code));
200 }
201
202 if self.config.generate_bank_accounts {
204 let bank_account = self.generate_bank_account(&vendor.vendor_id);
205 vendor.bank_accounts.push(bank_account);
206
207 if self.rng.gen::<f64>() < self.config.multiple_bank_account_rate {
208 let bank_account2 = self.generate_bank_account(&vendor.vendor_id);
209 vendor.bank_accounts.push(bank_account2);
210 }
211 }
212
213 vendor
214 }
215
216 pub fn generate_intercompany_vendor(
218 &mut self,
219 company_code: &str,
220 partner_company_code: &str,
221 effective_date: NaiveDate,
222 ) -> Vendor {
223 let mut vendor = self.generate_vendor(company_code, effective_date);
224 vendor.is_intercompany = true;
225 vendor.intercompany_code = Some(partner_company_code.to_string());
226 vendor.name = format!("{} - IC", partner_company_code);
227 vendor.payment_terms = PaymentTerms::Immediate; vendor
229 }
230
231 pub fn generate_vendor_pool(
233 &mut self,
234 count: usize,
235 company_code: &str,
236 effective_date: NaiveDate,
237 ) -> VendorPool {
238 let mut pool = VendorPool::new();
239
240 for _ in 0..count {
241 let vendor = self.generate_vendor(company_code, effective_date);
242 pool.add_vendor(vendor);
243 }
244
245 pool
246 }
247
248 pub fn generate_vendor_pool_with_ic(
250 &mut self,
251 count: usize,
252 company_code: &str,
253 partner_company_codes: &[String],
254 effective_date: NaiveDate,
255 ) -> VendorPool {
256 let mut pool = VendorPool::new();
257
258 let regular_count = count.saturating_sub(partner_company_codes.len());
260 for _ in 0..regular_count {
261 let vendor = self.generate_vendor(company_code, effective_date);
262 pool.add_vendor(vendor);
263 }
264
265 for partner in partner_company_codes {
267 let vendor = self.generate_intercompany_vendor(company_code, partner, effective_date);
268 pool.add_vendor(vendor);
269 }
270
271 pool
272 }
273
274 fn select_vendor_name(&mut self) -> (&'static str, &'static str) {
276 let category_idx = self.rng.gen_range(0..VENDOR_NAME_TEMPLATES.len());
277 let (category, names) = VENDOR_NAME_TEMPLATES[category_idx];
278 let name_idx = self.rng.gen_range(0..names.len());
279 (category, names[name_idx])
280 }
281
282 fn select_payment_terms(&mut self) -> PaymentTerms {
284 let roll: f64 = self.rng.gen();
285 let mut cumulative = 0.0;
286
287 for (terms, prob) in &self.config.payment_terms_distribution {
288 cumulative += prob;
289 if roll < cumulative {
290 return *terms;
291 }
292 }
293
294 PaymentTerms::Net30
295 }
296
297 fn select_vendor_behavior(&mut self) -> VendorBehavior {
299 let roll: f64 = self.rng.gen();
300 let mut cumulative = 0.0;
301
302 for (behavior, prob) in &self.config.behavior_distribution {
303 cumulative += prob;
304 if roll < cumulative {
305 return *behavior;
306 }
307 }
308
309 VendorBehavior::Flexible
310 }
311
312 fn generate_tax_id(&mut self) -> String {
314 format!(
315 "{:02}-{:07}",
316 self.rng.gen_range(10..99),
317 self.rng.gen_range(1000000..9999999)
318 )
319 }
320
321 fn generate_bank_account(&mut self, vendor_id: &str) -> BankAccount {
323 let bank_idx = self.rng.gen_range(0..BANK_NAMES.len());
324 let bank_name = BANK_NAMES[bank_idx];
325
326 let routing = format!("{:09}", self.rng.gen_range(100000000u64..999999999));
327 let account = format!("{:010}", self.rng.gen_range(1000000000u64..9999999999));
328
329 BankAccount {
330 bank_name: bank_name.to_string(),
331 bank_country: "US".to_string(),
332 account_number: account,
333 routing_code: routing,
334 holder_name: format!("Vendor {}", vendor_id),
335 is_primary: self.vendor_counter == 1,
336 }
337 }
338
339 fn generate_address(&mut self) -> String {
341 let street_num = self.rng.gen_range(100..9999);
342 let streets = [
343 "Main St",
344 "Oak Ave",
345 "Industrial Blvd",
346 "Commerce Dr",
347 "Business Park Way",
348 ];
349 let cities = [
350 "Chicago",
351 "Houston",
352 "Phoenix",
353 "Philadelphia",
354 "San Antonio",
355 "Dallas",
356 ];
357 let states = ["IL", "TX", "AZ", "PA", "TX", "TX"];
358
359 let idx = self.rng.gen_range(0..streets.len());
360 let zip = self.rng.gen_range(10000..99999);
361
362 format!(
363 "{} {}, {}, {} {}",
364 street_num, streets[idx], cities[idx], states[idx], zip
365 )
366 }
367
368 pub fn reset(&mut self) {
370 self.rng = ChaCha8Rng::seed_from_u64(self.seed);
371 self.vendor_counter = 0;
372 }
373}
374
375#[cfg(test)]
376mod tests {
377 use super::*;
378
379 #[test]
380 fn test_vendor_generation() {
381 let mut gen = VendorGenerator::new(42);
382 let vendor = gen.generate_vendor("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
383
384 assert!(!vendor.vendor_id.is_empty());
385 assert!(!vendor.name.is_empty());
386 assert!(vendor.tax_id.is_some());
387 assert!(!vendor.bank_accounts.is_empty());
388 }
389
390 #[test]
391 fn test_vendor_pool_generation() {
392 let mut gen = VendorGenerator::new(42);
393 let pool =
394 gen.generate_vendor_pool(10, "1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
395
396 assert_eq!(pool.vendors.len(), 10);
397 }
398
399 #[test]
400 fn test_intercompany_vendor() {
401 let mut gen = VendorGenerator::new(42);
402 let vendor = gen.generate_intercompany_vendor(
403 "1000",
404 "2000",
405 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
406 );
407
408 assert!(vendor.is_intercompany);
409 assert_eq!(vendor.intercompany_code, Some("2000".to_string()));
410 }
411
412 #[test]
413 fn test_deterministic_generation() {
414 let mut gen1 = VendorGenerator::new(42);
415 let mut gen2 = VendorGenerator::new(42);
416
417 let vendor1 = gen1.generate_vendor("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
418 let vendor2 = gen2.generate_vendor("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
419
420 assert_eq!(vendor1.vendor_id, vendor2.vendor_id);
421 assert_eq!(vendor1.name, vendor2.name);
422 }
423
424 #[test]
425 fn test_vendor_pool_with_ic() {
426 let mut gen = VendorGenerator::new(42);
427 let pool = gen.generate_vendor_pool_with_ic(
428 10,
429 "1000",
430 &["2000".to_string(), "3000".to_string()],
431 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
432 );
433
434 assert_eq!(pool.vendors.len(), 10);
435
436 let ic_vendors: Vec<_> = pool.vendors.iter().filter(|v| v.is_intercompany).collect();
437 assert_eq!(ic_vendors.len(), 2);
438 }
439}