use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BankingConfig {
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default)]
pub population: PopulationConfig,
#[serde(default)]
pub products: ProductConfig,
#[serde(default)]
pub compliance: ComplianceConfig,
#[serde(default)]
pub typologies: TypologyConfig,
#[serde(default)]
pub spoofing: SpoofingConfig,
#[serde(default)]
pub output: BankingOutputConfig,
#[serde(default)]
pub temporal: TemporalBehaviorConfig,
#[serde(default)]
pub device: DeviceFingerprintConfig,
}
fn default_true() -> bool {
true
}
impl Default for BankingConfig {
fn default() -> Self {
Self {
enabled: true,
population: PopulationConfig::default(),
products: ProductConfig::default(),
compliance: ComplianceConfig::default(),
typologies: TypologyConfig::default(),
spoofing: SpoofingConfig::default(),
output: BankingOutputConfig::default(),
temporal: TemporalBehaviorConfig::default(),
device: DeviceFingerprintConfig::default(),
}
}
}
impl BankingConfig {
pub fn small() -> Self {
Self {
population: PopulationConfig {
retail_customers: 100,
business_customers: 20,
trusts: 5,
..Default::default()
},
..Default::default()
}
}
pub fn medium() -> Self {
Self {
population: PopulationConfig {
retail_customers: 1_000,
business_customers: 200,
trusts: 50,
..Default::default()
},
..Default::default()
}
}
pub fn large() -> Self {
Self {
population: PopulationConfig {
retail_customers: 10_000,
business_customers: 1_000,
trusts: 100,
..Default::default()
},
..Default::default()
}
}
pub fn validate(&self) -> Result<(), Vec<String>> {
let mut errors = Vec::new();
if self.population.retail_customers == 0
&& self.population.business_customers == 0
&& self.population.trusts == 0
{
errors.push("At least one customer type must have non-zero count".to_string());
}
let retail_sum: f64 = self.population.retail_persona_weights.values().sum();
if (retail_sum - 1.0).abs() > 0.01 {
errors.push(format!(
"Retail persona weights must sum to 1.0, got {retail_sum}"
));
}
let total_suspicious = self.typologies.structuring_rate
+ self.typologies.funnel_rate
+ self.typologies.layering_rate
+ self.typologies.mule_rate
+ self.typologies.fraud_rate;
if total_suspicious > self.typologies.suspicious_rate + 0.001 {
errors.push(format!(
"Sum of typology rates ({}) exceeds suspicious_rate ({})",
total_suspicious, self.typologies.suspicious_rate
));
}
if self.spoofing.intensity < 0.0 || self.spoofing.intensity > 1.0 {
errors.push("Spoofing intensity must be between 0.0 and 1.0".to_string());
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PopulationConfig {
pub retail_customers: u32,
pub retail_persona_weights: HashMap<String, f64>,
pub business_customers: u32,
pub business_persona_weights: HashMap<String, f64>,
pub trusts: u32,
pub household_rate: f64,
pub avg_household_size: f64,
pub period_months: u32,
pub start_date: String,
}
impl Default for PopulationConfig {
fn default() -> Self {
let mut retail_weights = HashMap::new();
retail_weights.insert("student".to_string(), 0.15);
retail_weights.insert("early_career".to_string(), 0.25);
retail_weights.insert("mid_career".to_string(), 0.30);
retail_weights.insert("retiree".to_string(), 0.15);
retail_weights.insert("high_net_worth".to_string(), 0.05);
retail_weights.insert("gig_worker".to_string(), 0.10);
let mut business_weights = HashMap::new();
business_weights.insert("small_business".to_string(), 0.50);
business_weights.insert("mid_market".to_string(), 0.25);
business_weights.insert("enterprise".to_string(), 0.05);
business_weights.insert("cash_intensive".to_string(), 0.10);
business_weights.insert("import_export".to_string(), 0.05);
business_weights.insert("professional_services".to_string(), 0.05);
Self {
retail_customers: 10_000,
retail_persona_weights: retail_weights,
business_customers: 1_000,
business_persona_weights: business_weights,
trusts: 100,
household_rate: 0.4,
avg_household_size: 2.3,
period_months: 12,
start_date: "2024-01-01".to_string(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProductConfig {
pub cash_intensity: f64,
pub cross_border_rate: f64,
pub card_vs_transfer: f64,
pub avg_accounts_retail: f64,
pub avg_accounts_business: f64,
pub debit_card_rate: f64,
pub international_rate: f64,
}
impl Default for ProductConfig {
fn default() -> Self {
Self {
cash_intensity: 0.15,
cross_border_rate: 0.05,
card_vs_transfer: 0.6,
avg_accounts_retail: 1.5,
avg_accounts_business: 2.5,
debit_card_rate: 0.85,
international_rate: 0.10,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComplianceConfig {
pub risk_appetite: RiskAppetite,
pub kyc_completeness: f64,
pub high_risk_tolerance: f64,
pub pep_rate: f64,
pub edd_threshold: u64,
}
impl Default for ComplianceConfig {
fn default() -> Self {
Self {
risk_appetite: RiskAppetite::Medium,
kyc_completeness: 0.95,
high_risk_tolerance: 0.05,
pep_rate: 0.01,
edd_threshold: 50_000,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum RiskAppetite {
Low,
#[default]
Medium,
High,
}
impl RiskAppetite {
pub fn high_risk_multiplier(&self) -> f64 {
match self {
Self::Low => 0.5,
Self::Medium => 1.0,
Self::High => 2.0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TypologyConfig {
pub suspicious_rate: f64,
pub structuring_rate: f64,
pub funnel_rate: f64,
pub layering_rate: f64,
pub mule_rate: f64,
pub fraud_rate: f64,
pub sophistication: SophisticationDistribution,
pub detectability: f64,
pub round_tripping_rate: f64,
pub trade_based_rate: f64,
#[serde(default = "default_synth_id_rate")]
pub synthetic_identity_rate: f64,
#[serde(default = "default_crypto_rate")]
pub crypto_integration_rate: f64,
#[serde(default = "default_sanctions_rate")]
pub sanctions_evasion_rate: f64,
#[serde(default = "default_false_positive_rate")]
pub false_positive_rate: f64,
#[serde(default = "default_co_occurrence_rate")]
pub co_occurrence_rate: f64,
#[serde(default = "default_network_rate")]
pub network_typology_rate: f64,
#[serde(default = "default_payment_bridge_rate")]
pub payment_bridge_rate: f64,
}
fn default_synth_id_rate() -> f64 {
0.001
}
fn default_crypto_rate() -> f64 {
0.001
}
fn default_sanctions_rate() -> f64 {
0.0005
}
fn default_false_positive_rate() -> f64 {
0.05
}
fn default_co_occurrence_rate() -> f64 {
0.10
}
fn default_network_rate() -> f64 {
0.05
}
fn default_payment_bridge_rate() -> f64 {
0.75
}
impl Default for TypologyConfig {
fn default() -> Self {
Self {
suspicious_rate: 0.02,
structuring_rate: 0.004,
funnel_rate: 0.003,
layering_rate: 0.003,
mule_rate: 0.005,
fraud_rate: 0.005,
sophistication: SophisticationDistribution::default(),
detectability: 0.5,
round_tripping_rate: 0.001,
trade_based_rate: 0.001,
synthetic_identity_rate: 0.001,
crypto_integration_rate: 0.001,
sanctions_evasion_rate: 0.0005,
false_positive_rate: 0.05,
co_occurrence_rate: 0.10,
network_typology_rate: 0.05,
payment_bridge_rate: 0.75,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SophisticationDistribution {
pub basic: f64,
pub standard: f64,
pub professional: f64,
pub advanced: f64,
}
impl Default for SophisticationDistribution {
fn default() -> Self {
Self {
basic: 0.4,
standard: 0.35,
professional: 0.2,
advanced: 0.05,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SpoofingConfig {
pub enabled: bool,
pub intensity: f64,
pub spoof_timing: bool,
pub spoof_amounts: bool,
pub spoof_merchants: bool,
pub spoof_geography: bool,
pub add_delays: bool,
}
impl Default for SpoofingConfig {
fn default() -> Self {
Self {
enabled: true,
intensity: 0.3,
spoof_timing: true,
spoof_amounts: true,
spoof_merchants: true,
spoof_geography: false,
add_delays: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BankingOutputConfig {
pub directory: String,
pub include_customers: bool,
pub include_accounts: bool,
pub include_transactions: bool,
pub include_counterparties: bool,
pub include_beneficial_ownership: bool,
pub include_transaction_labels: bool,
pub include_entity_labels: bool,
pub include_relationship_labels: bool,
pub include_case_narratives: bool,
pub include_graph: bool,
}
impl Default for BankingOutputConfig {
fn default() -> Self {
Self {
directory: "banking".to_string(),
include_customers: true,
include_accounts: true,
include_transactions: true,
include_counterparties: true,
include_beneficial_ownership: true,
include_transaction_labels: true,
include_entity_labels: true,
include_relationship_labels: true,
include_case_narratives: true,
include_graph: true,
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = BankingConfig::default();
assert!(config.enabled);
assert!(config.validate().is_ok());
}
#[test]
fn test_small_config() {
let config = BankingConfig::small();
assert_eq!(config.population.retail_customers, 100);
assert!(config.validate().is_ok());
}
#[test]
fn test_validation_empty_population() {
let config = BankingConfig {
population: PopulationConfig {
retail_customers: 0,
business_customers: 0,
trusts: 0,
..Default::default()
},
..Default::default()
};
assert!(config.validate().is_err());
}
#[test]
fn test_persona_weights() {
let config = BankingConfig::default();
let sum: f64 = config.population.retail_persona_weights.values().sum();
assert!((sum - 1.0).abs() < 0.01);
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TemporalBehaviorConfig {
#[serde(default = "default_true")]
pub enable_lifecycle_phases: bool,
#[serde(default = "default_true")]
pub enable_behavioral_drift: bool,
#[serde(default = "default_true")]
pub enable_velocity_features: bool,
#[serde(default = "default_true")]
pub enable_impossible_travel: bool,
#[serde(default = "default_drift_rate")]
pub drift_rate: f64,
#[serde(default = "default_sudden_ratio")]
pub sudden_drift_ratio: f64,
#[serde(default = "default_impossible_travel_rate")]
pub impossible_travel_rate: f64,
}
fn default_drift_rate() -> f64 {
0.05
}
fn default_sudden_ratio() -> f64 {
0.30
}
fn default_impossible_travel_rate() -> f64 {
0.02
}
impl Default for TemporalBehaviorConfig {
fn default() -> Self {
Self {
enable_lifecycle_phases: true,
enable_behavioral_drift: true,
enable_velocity_features: true,
enable_impossible_travel: true,
drift_rate: 0.05,
sudden_drift_ratio: 0.30,
impossible_travel_rate: 0.02,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeviceFingerprintConfig {
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default = "default_device_reuse")]
pub device_reuse_rate: f64,
#[serde(default = "default_multi_device")]
pub multi_device_rate: f64,
}
fn default_device_reuse() -> f64 {
0.85
}
fn default_multi_device() -> f64 {
0.30
}
impl Default for DeviceFingerprintConfig {
fn default() -> Self {
Self {
enabled: true,
device_reuse_rate: 0.85,
multi_device_rate: 0.30,
}
}
}