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 {}",
107 retail_sum
108 ));
109 }
110
111 let total_suspicious = self.typologies.structuring_rate
113 + self.typologies.funnel_rate
114 + self.typologies.layering_rate
115 + self.typologies.mule_rate
116 + self.typologies.fraud_rate;
117 if total_suspicious > self.typologies.suspicious_rate + 0.001 {
118 errors.push(format!(
119 "Sum of typology rates ({}) exceeds suspicious_rate ({})",
120 total_suspicious, self.typologies.suspicious_rate
121 ));
122 }
123
124 if self.spoofing.intensity < 0.0 || self.spoofing.intensity > 1.0 {
126 errors.push("Spoofing intensity must be between 0.0 and 1.0".to_string());
127 }
128
129 if errors.is_empty() {
130 Ok(())
131 } else {
132 Err(errors)
133 }
134 }
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct PopulationConfig {
140 pub retail_customers: u32,
142 pub retail_persona_weights: HashMap<String, f64>,
144 pub business_customers: u32,
146 pub business_persona_weights: HashMap<String, f64>,
148 pub trusts: u32,
150 pub household_rate: f64,
152 pub avg_household_size: f64,
154 pub period_months: u32,
156 pub start_date: String,
158}
159
160impl Default for PopulationConfig {
161 fn default() -> Self {
162 let mut retail_weights = HashMap::new();
163 retail_weights.insert("student".to_string(), 0.15);
164 retail_weights.insert("early_career".to_string(), 0.25);
165 retail_weights.insert("mid_career".to_string(), 0.30);
166 retail_weights.insert("retiree".to_string(), 0.15);
167 retail_weights.insert("high_net_worth".to_string(), 0.05);
168 retail_weights.insert("gig_worker".to_string(), 0.10);
169
170 let mut business_weights = HashMap::new();
171 business_weights.insert("small_business".to_string(), 0.50);
172 business_weights.insert("mid_market".to_string(), 0.25);
173 business_weights.insert("enterprise".to_string(), 0.05);
174 business_weights.insert("cash_intensive".to_string(), 0.10);
175 business_weights.insert("import_export".to_string(), 0.05);
176 business_weights.insert("professional_services".to_string(), 0.05);
177
178 Self {
179 retail_customers: 10_000,
180 retail_persona_weights: retail_weights,
181 business_customers: 1_000,
182 business_persona_weights: business_weights,
183 trusts: 100,
184 household_rate: 0.4,
185 avg_household_size: 2.3,
186 period_months: 12,
187 start_date: "2024-01-01".to_string(),
188 }
189 }
190}
191
192#[derive(Debug, Clone, Serialize, Deserialize)]
194pub struct ProductConfig {
195 pub cash_intensity: f64,
197 pub cross_border_rate: f64,
199 pub card_vs_transfer: f64,
201 pub avg_accounts_retail: f64,
203 pub avg_accounts_business: f64,
205 pub debit_card_rate: f64,
207 pub international_rate: f64,
209}
210
211impl Default for ProductConfig {
212 fn default() -> Self {
213 Self {
214 cash_intensity: 0.15,
215 cross_border_rate: 0.05,
216 card_vs_transfer: 0.6,
217 avg_accounts_retail: 1.5,
218 avg_accounts_business: 2.5,
219 debit_card_rate: 0.85,
220 international_rate: 0.10,
221 }
222 }
223}
224
225#[derive(Debug, Clone, Serialize, Deserialize)]
227pub struct ComplianceConfig {
228 pub risk_appetite: RiskAppetite,
230 pub kyc_completeness: f64,
232 pub high_risk_tolerance: f64,
234 pub pep_rate: f64,
236 pub edd_threshold: u64,
238}
239
240impl Default for ComplianceConfig {
241 fn default() -> Self {
242 Self {
243 risk_appetite: RiskAppetite::Medium,
244 kyc_completeness: 0.95,
245 high_risk_tolerance: 0.05,
246 pep_rate: 0.01,
247 edd_threshold: 50_000,
248 }
249 }
250}
251
252#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
254#[serde(rename_all = "snake_case")]
255pub enum RiskAppetite {
256 Low,
258 #[default]
260 Medium,
261 High,
263}
264
265impl RiskAppetite {
266 pub fn high_risk_multiplier(&self) -> f64 {
268 match self {
269 Self::Low => 0.5,
270 Self::Medium => 1.0,
271 Self::High => 2.0,
272 }
273 }
274}
275
276#[derive(Debug, Clone, Serialize, Deserialize)]
278pub struct TypologyConfig {
279 pub suspicious_rate: f64,
281 pub structuring_rate: f64,
283 pub funnel_rate: f64,
285 pub layering_rate: f64,
287 pub mule_rate: f64,
289 pub fraud_rate: f64,
291 pub sophistication: SophisticationDistribution,
293 pub detectability: f64,
295 pub round_tripping_rate: f64,
297 pub trade_based_rate: f64,
299}
300
301impl Default for TypologyConfig {
302 fn default() -> Self {
303 Self {
304 suspicious_rate: 0.02,
305 structuring_rate: 0.004,
306 funnel_rate: 0.003,
307 layering_rate: 0.003,
308 mule_rate: 0.005,
309 fraud_rate: 0.005,
310 sophistication: SophisticationDistribution::default(),
311 detectability: 0.5,
312 round_tripping_rate: 0.001,
313 trade_based_rate: 0.001,
314 }
315 }
316}
317
318#[derive(Debug, Clone, Serialize, Deserialize)]
320pub struct SophisticationDistribution {
321 pub basic: f64,
323 pub standard: f64,
325 pub professional: f64,
327 pub advanced: f64,
329}
330
331impl Default for SophisticationDistribution {
332 fn default() -> Self {
333 Self {
334 basic: 0.4,
335 standard: 0.35,
336 professional: 0.2,
337 advanced: 0.05,
338 }
339 }
340}
341
342#[derive(Debug, Clone, Serialize, Deserialize)]
344pub struct SpoofingConfig {
345 pub enabled: bool,
347 pub intensity: f64,
349 pub spoof_timing: bool,
351 pub spoof_amounts: bool,
353 pub spoof_merchants: bool,
355 pub spoof_geography: bool,
357 pub add_delays: bool,
359}
360
361impl Default for SpoofingConfig {
362 fn default() -> Self {
363 Self {
364 enabled: true,
365 intensity: 0.3,
366 spoof_timing: true,
367 spoof_amounts: true,
368 spoof_merchants: true,
369 spoof_geography: false,
370 add_delays: true,
371 }
372 }
373}
374
375#[derive(Debug, Clone, Serialize, Deserialize)]
377pub struct BankingOutputConfig {
378 pub directory: String,
380 pub include_customers: bool,
382 pub include_accounts: bool,
384 pub include_transactions: bool,
386 pub include_counterparties: bool,
388 pub include_beneficial_ownership: bool,
390 pub include_transaction_labels: bool,
392 pub include_entity_labels: bool,
394 pub include_relationship_labels: bool,
396 pub include_case_narratives: bool,
398 pub include_graph: bool,
400}
401
402impl Default for BankingOutputConfig {
403 fn default() -> Self {
404 Self {
405 directory: "banking".to_string(),
406 include_customers: true,
407 include_accounts: true,
408 include_transactions: true,
409 include_counterparties: true,
410 include_beneficial_ownership: true,
411 include_transaction_labels: true,
412 include_entity_labels: true,
413 include_relationship_labels: true,
414 include_case_narratives: true,
415 include_graph: true,
416 }
417 }
418}
419
420#[cfg(test)]
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}