Skip to main content

datasynth_generators/treasury/
bank_guarantee_generator.rs

1//! Bank Guarantee Generator.
2//!
3//! Creates bank guarantees and letters of credit for an entity, selecting
4//! beneficiaries from the vendor pool and issuing banks from a fixed pool.
5
6use chrono::NaiveDate;
7use datasynth_core::utils::seeded_rng;
8use rand::prelude::*;
9use rand_chacha::ChaCha8Rng;
10use rust_decimal::Decimal;
11
12use datasynth_config::schema::BankGuaranteeSchemaConfig;
13use datasynth_core::models::{BankGuarantee, GuaranteeStatus, GuaranteeType};
14
15// ---------------------------------------------------------------------------
16// Issuing bank pool
17// ---------------------------------------------------------------------------
18
19const ISSUING_BANKS: &[&str] = &[
20    "Deutsche Bank",
21    "HSBC",
22    "Citibank",
23    "BNP Paribas",
24    "Standard Chartered",
25    "JPMorgan Chase",
26    "Barclays",
27    "Commerzbank",
28    "Societe Generale",
29    "ING Bank",
30];
31
32const GUARANTEE_TYPES: &[GuaranteeType] = &[
33    GuaranteeType::PerformanceBond,
34    GuaranteeType::BankGuarantee,
35    GuaranteeType::StandbyLc,
36    GuaranteeType::CommercialLc,
37];
38
39// ---------------------------------------------------------------------------
40// Generator
41// ---------------------------------------------------------------------------
42
43/// Generates bank guarantees and letters of credit.
44pub struct BankGuaranteeGenerator {
45    rng: ChaCha8Rng,
46    config: BankGuaranteeSchemaConfig,
47    counter: u64,
48}
49
50impl BankGuaranteeGenerator {
51    /// Creates a new bank guarantee generator.
52    pub fn new(config: BankGuaranteeSchemaConfig, seed: u64) -> Self {
53        Self {
54            rng: seeded_rng(seed, 0),
55            config,
56            counter: 0,
57        }
58    }
59
60    /// Generates bank guarantees for an entity.
61    ///
62    /// Uses vendors as beneficiaries. Generates the configured number of
63    /// guarantees with random types, amounts (5K-500K), and durations
64    /// (90-365 days). Most guarantees are `Active`; roughly 20% are `Expired`.
65    pub fn generate(
66        &mut self,
67        entity_id: &str,
68        currency: &str,
69        start_date: NaiveDate,
70        vendors: &[String],
71    ) -> Vec<BankGuarantee> {
72        if vendors.is_empty() {
73            return Vec::new();
74        }
75
76        let count = self.config.count as usize;
77        let mut guarantees = Vec::with_capacity(count);
78
79        for _ in 0..count {
80            self.counter += 1;
81            let id = format!("BG-{:06}", self.counter);
82
83            // Pick guarantee type
84            let gt_idx = self.rng.random_range(0..GUARANTEE_TYPES.len());
85            let guarantee_type = GUARANTEE_TYPES[gt_idx];
86
87            // Random amount between 5,000 and 500,000
88            let amount_f = self.rng.random_range(5_000.0f64..500_000.0);
89            let amount = Decimal::try_from(amount_f)
90                .unwrap_or(Decimal::new(50_000, 0))
91                .round_dp(2);
92
93            // Pick beneficiary from vendors
94            let vendor_idx = self.rng.random_range(0..vendors.len());
95            let beneficiary = &vendors[vendor_idx];
96
97            // Pick issuing bank
98            let bank_idx = self.rng.random_range(0..ISSUING_BANKS.len());
99            let issuing_bank = ISSUING_BANKS[bank_idx];
100
101            // Issue date: random offset within the period
102            let offset_days = self.rng.random_range(0i64..180);
103            let issue_date = start_date + chrono::Duration::days(offset_days);
104
105            // Duration: 90-365 days
106            let duration_days = self.rng.random_range(90i64..365);
107            let expiry_date = issue_date + chrono::Duration::days(duration_days);
108
109            let mut guarantee = BankGuarantee::new(
110                id,
111                entity_id,
112                guarantee_type,
113                amount,
114                currency,
115                beneficiary.as_str(),
116                issuing_bank,
117                issue_date,
118                expiry_date,
119            );
120
121            // ~20% are expired
122            if self.rng.random_bool(0.20) {
123                guarantee = guarantee.with_status(GuaranteeStatus::Expired);
124            }
125
126            guarantees.push(guarantee);
127        }
128
129        guarantees
130    }
131}
132
133// ---------------------------------------------------------------------------
134// Tests
135// ---------------------------------------------------------------------------
136
137#[cfg(test)]
138#[allow(clippy::unwrap_used)]
139mod tests {
140    use super::*;
141
142    fn d(s: &str) -> NaiveDate {
143        NaiveDate::parse_from_str(s, "%Y-%m-%d").unwrap()
144    }
145
146    fn sample_vendors() -> Vec<String> {
147        vec![
148            "Acme Corp".to_string(),
149            "Widget Co".to_string(),
150            "BuildRight Ltd".to_string(),
151        ]
152    }
153
154    #[test]
155    fn test_basic_generation() {
156        let config = BankGuaranteeSchemaConfig {
157            enabled: true,
158            count: 5,
159        };
160        let mut gen = BankGuaranteeGenerator::new(config, 42);
161        let guarantees = gen.generate("C001", "USD", d("2025-01-01"), &sample_vendors());
162
163        assert_eq!(guarantees.len(), 5);
164        for g in &guarantees {
165            assert!(g.id.starts_with("BG-"));
166            assert_eq!(g.entity_id, "C001");
167            assert_eq!(g.currency, "USD");
168            assert!(g.amount > Decimal::ZERO);
169            assert!(g.expiry_date > g.issue_date);
170            assert!(!g.beneficiary.is_empty());
171            assert!(!g.issuing_bank.is_empty());
172        }
173    }
174
175    #[test]
176    fn test_deterministic() {
177        let config = BankGuaranteeSchemaConfig {
178            enabled: true,
179            count: 3,
180        };
181        let vendors = sample_vendors();
182
183        let mut gen1 = BankGuaranteeGenerator::new(config.clone(), 42);
184        let r1 = gen1.generate("C001", "USD", d("2025-01-01"), &vendors);
185
186        let mut gen2 = BankGuaranteeGenerator::new(config, 42);
187        let r2 = gen2.generate("C001", "USD", d("2025-01-01"), &vendors);
188
189        assert_eq!(r1.len(), r2.len());
190        for (a, b) in r1.iter().zip(r2.iter()) {
191            assert_eq!(a.id, b.id);
192            assert_eq!(a.amount, b.amount);
193            assert_eq!(a.guarantee_type, b.guarantee_type);
194            assert_eq!(a.beneficiary, b.beneficiary);
195            assert_eq!(a.status, b.status);
196        }
197    }
198
199    #[test]
200    fn test_empty_vendors() {
201        let config = BankGuaranteeSchemaConfig {
202            enabled: true,
203            count: 5,
204        };
205        let mut gen = BankGuaranteeGenerator::new(config, 42);
206        let guarantees = gen.generate("C001", "USD", d("2025-01-01"), &[]);
207
208        assert!(guarantees.is_empty());
209    }
210}