Skip to main content

datasynth_generators/master_data/
vendor_generator.rs

1//! Enhanced vendor generator with realistic payment behavior and bank accounts.
2
3use chrono::NaiveDate;
4use datasynth_core::models::{BankAccount, PaymentTerms, Vendor, VendorBehavior, VendorPool};
5use rand::prelude::*;
6use rand_chacha::ChaCha8Rng;
7
8/// Configuration for vendor generation.
9#[derive(Debug, Clone)]
10pub struct VendorGeneratorConfig {
11    /// Distribution of payment terms (terms, probability)
12    pub payment_terms_distribution: Vec<(PaymentTerms, f64)>,
13    /// Distribution of vendor behaviors (behavior, probability)
14    pub behavior_distribution: Vec<(VendorBehavior, f64)>,
15    /// Probability of vendor being intercompany
16    pub intercompany_rate: f64,
17    /// Default country for vendors
18    pub default_country: String,
19    /// Default currency
20    pub default_currency: String,
21    /// Generate bank accounts
22    pub generate_bank_accounts: bool,
23    /// Probability of vendor having multiple bank accounts
24    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
52/// Vendor name templates by category.
53const 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
134/// Bank name templates.
135const 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
148/// Generator for vendor master data.
149pub struct VendorGenerator {
150    rng: ChaCha8Rng,
151    seed: u64,
152    config: VendorGeneratorConfig,
153    vendor_counter: usize,
154}
155
156impl VendorGenerator {
157    /// Create a new vendor generator.
158    pub fn new(seed: u64) -> Self {
159        Self::with_config(seed, VendorGeneratorConfig::default())
160    }
161
162    /// Create a new vendor generator with custom configuration.
163    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    /// Generate a single vendor.
173    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        // Note: category, effective_date, address are not fields on Vendor
189
190        // Set payment terms
191        vendor.payment_terms = self.select_payment_terms();
192
193        // Set behavior
194        vendor.behavior = self.select_vendor_behavior();
195
196        // Check if intercompany
197        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        // Generate bank accounts
203        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    /// Generate an intercompany vendor (always intercompany).
217    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; // IC usually immediate
228        vendor
229    }
230
231    /// Generate a vendor pool with specified count.
232    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    /// Generate a vendor pool with intercompany vendors.
249    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        // Generate regular vendors
259        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        // Generate IC vendors for each partner
266        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    /// Select a vendor name from templates.
275    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    /// Select payment terms based on distribution.
283    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    /// Select vendor behavior based on distribution.
298    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    /// Generate a tax ID.
313    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    /// Generate a bank account.
322    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    /// Generate an address.
340    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    /// Reset the generator.
369    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}