Skip to main content

datasynth_banking/models/
counterparty.rs

1//! Counterparty models for banking transactions.
2
3use datasynth_core::models::banking::MerchantCategoryCode;
4use rust_decimal::Decimal;
5use serde::{Deserialize, Serialize};
6use uuid::Uuid;
7
8/// A merchant counterparty.
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct Merchant {
11    /// Unique merchant identifier
12    pub merchant_id: Uuid,
13    /// Merchant name
14    pub name: String,
15    /// Merchant category code
16    pub mcc: MerchantCategoryCode,
17    /// Country (ISO 3166-1 alpha-2)
18    pub country: String,
19    /// City
20    pub city: Option<String>,
21    /// Is online-only merchant
22    pub is_online: bool,
23    /// Typical transaction amount range
24    pub typical_amount_range: (Decimal, Decimal),
25    /// Whether merchant is high-risk
26    pub is_high_risk: bool,
27}
28
29impl Merchant {
30    /// Create a new merchant.
31    pub fn new(merchant_id: Uuid, name: &str, mcc: MerchantCategoryCode, country: &str) -> Self {
32        Self {
33            merchant_id,
34            name: name.to_string(),
35            mcc,
36            country: country.to_string(),
37            city: None,
38            is_online: false,
39            typical_amount_range: (Decimal::from(10), Decimal::from(500)),
40            is_high_risk: mcc.is_high_risk(),
41        }
42    }
43
44    /// Create an online merchant.
45    pub fn online(merchant_id: Uuid, name: &str, mcc: MerchantCategoryCode) -> Self {
46        Self {
47            merchant_id,
48            name: name.to_string(),
49            mcc,
50            country: "US".to_string(),
51            city: None,
52            is_online: true,
53            typical_amount_range: (Decimal::from(10), Decimal::from(1000)),
54            is_high_risk: mcc.is_high_risk(),
55        }
56    }
57}
58
59/// An employer counterparty.
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct Employer {
62    /// Unique employer identifier
63    pub employer_id: Uuid,
64    /// Employer name
65    pub name: String,
66    /// Country (ISO 3166-1 alpha-2)
67    pub country: String,
68    /// Industry code (NAICS)
69    pub industry_code: Option<String>,
70    /// Typical salary range
71    pub salary_range: (Decimal, Decimal),
72    /// Pay frequency
73    pub pay_frequency: PayFrequency,
74}
75
76impl Employer {
77    /// Create a new employer.
78    pub fn new(employer_id: Uuid, name: &str, country: &str) -> Self {
79        Self {
80            employer_id,
81            name: name.to_string(),
82            country: country.to_string(),
83            industry_code: None,
84            salary_range: (Decimal::from(3000), Decimal::from(10000)),
85            pay_frequency: PayFrequency::Monthly,
86        }
87    }
88}
89
90/// Pay frequency for salary deposits.
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
92#[serde(rename_all = "snake_case")]
93pub enum PayFrequency {
94    /// Weekly pay
95    Weekly,
96    /// Bi-weekly pay
97    BiWeekly,
98    /// Semi-monthly (twice per month)
99    SemiMonthly,
100    /// Monthly pay
101    #[default]
102    Monthly,
103}
104
105impl PayFrequency {
106    /// Days between payments.
107    pub fn interval_days(&self) -> u32 {
108        match self {
109            Self::Weekly => 7,
110            Self::BiWeekly => 14,
111            Self::SemiMonthly => 15,
112            Self::Monthly => 30,
113        }
114    }
115}
116
117/// A utility company counterparty.
118#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct UtilityCompany {
120    /// Unique utility identifier
121    pub utility_id: Uuid,
122    /// Utility name
123    pub name: String,
124    /// Utility type
125    pub utility_type: UtilityType,
126    /// Country (ISO 3166-1 alpha-2)
127    pub country: String,
128    /// Typical bill range
129    pub typical_bill_range: (Decimal, Decimal),
130}
131
132impl UtilityCompany {
133    /// Create a new utility company.
134    pub fn new(utility_id: Uuid, name: &str, utility_type: UtilityType, country: &str) -> Self {
135        let bill_range = match utility_type {
136            UtilityType::Electric => (Decimal::from(50), Decimal::from(300)),
137            UtilityType::Gas => (Decimal::from(30), Decimal::from(200)),
138            UtilityType::Water => (Decimal::from(20), Decimal::from(100)),
139            UtilityType::Internet => (Decimal::from(40), Decimal::from(150)),
140            UtilityType::Phone => (Decimal::from(30), Decimal::from(200)),
141            UtilityType::Cable => (Decimal::from(50), Decimal::from(200)),
142            UtilityType::Streaming => (Decimal::from(10), Decimal::from(50)),
143            UtilityType::Insurance => (Decimal::from(100), Decimal::from(500)),
144        };
145
146        Self {
147            utility_id,
148            name: name.to_string(),
149            utility_type,
150            country: country.to_string(),
151            typical_bill_range: bill_range,
152        }
153    }
154}
155
156/// Type of utility.
157#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
158#[serde(rename_all = "snake_case")]
159pub enum UtilityType {
160    /// Electric utility
161    Electric,
162    /// Gas utility
163    Gas,
164    /// Water utility
165    Water,
166    /// Internet service
167    Internet,
168    /// Phone service
169    Phone,
170    /// Cable/satellite TV
171    Cable,
172    /// Streaming service
173    Streaming,
174    /// Insurance
175    Insurance,
176}
177
178/// A government agency counterparty.
179#[derive(Debug, Clone, Serialize, Deserialize)]
180pub struct GovernmentAgency {
181    /// Unique agency identifier
182    pub agency_id: Uuid,
183    /// Agency name
184    pub name: String,
185    /// Agency type
186    pub agency_type: GovernmentAgencyType,
187    /// Country (ISO 3166-1 alpha-2)
188    pub country: String,
189}
190
191impl GovernmentAgency {
192    /// Create a new government agency.
193    pub fn new(
194        agency_id: Uuid,
195        name: &str,
196        agency_type: GovernmentAgencyType,
197        country: &str,
198    ) -> Self {
199        Self {
200            agency_id,
201            name: name.to_string(),
202            agency_type,
203            country: country.to_string(),
204        }
205    }
206}
207
208/// Type of government agency.
209#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
210#[serde(rename_all = "snake_case")]
211pub enum GovernmentAgencyType {
212    /// Tax authority
213    TaxAuthority,
214    /// Social security / benefits
215    SocialSecurity,
216    /// Unemployment benefits
217    Unemployment,
218    /// Veterans benefits
219    Veterans,
220    /// Local government
221    Local,
222    /// Federal government
223    Federal,
224    /// State/provincial government
225    State,
226}
227
228/// Pool of counterparties for transaction generation.
229#[derive(Debug, Clone, Default)]
230pub struct CounterpartyPool {
231    /// Merchants
232    pub merchants: Vec<Merchant>,
233    /// Employers
234    pub employers: Vec<Employer>,
235    /// Utilities
236    pub utilities: Vec<UtilityCompany>,
237    /// Government agencies
238    pub government_agencies: Vec<GovernmentAgency>,
239}
240
241impl CounterpartyPool {
242    /// Create a new empty pool.
243    pub fn new() -> Self {
244        Self::default()
245    }
246
247    /// Add a merchant.
248    pub fn add_merchant(&mut self, merchant: Merchant) {
249        self.merchants.push(merchant);
250    }
251
252    /// Add an employer.
253    pub fn add_employer(&mut self, employer: Employer) {
254        self.employers.push(employer);
255    }
256
257    /// Add a utility.
258    pub fn add_utility(&mut self, utility: UtilityCompany) {
259        self.utilities.push(utility);
260    }
261
262    /// Add a government agency.
263    pub fn add_government(&mut self, agency: GovernmentAgency) {
264        self.government_agencies.push(agency);
265    }
266
267    /// Create a standard counterparty pool.
268    pub fn standard() -> Self {
269        let mut pool = Self::new();
270
271        // Add common merchants
272        let merchants = [
273            ("Walmart", MerchantCategoryCode::GROCERY_STORES),
274            ("Amazon", MerchantCategoryCode(5999)),
275            ("Target", MerchantCategoryCode::DEPARTMENT_STORES),
276            ("Starbucks", MerchantCategoryCode::RESTAURANTS),
277            ("Shell Gas", MerchantCategoryCode::GAS_STATIONS),
278            ("CVS Pharmacy", MerchantCategoryCode::DRUG_STORES),
279            ("Netflix", MerchantCategoryCode(4899)),
280            ("Uber", MerchantCategoryCode(4121)),
281        ];
282
283        for (name, mcc) in merchants {
284            pool.add_merchant(Merchant::new(Uuid::new_v4(), name, mcc, "US"));
285        }
286
287        // Add common utilities
288        let utilities = [
289            ("Electric Company", UtilityType::Electric),
290            ("Gas Company", UtilityType::Gas),
291            ("Water Utility", UtilityType::Water),
292            ("Comcast", UtilityType::Internet),
293            ("AT&T", UtilityType::Phone),
294            ("State Farm Insurance", UtilityType::Insurance),
295        ];
296
297        for (name, utype) in utilities {
298            pool.add_utility(UtilityCompany::new(Uuid::new_v4(), name, utype, "US"));
299        }
300
301        // Add government agencies
302        pool.add_government(GovernmentAgency::new(
303            Uuid::new_v4(),
304            "IRS",
305            GovernmentAgencyType::TaxAuthority,
306            "US",
307        ));
308        pool.add_government(GovernmentAgency::new(
309            Uuid::new_v4(),
310            "Social Security Administration",
311            GovernmentAgencyType::SocialSecurity,
312            "US",
313        ));
314
315        pool
316    }
317}
318
319#[cfg(test)]
320mod tests {
321    use super::*;
322
323    #[test]
324    fn test_merchant_creation() {
325        let merchant = Merchant::new(
326            Uuid::new_v4(),
327            "Test Store",
328            MerchantCategoryCode::GROCERY_STORES,
329            "US",
330        );
331        assert!(!merchant.is_high_risk);
332    }
333
334    #[test]
335    fn test_counterparty_pool() {
336        let pool = CounterpartyPool::standard();
337        assert!(!pool.merchants.is_empty());
338        assert!(!pool.utilities.is_empty());
339    }
340
341    #[test]
342    fn test_pay_frequency() {
343        assert_eq!(PayFrequency::Weekly.interval_days(), 7);
344        assert_eq!(PayFrequency::Monthly.interval_days(), 30);
345    }
346}