1use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct BankingConfig {
9 #[serde(default = "default_true")]
11 pub enabled: bool,
12 #[serde(default)]
14 pub population: PopulationConfig,
15 #[serde(default)]
17 pub products: ProductConfig,
18 #[serde(default)]
20 pub compliance: ComplianceConfig,
21 #[serde(default)]
23 pub typologies: TypologyConfig,
24 #[serde(default)]
26 pub spoofing: SpoofingConfig,
27 #[serde(default)]
29 pub output: BankingOutputConfig,
30}
31
32fn default_true() -> bool {
33 true
34}
35
36impl Default for BankingConfig {
37 fn default() -> Self {
38 Self {
39 enabled: true,
40 population: PopulationConfig::default(),
41 products: ProductConfig::default(),
42 compliance: ComplianceConfig::default(),
43 typologies: TypologyConfig::default(),
44 spoofing: SpoofingConfig::default(),
45 output: BankingOutputConfig::default(),
46 }
47 }
48}
49
50impl BankingConfig {
51 pub fn small() -> Self {
53 Self {
54 population: PopulationConfig {
55 retail_customers: 100,
56 business_customers: 20,
57 trusts: 5,
58 ..Default::default()
59 },
60 ..Default::default()
61 }
62 }
63
64 pub fn medium() -> Self {
66 Self {
67 population: PopulationConfig {
68 retail_customers: 1_000,
69 business_customers: 200,
70 trusts: 50,
71 ..Default::default()
72 },
73 ..Default::default()
74 }
75 }
76
77 pub fn large() -> Self {
79 Self {
80 population: PopulationConfig {
81 retail_customers: 10_000,
82 business_customers: 1_000,
83 trusts: 100,
84 ..Default::default()
85 },
86 ..Default::default()
87 }
88 }
89
90 pub fn validate(&self) -> Result<(), Vec<String>> {
92 let mut errors = Vec::new();
93
94 if self.population.retail_customers == 0
96 && self.population.business_customers == 0
97 && self.population.trusts == 0
98 {
99 errors.push("At least one customer type must have non-zero count".to_string());
100 }
101
102 let retail_sum: f64 = self.population.retail_persona_weights.values().sum();
104 if (retail_sum - 1.0).abs() > 0.01 {
105 errors.push(format!(
106 "Retail persona weights must sum to 1.0, got {retail_sum}"
107 ));
108 }
109
110 let total_suspicious = self.typologies.structuring_rate
112 + self.typologies.funnel_rate
113 + self.typologies.layering_rate
114 + self.typologies.mule_rate
115 + self.typologies.fraud_rate;
116 if total_suspicious > self.typologies.suspicious_rate + 0.001 {
117 errors.push(format!(
118 "Sum of typology rates ({}) exceeds suspicious_rate ({})",
119 total_suspicious, self.typologies.suspicious_rate
120 ));
121 }
122
123 if self.spoofing.intensity < 0.0 || self.spoofing.intensity > 1.0 {
125 errors.push("Spoofing intensity must be between 0.0 and 1.0".to_string());
126 }
127
128 if errors.is_empty() {
129 Ok(())
130 } else {
131 Err(errors)
132 }
133 }
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct PopulationConfig {
139 pub retail_customers: u32,
141 pub retail_persona_weights: HashMap<String, f64>,
143 pub business_customers: u32,
145 pub business_persona_weights: HashMap<String, f64>,
147 pub trusts: u32,
149 pub household_rate: f64,
151 pub avg_household_size: f64,
153 pub period_months: u32,
155 pub start_date: String,
157}
158
159impl Default for PopulationConfig {
160 fn default() -> Self {
161 let mut retail_weights = HashMap::new();
162 retail_weights.insert("student".to_string(), 0.15);
163 retail_weights.insert("early_career".to_string(), 0.25);
164 retail_weights.insert("mid_career".to_string(), 0.30);
165 retail_weights.insert("retiree".to_string(), 0.15);
166 retail_weights.insert("high_net_worth".to_string(), 0.05);
167 retail_weights.insert("gig_worker".to_string(), 0.10);
168
169 let mut business_weights = HashMap::new();
170 business_weights.insert("small_business".to_string(), 0.50);
171 business_weights.insert("mid_market".to_string(), 0.25);
172 business_weights.insert("enterprise".to_string(), 0.05);
173 business_weights.insert("cash_intensive".to_string(), 0.10);
174 business_weights.insert("import_export".to_string(), 0.05);
175 business_weights.insert("professional_services".to_string(), 0.05);
176
177 Self {
178 retail_customers: 10_000,
179 retail_persona_weights: retail_weights,
180 business_customers: 1_000,
181 business_persona_weights: business_weights,
182 trusts: 100,
183 household_rate: 0.4,
184 avg_household_size: 2.3,
185 period_months: 12,
186 start_date: "2024-01-01".to_string(),
187 }
188 }
189}
190
191#[derive(Debug, Clone, Serialize, Deserialize)]
193pub struct ProductConfig {
194 pub cash_intensity: f64,
196 pub cross_border_rate: f64,
198 pub card_vs_transfer: f64,
200 pub avg_accounts_retail: f64,
202 pub avg_accounts_business: f64,
204 pub debit_card_rate: f64,
206 pub international_rate: f64,
208}
209
210impl Default for ProductConfig {
211 fn default() -> Self {
212 Self {
213 cash_intensity: 0.15,
214 cross_border_rate: 0.05,
215 card_vs_transfer: 0.6,
216 avg_accounts_retail: 1.5,
217 avg_accounts_business: 2.5,
218 debit_card_rate: 0.85,
219 international_rate: 0.10,
220 }
221 }
222}
223
224#[derive(Debug, Clone, Serialize, Deserialize)]
226pub struct ComplianceConfig {
227 pub risk_appetite: RiskAppetite,
229 pub kyc_completeness: f64,
231 pub high_risk_tolerance: f64,
233 pub pep_rate: f64,
235 pub edd_threshold: u64,
237}
238
239impl Default for ComplianceConfig {
240 fn default() -> Self {
241 Self {
242 risk_appetite: RiskAppetite::Medium,
243 kyc_completeness: 0.95,
244 high_risk_tolerance: 0.05,
245 pep_rate: 0.01,
246 edd_threshold: 50_000,
247 }
248 }
249}
250
251#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
253#[serde(rename_all = "snake_case")]
254pub enum RiskAppetite {
255 Low,
257 #[default]
259 Medium,
260 High,
262}
263
264impl RiskAppetite {
265 pub fn high_risk_multiplier(&self) -> f64 {
267 match self {
268 Self::Low => 0.5,
269 Self::Medium => 1.0,
270 Self::High => 2.0,
271 }
272 }
273}
274
275#[derive(Debug, Clone, Serialize, Deserialize)]
277pub struct TypologyConfig {
278 pub suspicious_rate: f64,
280 pub structuring_rate: f64,
282 pub funnel_rate: f64,
284 pub layering_rate: f64,
286 pub mule_rate: f64,
288 pub fraud_rate: f64,
290 pub sophistication: SophisticationDistribution,
292 pub detectability: f64,
294 pub round_tripping_rate: f64,
296 pub trade_based_rate: f64,
298}
299
300impl Default for TypologyConfig {
301 fn default() -> Self {
302 Self {
303 suspicious_rate: 0.02,
304 structuring_rate: 0.004,
305 funnel_rate: 0.003,
306 layering_rate: 0.003,
307 mule_rate: 0.005,
308 fraud_rate: 0.005,
309 sophistication: SophisticationDistribution::default(),
310 detectability: 0.5,
311 round_tripping_rate: 0.001,
312 trade_based_rate: 0.001,
313 }
314 }
315}
316
317#[derive(Debug, Clone, Serialize, Deserialize)]
319pub struct SophisticationDistribution {
320 pub basic: f64,
322 pub standard: f64,
324 pub professional: f64,
326 pub advanced: f64,
328}
329
330impl Default for SophisticationDistribution {
331 fn default() -> Self {
332 Self {
333 basic: 0.4,
334 standard: 0.35,
335 professional: 0.2,
336 advanced: 0.05,
337 }
338 }
339}
340
341#[derive(Debug, Clone, Serialize, Deserialize)]
343pub struct SpoofingConfig {
344 pub enabled: bool,
346 pub intensity: f64,
348 pub spoof_timing: bool,
350 pub spoof_amounts: bool,
352 pub spoof_merchants: bool,
354 pub spoof_geography: bool,
356 pub add_delays: bool,
358}
359
360impl Default for SpoofingConfig {
361 fn default() -> Self {
362 Self {
363 enabled: true,
364 intensity: 0.3,
365 spoof_timing: true,
366 spoof_amounts: true,
367 spoof_merchants: true,
368 spoof_geography: false,
369 add_delays: true,
370 }
371 }
372}
373
374#[derive(Debug, Clone, Serialize, Deserialize)]
376pub struct BankingOutputConfig {
377 pub directory: String,
379 pub include_customers: bool,
381 pub include_accounts: bool,
383 pub include_transactions: bool,
385 pub include_counterparties: bool,
387 pub include_beneficial_ownership: bool,
389 pub include_transaction_labels: bool,
391 pub include_entity_labels: bool,
393 pub include_relationship_labels: bool,
395 pub include_case_narratives: bool,
397 pub include_graph: bool,
399}
400
401impl Default for BankingOutputConfig {
402 fn default() -> Self {
403 Self {
404 directory: "banking".to_string(),
405 include_customers: true,
406 include_accounts: true,
407 include_transactions: true,
408 include_counterparties: true,
409 include_beneficial_ownership: true,
410 include_transaction_labels: true,
411 include_entity_labels: true,
412 include_relationship_labels: true,
413 include_case_narratives: true,
414 include_graph: true,
415 }
416 }
417}
418
419#[cfg(test)]
420#[allow(clippy::unwrap_used)]
421mod tests {
422 use super::*;
423
424 #[test]
425 fn test_default_config() {
426 let config = BankingConfig::default();
427 assert!(config.enabled);
428 assert!(config.validate().is_ok());
429 }
430
431 #[test]
432 fn test_small_config() {
433 let config = BankingConfig::small();
434 assert_eq!(config.population.retail_customers, 100);
435 assert!(config.validate().is_ok());
436 }
437
438 #[test]
439 fn test_validation_empty_population() {
440 let config = BankingConfig {
441 population: PopulationConfig {
442 retail_customers: 0,
443 business_customers: 0,
444 trusts: 0,
445 ..Default::default()
446 },
447 ..Default::default()
448 };
449 assert!(config.validate().is_err());
450 }
451
452 #[test]
453 fn test_persona_weights() {
454 let config = BankingConfig::default();
455 let sum: f64 = config.population.retail_persona_weights.values().sum();
456 assert!((sum - 1.0).abs() < 0.01);
457 }
458}