Skip to main content

datasynth_generators/master_data/
customer_generator.rs

1//! Enhanced customer generator with credit management and payment behavior.
2
3use chrono::NaiveDate;
4use datasynth_core::models::{
5    CreditRating, Customer, CustomerPaymentBehavior, CustomerPool, PaymentTerms,
6};
7use rand::prelude::*;
8use rand_chacha::ChaCha8Rng;
9use rust_decimal::Decimal;
10
11/// Configuration for customer generation.
12#[derive(Debug, Clone)]
13pub struct CustomerGeneratorConfig {
14    /// Distribution of credit ratings (rating, probability)
15    pub credit_rating_distribution: Vec<(CreditRating, f64)>,
16    /// Distribution of payment behaviors (behavior, probability)
17    pub payment_behavior_distribution: Vec<(CustomerPaymentBehavior, f64)>,
18    /// Distribution of payment terms (terms, probability)
19    pub payment_terms_distribution: Vec<(PaymentTerms, f64)>,
20    /// Probability of customer being intercompany
21    pub intercompany_rate: f64,
22    /// Default country for customers
23    pub default_country: String,
24    /// Default currency
25    pub default_currency: String,
26    /// Credit limit ranges by rating (min, max)
27    pub credit_limits: Vec<(CreditRating, Decimal, Decimal)>,
28}
29
30impl Default for CustomerGeneratorConfig {
31    fn default() -> Self {
32        Self {
33            credit_rating_distribution: vec![
34                (CreditRating::AAA, 0.05),
35                (CreditRating::AA, 0.10),
36                (CreditRating::A, 0.25),
37                (CreditRating::BBB, 0.30),
38                (CreditRating::BB, 0.15),
39                (CreditRating::B, 0.10),
40                (CreditRating::CCC, 0.04),
41                (CreditRating::D, 0.01),
42            ],
43            payment_behavior_distribution: vec![
44                (CustomerPaymentBehavior::EarlyPayer, 0.15),
45                (CustomerPaymentBehavior::OnTime, 0.45),
46                (CustomerPaymentBehavior::SlightlyLate, 0.25),
47                (CustomerPaymentBehavior::OftenLate, 0.10),
48                (CustomerPaymentBehavior::HighRisk, 0.05),
49            ],
50            payment_terms_distribution: vec![
51                (PaymentTerms::Net30, 0.50),
52                (PaymentTerms::Net60, 0.20),
53                (PaymentTerms::TwoTenNet30, 0.20),
54                (PaymentTerms::Net15, 0.05),
55                (PaymentTerms::Immediate, 0.05),
56            ],
57            intercompany_rate: 0.05,
58            default_country: "US".to_string(),
59            default_currency: "USD".to_string(),
60            credit_limits: vec![
61                (
62                    CreditRating::AAA,
63                    Decimal::from(1_000_000),
64                    Decimal::from(10_000_000),
65                ),
66                (
67                    CreditRating::AA,
68                    Decimal::from(500_000),
69                    Decimal::from(2_000_000),
70                ),
71                (
72                    CreditRating::A,
73                    Decimal::from(250_000),
74                    Decimal::from(1_000_000),
75                ),
76                (
77                    CreditRating::BBB,
78                    Decimal::from(100_000),
79                    Decimal::from(500_000),
80                ),
81                (
82                    CreditRating::BB,
83                    Decimal::from(50_000),
84                    Decimal::from(250_000),
85                ),
86                (
87                    CreditRating::B,
88                    Decimal::from(25_000),
89                    Decimal::from(100_000),
90                ),
91                (
92                    CreditRating::CCC,
93                    Decimal::from(10_000),
94                    Decimal::from(50_000),
95                ),
96                (CreditRating::D, Decimal::from(0), Decimal::from(10_000)),
97            ],
98        }
99    }
100}
101
102/// Customer name templates by industry.
103const CUSTOMER_NAME_TEMPLATES: &[(&str, &[&str])] = &[
104    (
105        "Retail",
106        &[
107            "Consumer Goods Corp.",
108            "Retail Solutions Inc.",
109            "Shop Direct Ltd.",
110            "Market Leaders LLC",
111            "Consumer Brands Group",
112            "Retail Partners Co.",
113            "Shopping Networks Inc.",
114            "Direct Sales Corp.",
115        ],
116    ),
117    (
118        "Manufacturing",
119        &[
120            "Industrial Manufacturing Inc.",
121            "Production Systems Corp.",
122            "Assembly Technologies LLC",
123            "Manufacturing Partners Group",
124            "Factory Solutions Ltd.",
125            "Production Line Inc.",
126            "Industrial Works Corp.",
127            "Manufacturing Excellence Co.",
128        ],
129    ),
130    (
131        "Healthcare",
132        &[
133            "Healthcare Systems Inc.",
134            "Medical Solutions Corp.",
135            "Health Partners LLC",
136            "Medical Equipment Group",
137            "Healthcare Providers Ltd.",
138            "Clinical Services Inc.",
139            "Health Networks Corp.",
140            "Medical Supplies Co.",
141        ],
142    ),
143    (
144        "Technology",
145        &[
146            "Tech Innovations Inc.",
147            "Digital Solutions Corp.",
148            "Software Systems LLC",
149            "Technology Partners Group",
150            "IT Solutions Ltd.",
151            "Tech Enterprises Inc.",
152            "Digital Networks Corp.",
153            "Innovation Labs Co.",
154        ],
155    ),
156    (
157        "Financial",
158        &[
159            "Financial Services Inc.",
160            "Banking Solutions Corp.",
161            "Investment Partners LLC",
162            "Financial Networks Group",
163            "Capital Services Ltd.",
164            "Banking Partners Inc.",
165            "Finance Solutions Corp.",
166            "Investment Group Co.",
167        ],
168    ),
169    (
170        "Energy",
171        &[
172            "Energy Solutions Inc.",
173            "Power Systems Corp.",
174            "Renewable Partners LLC",
175            "Energy Networks Group",
176            "Utility Services Ltd.",
177            "Power Generation Inc.",
178            "Energy Partners Corp.",
179            "Sustainable Energy Co.",
180        ],
181    ),
182    (
183        "Transportation",
184        &[
185            "Transport Solutions Inc.",
186            "Logistics Systems Corp.",
187            "Freight Partners LLC",
188            "Transportation Networks Group",
189            "Shipping Services Ltd.",
190            "Fleet Management Inc.",
191            "Logistics Partners Corp.",
192            "Transport Dynamics Co.",
193        ],
194    ),
195    (
196        "Construction",
197        &[
198            "Construction Solutions Inc.",
199            "Building Systems Corp.",
200            "Development Partners LLC",
201            "Construction Group Ltd.",
202            "Building Services Inc.",
203            "Property Development Corp.",
204            "Construction Partners Co.",
205            "Infrastructure Systems LLC",
206        ],
207    ),
208];
209
210/// Generator for customer master data.
211pub struct CustomerGenerator {
212    rng: ChaCha8Rng,
213    seed: u64,
214    config: CustomerGeneratorConfig,
215    customer_counter: usize,
216}
217
218impl CustomerGenerator {
219    /// Create a new customer generator.
220    pub fn new(seed: u64) -> Self {
221        Self::with_config(seed, CustomerGeneratorConfig::default())
222    }
223
224    /// Create a new customer generator with custom configuration.
225    pub fn with_config(seed: u64, config: CustomerGeneratorConfig) -> Self {
226        Self {
227            rng: ChaCha8Rng::seed_from_u64(seed),
228            seed,
229            config,
230            customer_counter: 0,
231        }
232    }
233
234    /// Generate a single customer.
235    pub fn generate_customer(
236        &mut self,
237        company_code: &str,
238        _effective_date: NaiveDate,
239    ) -> Customer {
240        self.customer_counter += 1;
241
242        let customer_id = format!("C-{:06}", self.customer_counter);
243        let (_industry, name) = self.select_customer_name();
244
245        let mut customer = Customer::new(
246            &customer_id,
247            name,
248            datasynth_core::models::CustomerType::Corporate,
249        );
250
251        customer.country = self.config.default_country.clone();
252        customer.currency = self.config.default_currency.clone();
253        // Note: industry and effective_date are not fields on Customer
254
255        // Set credit rating and limit
256        customer.credit_rating = self.select_credit_rating();
257        customer.credit_limit = self.generate_credit_limit(&customer.credit_rating);
258
259        // Set payment behavior
260        customer.payment_behavior = self.select_payment_behavior();
261
262        // Set payment terms
263        customer.payment_terms = self.select_payment_terms();
264
265        // Check if intercompany
266        if self.rng.gen::<f64>() < self.config.intercompany_rate {
267            customer.is_intercompany = true;
268            customer.intercompany_code = Some(format!("IC-{}", company_code));
269        }
270
271        // Note: address, contact_name, contact_email are not fields on Customer
272
273        customer
274    }
275
276    /// Generate an intercompany customer (always intercompany).
277    pub fn generate_intercompany_customer(
278        &mut self,
279        company_code: &str,
280        partner_company_code: &str,
281        effective_date: NaiveDate,
282    ) -> Customer {
283        let mut customer = self.generate_customer(company_code, effective_date);
284        customer.is_intercompany = true;
285        customer.intercompany_code = Some(partner_company_code.to_string());
286        customer.name = format!("{} - IC", partner_company_code);
287        customer.credit_rating = CreditRating::AAA; // IC always highest rating
288        customer.credit_limit = Decimal::from(100_000_000); // High limit for IC
289        customer.payment_behavior = CustomerPaymentBehavior::OnTime;
290        customer
291    }
292
293    /// Generate a customer with specific credit profile.
294    pub fn generate_customer_with_credit(
295        &mut self,
296        company_code: &str,
297        credit_rating: CreditRating,
298        credit_limit: Decimal,
299        effective_date: NaiveDate,
300    ) -> Customer {
301        let mut customer = self.generate_customer(company_code, effective_date);
302        customer.credit_rating = credit_rating;
303        customer.credit_limit = credit_limit;
304
305        // Adjust payment behavior based on credit rating
306        customer.payment_behavior = match credit_rating {
307            CreditRating::AAA | CreditRating::AA => {
308                if self.rng.gen::<f64>() < 0.7 {
309                    CustomerPaymentBehavior::EarlyPayer
310                } else {
311                    CustomerPaymentBehavior::OnTime
312                }
313            }
314            CreditRating::A | CreditRating::BBB => CustomerPaymentBehavior::OnTime,
315            CreditRating::BB | CreditRating::B => CustomerPaymentBehavior::SlightlyLate,
316            CreditRating::CCC | CreditRating::CC => CustomerPaymentBehavior::OftenLate,
317            CreditRating::C | CreditRating::D => CustomerPaymentBehavior::HighRisk,
318        };
319
320        customer
321    }
322
323    /// Generate a customer pool with specified count.
324    pub fn generate_customer_pool(
325        &mut self,
326        count: usize,
327        company_code: &str,
328        effective_date: NaiveDate,
329    ) -> CustomerPool {
330        let mut pool = CustomerPool::new();
331
332        for _ in 0..count {
333            let customer = self.generate_customer(company_code, effective_date);
334            pool.add_customer(customer);
335        }
336
337        pool
338    }
339
340    /// Generate a customer pool with intercompany customers.
341    pub fn generate_customer_pool_with_ic(
342        &mut self,
343        count: usize,
344        company_code: &str,
345        partner_company_codes: &[String],
346        effective_date: NaiveDate,
347    ) -> CustomerPool {
348        let mut pool = CustomerPool::new();
349
350        // Generate regular customers
351        let regular_count = count.saturating_sub(partner_company_codes.len());
352        for _ in 0..regular_count {
353            let customer = self.generate_customer(company_code, effective_date);
354            pool.add_customer(customer);
355        }
356
357        // Generate IC customers for each partner
358        for partner in partner_company_codes {
359            let customer =
360                self.generate_intercompany_customer(company_code, partner, effective_date);
361            pool.add_customer(customer);
362        }
363
364        pool
365    }
366
367    /// Generate a diverse customer pool with various credit profiles.
368    pub fn generate_diverse_pool(
369        &mut self,
370        count: usize,
371        company_code: &str,
372        effective_date: NaiveDate,
373    ) -> CustomerPool {
374        let mut pool = CustomerPool::new();
375
376        // Generate customers with varied credit ratings ensuring coverage
377        let rating_counts = [
378            (CreditRating::AAA, (count as f64 * 0.05) as usize),
379            (CreditRating::AA, (count as f64 * 0.10) as usize),
380            (CreditRating::A, (count as f64 * 0.20) as usize),
381            (CreditRating::BBB, (count as f64 * 0.30) as usize),
382            (CreditRating::BB, (count as f64 * 0.15) as usize),
383            (CreditRating::B, (count as f64 * 0.10) as usize),
384            (CreditRating::CCC, (count as f64 * 0.07) as usize),
385            (CreditRating::D, (count as f64 * 0.03) as usize),
386        ];
387
388        for (rating, rating_count) in rating_counts {
389            for _ in 0..rating_count {
390                let credit_limit = self.generate_credit_limit(&rating);
391                let customer = self.generate_customer_with_credit(
392                    company_code,
393                    rating,
394                    credit_limit,
395                    effective_date,
396                );
397                pool.add_customer(customer);
398            }
399        }
400
401        // Fill any remaining slots
402        while pool.customers.len() < count {
403            let customer = self.generate_customer(company_code, effective_date);
404            pool.add_customer(customer);
405        }
406
407        pool
408    }
409
410    /// Select a customer name from templates.
411    fn select_customer_name(&mut self) -> (&'static str, &'static str) {
412        let industry_idx = self.rng.gen_range(0..CUSTOMER_NAME_TEMPLATES.len());
413        let (industry, names) = CUSTOMER_NAME_TEMPLATES[industry_idx];
414        let name_idx = self.rng.gen_range(0..names.len());
415        (industry, names[name_idx])
416    }
417
418    /// Select credit rating based on distribution.
419    fn select_credit_rating(&mut self) -> CreditRating {
420        let roll: f64 = self.rng.gen();
421        let mut cumulative = 0.0;
422
423        for (rating, prob) in &self.config.credit_rating_distribution {
424            cumulative += prob;
425            if roll < cumulative {
426                return *rating;
427            }
428        }
429
430        CreditRating::BBB
431    }
432
433    /// Generate credit limit for rating.
434    fn generate_credit_limit(&mut self, rating: &CreditRating) -> Decimal {
435        for (r, min, max) in &self.config.credit_limits {
436            if r == rating {
437                let range = (*max - *min).to_string().parse::<f64>().unwrap_or(0.0);
438                let offset = Decimal::from_f64_retain(self.rng.gen::<f64>() * range)
439                    .unwrap_or(Decimal::ZERO);
440                return *min + offset;
441            }
442        }
443
444        Decimal::from(100_000)
445    }
446
447    /// Select payment behavior based on distribution.
448    fn select_payment_behavior(&mut self) -> CustomerPaymentBehavior {
449        let roll: f64 = self.rng.gen();
450        let mut cumulative = 0.0;
451
452        for (behavior, prob) in &self.config.payment_behavior_distribution {
453            cumulative += prob;
454            if roll < cumulative {
455                return *behavior;
456            }
457        }
458
459        CustomerPaymentBehavior::OnTime
460    }
461
462    /// Select payment terms based on distribution.
463    fn select_payment_terms(&mut self) -> PaymentTerms {
464        let roll: f64 = self.rng.gen();
465        let mut cumulative = 0.0;
466
467        for (terms, prob) in &self.config.payment_terms_distribution {
468            cumulative += prob;
469            if roll < cumulative {
470                return *terms;
471            }
472        }
473
474        PaymentTerms::Net30
475    }
476
477    /// Generate an address.
478    fn generate_address(&mut self) -> String {
479        let street_num = self.rng.gen_range(1..9999);
480        let streets = [
481            "Corporate Dr",
482            "Business Center",
483            "Commerce Way",
484            "Executive Plaza",
485            "Industry Park",
486            "Trade Center",
487        ];
488        let cities = [
489            "New York",
490            "Los Angeles",
491            "Chicago",
492            "Houston",
493            "Phoenix",
494            "Philadelphia",
495            "San Antonio",
496            "San Diego",
497        ];
498        let states = ["NY", "CA", "IL", "TX", "AZ", "PA", "TX", "CA"];
499
500        let idx = self.rng.gen_range(0..cities.len());
501        let street_idx = self.rng.gen_range(0..streets.len());
502        let zip = self.rng.gen_range(10000..99999);
503
504        format!(
505            "{} {}, {}, {} {}",
506            street_num, streets[street_idx], cities[idx], states[idx], zip
507        )
508    }
509
510    /// Generate a contact name.
511    fn generate_contact_name(&mut self) -> String {
512        let first_names = [
513            "John", "Jane", "Michael", "Sarah", "David", "Emily", "Robert", "Lisa",
514        ];
515        let last_names = [
516            "Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis",
517        ];
518
519        let first = first_names[self.rng.gen_range(0..first_names.len())];
520        let last = last_names[self.rng.gen_range(0..last_names.len())];
521
522        format!("{} {}", first, last)
523    }
524
525    /// Generate a contact email.
526    fn generate_contact_email(&mut self, company_name: &str) -> String {
527        let domain = company_name
528            .to_lowercase()
529            .replace([' ', '.', ','], "")
530            .chars()
531            .filter(|c| c.is_alphanumeric())
532            .take(15)
533            .collect::<String>();
534
535        format!("contact@{}.com", domain)
536    }
537
538    /// Reset the generator.
539    pub fn reset(&mut self) {
540        self.rng = ChaCha8Rng::seed_from_u64(self.seed);
541        self.customer_counter = 0;
542    }
543}
544
545#[cfg(test)]
546mod tests {
547    use super::*;
548
549    #[test]
550    fn test_customer_generation() {
551        let mut gen = CustomerGenerator::new(42);
552        let customer = gen.generate_customer("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
553
554        assert!(!customer.customer_id.is_empty());
555        assert!(!customer.name.is_empty());
556        assert!(customer.credit_limit > Decimal::ZERO);
557    }
558
559    #[test]
560    fn test_customer_pool_generation() {
561        let mut gen = CustomerGenerator::new(42);
562        let pool =
563            gen.generate_customer_pool(20, "1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
564
565        assert_eq!(pool.customers.len(), 20);
566    }
567
568    #[test]
569    fn test_intercompany_customer() {
570        let mut gen = CustomerGenerator::new(42);
571        let customer = gen.generate_intercompany_customer(
572            "1000",
573            "2000",
574            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
575        );
576
577        assert!(customer.is_intercompany);
578        assert_eq!(customer.intercompany_code, Some("2000".to_string()));
579        assert_eq!(customer.credit_rating, CreditRating::AAA);
580    }
581
582    #[test]
583    fn test_diverse_pool() {
584        let mut gen = CustomerGenerator::new(42);
585        let pool =
586            gen.generate_diverse_pool(100, "1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
587
588        // Should have customers with various credit ratings
589        let aaa_count = pool
590            .customers
591            .iter()
592            .filter(|c| c.credit_rating == CreditRating::AAA)
593            .count();
594        let d_count = pool
595            .customers
596            .iter()
597            .filter(|c| c.credit_rating == CreditRating::D)
598            .count();
599
600        assert!(aaa_count > 0);
601        assert!(d_count > 0);
602    }
603
604    #[test]
605    fn test_deterministic_generation() {
606        let mut gen1 = CustomerGenerator::new(42);
607        let mut gen2 = CustomerGenerator::new(42);
608
609        let customer1 =
610            gen1.generate_customer("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
611        let customer2 =
612            gen2.generate_customer("1000", NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
613
614        assert_eq!(customer1.customer_id, customer2.customer_id);
615        assert_eq!(customer1.name, customer2.name);
616        assert_eq!(customer1.credit_rating, customer2.credit_rating);
617    }
618
619    #[test]
620    fn test_customer_with_specific_credit() {
621        let mut gen = CustomerGenerator::new(42);
622        let customer = gen.generate_customer_with_credit(
623            "1000",
624            CreditRating::D,
625            Decimal::from(5000),
626            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
627        );
628
629        assert_eq!(customer.credit_rating, CreditRating::D);
630        assert_eq!(customer.credit_limit, Decimal::from(5000));
631        assert_eq!(customer.payment_behavior, CustomerPaymentBehavior::HighRisk);
632    }
633}