rust_transaction_validator/
aml_compliance.rs

1//! AML/KYC compliance checks
2
3use crate::Transaction;
4use serde::{Deserialize, Serialize};
5
6/// AML compliance checker
7pub struct AMLChecker {
8    /// Suspicious activity thresholds
9    thresholds: AMLThresholds,
10    /// Sanctioned entities list
11    sanctioned_entities: Vec<String>,
12}
13
14/// AML thresholds (FinCEN guidelines)
15#[derive(Debug, Clone)]
16pub struct AMLThresholds {
17    /// Currency Transaction Report threshold (USD)
18    pub ctr_threshold: f64,
19    /// Suspicious Activity Report threshold (USD)
20    pub sar_threshold: f64,
21    /// Structuring detection threshold
22    pub structuring_threshold: f64,
23}
24
25impl Default for AMLThresholds {
26    fn default() -> Self {
27        Self {
28            ctr_threshold: 10000.0,        // FinCEN CTR requirement
29            sar_threshold: 5000.0,         // FinCEN SAR guideline
30            structuring_threshold: 9500.0, // Just under $10k
31        }
32    }
33}
34
35/// AML compliance result
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct AMLResult {
38    pub compliant: bool,
39    pub requires_ctr: bool,
40    pub requires_sar: bool,
41    pub red_flags: Vec<AMLRedFlag>,
42    pub risk_score: u8,
43}
44
45/// AML red flags
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct AMLRedFlag {
48    pub flag_type: RedFlagType,
49    pub description: String,
50    pub severity: AlertSeverity,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
54pub enum RedFlagType {
55    PotentialStructuring,
56    HighValueTransaction,
57    SanctionedEntity,
58    RapidMovement,
59    UnusualPattern,
60    CashIntensive,
61    CrossBorder,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
65pub enum AlertSeverity {
66    Low,
67    Medium,
68    High,
69    Critical,
70}
71
72impl AMLChecker {
73    /// Create new AML checker
74    pub fn new() -> Self {
75        Self {
76            thresholds: AMLThresholds::default(),
77            sanctioned_entities: vec![
78                "OFAC-SANCTIONED-001".to_string(),
79                "SANCTIONED-ENTITY-002".to_string(),
80            ],
81        }
82    }
83
84    /// Check transaction for AML compliance
85    pub fn check_compliance(&self, transaction: &Transaction) -> AMLResult {
86        let mut red_flags = Vec::new();
87        let mut risk_score = 0u8;
88
89        // Check if CTR required (>$10,000)
90        let requires_ctr = transaction.amount >= self.thresholds.ctr_threshold;
91
92        // Check if SAR may be required
93        let mut requires_sar = false;
94
95        // Structuring detection (amounts just below $10k)
96        if self.is_potential_structuring(transaction) {
97            red_flags.push(AMLRedFlag {
98                flag_type: RedFlagType::PotentialStructuring,
99                description: format!(
100                    "Amount {} is just below CTR threshold (potential structuring)",
101                    transaction.amount
102                ),
103                severity: AlertSeverity::High,
104            });
105            risk_score += 35;
106            requires_sar = true;
107        }
108
109        // High value transaction
110        if transaction.amount >= self.thresholds.ctr_threshold {
111            red_flags.push(AMLRedFlag {
112                flag_type: RedFlagType::HighValueTransaction,
113                description: format!(
114                    "High value transaction: {} (CTR required)",
115                    transaction.amount
116                ),
117                severity: AlertSeverity::Medium,
118            });
119            risk_score += 15;
120        }
121
122        // Sanctioned entity check
123        let from_sanctioned = transaction
124            .from_account
125            .as_deref()
126            .map(|a| self.is_sanctioned_entity(a))
127            .unwrap_or(false);
128        let to_sanctioned = transaction
129            .to_account
130            .as_deref()
131            .map(|a| self.is_sanctioned_entity(a))
132            .unwrap_or(false);
133        if from_sanctioned || to_sanctioned {
134            red_flags.push(AMLRedFlag {
135                flag_type: RedFlagType::SanctionedEntity,
136                description: "Transaction involves sanctioned entity".to_string(),
137                severity: AlertSeverity::Critical,
138            });
139            risk_score = 100;
140            requires_sar = true;
141        }
142
143        // Cross-border transaction check
144        if let Some(ref metadata) = transaction.metadata {
145            if metadata
146                .get("cross_border")
147                .map(|v| v == "true")
148                .unwrap_or(false)
149            {
150                red_flags.push(AMLRedFlag {
151                    flag_type: RedFlagType::CrossBorder,
152                    description: "Cross-border transaction requires additional due diligence"
153                        .to_string(),
154                    severity: AlertSeverity::Medium,
155                });
156                risk_score += 20;
157            }
158        }
159
160        // Cash intensive check
161        if matches!(
162            transaction.transaction_type,
163            crate::TransactionType::Deposit | crate::TransactionType::Withdrawal
164        ) && transaction.amount >= 5000.0
165        {
166            red_flags.push(AMLRedFlag {
167                flag_type: RedFlagType::CashIntensive,
168                description: format!(
169                    "Large cash {} of {}",
170                    transaction.transaction_type, transaction.amount
171                ),
172                severity: AlertSeverity::High,
173            });
174            risk_score += 25;
175        }
176
177        AMLResult {
178            compliant: risk_score < 75,
179            requires_ctr,
180            requires_sar,
181            red_flags,
182            risk_score: risk_score.min(100),
183        }
184    }
185
186    fn is_potential_structuring(&self, transaction: &Transaction) -> bool {
187        transaction.amount >= self.thresholds.structuring_threshold
188            && transaction.amount < self.thresholds.ctr_threshold
189    }
190
191    fn is_sanctioned_entity(&self, entity: &str) -> bool {
192        self.sanctioned_entities.iter().any(|s| entity.contains(s))
193    }
194
195    /// Add sanctioned entity to list
196    pub fn add_sanctioned_entity(&mut self, entity: String) {
197        if !self.sanctioned_entities.contains(&entity) {
198            self.sanctioned_entities.push(entity);
199        }
200    }
201
202    /// Check if entity is on sanctions list
203    pub fn check_sanctions_list(&self, entity: &str) -> bool {
204        self.is_sanctioned_entity(entity)
205    }
206}
207
208impl Default for AMLChecker {
209    fn default() -> Self {
210        Self::new()
211    }
212}
213
214/// KYC (Know Your Customer) validator
215pub struct KYCValidator;
216
217impl KYCValidator {
218    /// Validate customer information completeness
219    pub fn validate_customer_data(customer_data: &serde_json::Value) -> KYCValidationResult {
220        let mut missing_fields = Vec::new();
221        let mut warnings = Vec::new();
222
223        // Required fields for KYC
224        let required = [
225            "full_name",
226            "date_of_birth",
227            "address",
228            "id_number",
229            "id_type",
230        ];
231
232        for field in &required {
233            if customer_data.get(field).is_none() {
234                missing_fields.push(field.to_string());
235            }
236        }
237
238        // Check for enhanced due diligence triggers
239        if let Some(country) = customer_data.get("country").and_then(|v| v.as_str()) {
240            if Self::is_high_risk_jurisdiction(country) {
241                warnings.push(
242                    "Customer from high-risk jurisdiction - Enhanced Due Diligence required"
243                        .to_string(),
244                );
245            }
246        }
247
248        if let Some(pep) = customer_data
249            .get("politically_exposed_person")
250            .and_then(|v| v.as_bool())
251        {
252            if pep {
253                warnings.push(
254                    "Politically Exposed Person - Enhanced Due Diligence required".to_string(),
255                );
256            }
257        }
258
259        let requires_enhanced_dd = !warnings.is_empty();
260        KYCValidationResult {
261            valid: missing_fields.is_empty(),
262            missing_fields,
263            warnings,
264            requires_enhanced_dd,
265        }
266    }
267
268    fn is_high_risk_jurisdiction(country: &str) -> bool {
269        // Simplified check
270        matches!(country, "KP" | "IR" | "SY" | "CU" | "SD")
271    }
272}
273
274/// KYC validation result
275#[derive(Debug, Clone, Serialize, Deserialize)]
276pub struct KYCValidationResult {
277    pub valid: bool,
278    pub missing_fields: Vec<String>,
279    pub warnings: Vec<String>,
280    pub requires_enhanced_dd: bool,
281}
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286    use chrono::Utc;
287
288    fn create_test_transaction(amount: f64, txn_type: crate::TransactionType) -> Transaction {
289        Transaction {
290            transaction_id: "TXN-001".to_string(),
291            from_account: Some("ACC-123".to_string()),
292            to_account: Some("ACC-456".to_string()),
293            amount,
294            currency: "USD".to_string(),
295            timestamp: Utc::now(),
296            transaction_type: txn_type,
297            user_id: "USER-001".to_string(),
298            metadata: None,
299        }
300    }
301
302    #[test]
303    fn test_ctr_requirement() {
304        let checker = AMLChecker::new();
305        let txn = create_test_transaction(15000.0, crate::TransactionType::Transfer);
306        let result = checker.check_compliance(&txn);
307
308        assert!(result.requires_ctr);
309        assert!(result
310            .red_flags
311            .iter()
312            .any(|f| f.flag_type == RedFlagType::HighValueTransaction));
313    }
314
315    #[test]
316    fn test_structuring_detection() {
317        let checker = AMLChecker::new();
318        let txn = create_test_transaction(9800.0, crate::TransactionType::Transfer);
319        let result = checker.check_compliance(&txn);
320
321        assert!(result.requires_sar);
322        assert!(result
323            .red_flags
324            .iter()
325            .any(|f| f.flag_type == RedFlagType::PotentialStructuring));
326    }
327
328    #[test]
329    fn test_sanctioned_entity() {
330        let checker = AMLChecker::new();
331        let mut txn = create_test_transaction(1000.0, crate::TransactionType::Transfer);
332        txn.from_account = Some("OFAC-SANCTIONED-001".to_string());
333
334        let result = checker.check_compliance(&txn);
335
336        assert!(!result.compliant);
337        assert_eq!(result.risk_score, 100);
338        assert!(result
339            .red_flags
340            .iter()
341            .any(|f| f.flag_type == RedFlagType::SanctionedEntity));
342    }
343
344    #[test]
345    fn test_cash_intensive() {
346        let checker = AMLChecker::new();
347        let txn = create_test_transaction(8000.0, crate::TransactionType::Deposit);
348        let result = checker.check_compliance(&txn);
349
350        assert!(result
351            .red_flags
352            .iter()
353            .any(|f| f.flag_type == RedFlagType::CashIntensive));
354    }
355
356    #[test]
357    fn test_cross_border() {
358        let checker = AMLChecker::new();
359        let mut txn = create_test_transaction(5000.0, crate::TransactionType::Transfer);
360        let mut metadata = std::collections::HashMap::new();
361        metadata.insert("cross_border".to_string(), "true".to_string());
362        txn.metadata = Some(metadata);
363
364        let result = checker.check_compliance(&txn);
365
366        assert!(result
367            .red_flags
368            .iter()
369            .any(|f| f.flag_type == RedFlagType::CrossBorder));
370    }
371
372    #[test]
373    fn test_kyc_validation() {
374        let complete_data = serde_json::json!({
375            "full_name": "John Doe",
376            "date_of_birth": "1990-01-01",
377            "address": "123 Main St",
378            "id_number": "123456789",
379            "id_type": "passport",
380            "country": "US"
381        });
382
383        let result = KYCValidator::validate_customer_data(&complete_data);
384        assert!(result.valid);
385        assert!(result.missing_fields.is_empty());
386    }
387
388    #[test]
389    fn test_kyc_missing_fields() {
390        let incomplete_data = serde_json::json!({
391            "full_name": "John Doe"
392        });
393
394        let result = KYCValidator::validate_customer_data(&incomplete_data);
395        assert!(!result.valid);
396        assert!(!result.missing_fields.is_empty());
397    }
398
399    #[test]
400    fn test_kyc_enhanced_dd() {
401        let pep_data = serde_json::json!({
402            "full_name": "John Doe",
403            "date_of_birth": "1990-01-01",
404            "address": "123 Main St",
405            "id_number": "123456789",
406            "id_type": "passport",
407            "politically_exposed_person": true
408        });
409
410        let result = KYCValidator::validate_customer_data(&pep_data);
411        assert!(result.requires_enhanced_dd);
412        assert!(!result.warnings.is_empty());
413    }
414}