1pub mod aml_compliance;
34pub mod fraud_patterns;
35pub mod sanctions;
36pub mod geographic_risk;
37pub mod network_analysis;
38
39pub use aml_compliance::{AMLChecker, AMLResult, KYCValidationResult, KYCValidator};
40pub use fraud_patterns::{FraudDetector, FraudScore, FraudThresholds, RiskLevel};
41pub use sanctions::{SanctionsScreener, SanctionsResult, SanctionsList};
42pub use geographic_risk::{GeographicRiskScorer, CountryRisk, JurisdictionRisk};
43pub use network_analysis::{TransactionGraph, NetworkAnalyzer, SuspiciousPattern};
44
45use chrono::{DateTime, Duration, Timelike, Utc};
46use regex::Regex;
47use serde::{Deserialize, Serialize};
48use std::collections::HashMap;
49use thiserror::Error;
50
51#[derive(Error, Debug, Clone, Serialize, Deserialize)]
53pub enum ValidationError {
54 #[error("Invalid amount: {0}")]
55 InvalidAmount(String),
56
57 #[error("Invalid account number: {0}")]
58 InvalidAccount(String),
59
60 #[error("Duplicate transaction detected: {0}")]
61 DuplicateTransaction(String),
62
63 #[error("Fraud pattern detected: {0}")]
64 FraudDetected(String),
65
66 #[error("Compliance check failed: {0}")]
67 ComplianceFailed(String),
68
69 #[error("Business rule violation: {0}")]
70 BusinessRuleViolation(String),
71
72 #[error("Velocity check failed: {0}")]
73 VelocityViolation(String),
74
75 #[error("Risk threshold exceeded: {0}")]
76 RiskThresholdExceeded(String),
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct RiskBreakdown {
82 pub amount_risk: u8,
83 pub velocity_risk: u8,
84 pub pattern_risk: u8,
85 pub time_risk: u8,
86 pub total_score: u8,
87}
88
89impl RiskBreakdown {
90 fn new() -> Self {
91 Self {
92 amount_risk: 0,
93 velocity_risk: 0,
94 pattern_risk: 0,
95 time_risk: 0,
96 total_score: 0,
97 }
98 }
99
100 fn calculate_total(&mut self) {
101 self.total_score = self
102 .amount_risk
103 .saturating_add(self.velocity_risk)
104 .saturating_add(self.pattern_risk)
105 .saturating_add(self.time_risk)
106 .min(100);
107 }
108}
109
110#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
112pub enum TransactionType {
113 Deposit,
114 Withdrawal,
115 Transfer,
116 Payment,
117 WireTransfer,
118}
119
120impl std::fmt::Display for TransactionType {
121 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122 match self {
123 TransactionType::Deposit => write!(f, "deposit"),
124 TransactionType::Withdrawal => write!(f, "withdrawal"),
125 TransactionType::Transfer => write!(f, "transfer"),
126 TransactionType::Payment => write!(f, "payment"),
127 TransactionType::WireTransfer => write!(f, "wire_transfer"),
128 }
129 }
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct Transaction {
135 pub transaction_id: String,
136 pub transaction_type: TransactionType,
137 pub amount: f64,
138 pub currency: String,
139 pub from_account: Option<String>,
140 pub to_account: Option<String>,
141 pub timestamp: DateTime<Utc>,
142 pub user_id: String,
143 pub metadata: Option<HashMap<String, String>>,
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct ValidationResult {
149 pub transaction_id: String,
150 pub is_valid: bool,
151 pub errors: Vec<ValidationError>,
152 pub warnings: Vec<String>,
153 pub fraud_score: u8,
154 pub risk_breakdown: RiskBreakdown,
155 pub compliance_checks: HashMap<String, bool>,
156 pub validated_at: DateTime<Utc>,
157}
158
159impl ValidationResult {
160 pub fn is_approved(&self) -> bool {
162 self.is_valid && self.errors.is_empty() && self.fraud_score < 50
163 }
164
165 pub fn requires_manual_review(&self) -> bool {
167 self.fraud_score >= 50 || !self.warnings.is_empty()
168 }
169
170 pub fn risk_level(&self) -> &str {
172 match self.fraud_score {
173 0..=25 => "Low",
174 26..=50 => "Medium",
175 51..=75 => "High",
176 _ => "Critical",
177 }
178 }
179
180 pub fn to_json(&self) -> Result<String, serde_json::Error> {
182 serde_json::to_string_pretty(self)
183 }
184}
185
186#[derive(Debug, Clone)]
188struct TransactionHistory {
189 user_id: String,
190 timestamp: DateTime<Utc>,
191 amount: f64,
192}
193
194#[derive(Debug, Clone)]
196pub struct ValidatorConfig {
197 pub max_transaction_amount: f64,
198 pub min_transaction_amount: f64,
199 pub fraud_threshold: u8,
200 pub enable_duplicate_check: bool,
201 pub enable_aml_check: bool,
202 pub velocity_check_window_minutes: i64,
203 pub max_transactions_per_window: usize,
204 pub max_amount_per_window: f64,
205}
206
207impl Default for ValidatorConfig {
208 fn default() -> Self {
209 Self {
210 max_transaction_amount: 1_000_000.0,
211 min_transaction_amount: 0.01,
212 fraud_threshold: 70,
213 enable_duplicate_check: true,
214 enable_aml_check: true,
215 velocity_check_window_minutes: 60, max_transactions_per_window: 10,
217 max_amount_per_window: 100_000.0,
218 }
219 }
220}
221
222pub struct TransactionValidator {
224 config: ValidatorConfig,
225 processed_transactions: Vec<String>,
226 transaction_history: Vec<TransactionHistory>,
227}
228
229impl TransactionValidator {
230 pub fn new() -> Self {
232 Self {
233 config: ValidatorConfig::default(),
234 processed_transactions: Vec::new(),
235 transaction_history: Vec::new(),
236 }
237 }
238
239 pub fn with_config(config: ValidatorConfig) -> Self {
241 Self {
242 config,
243 processed_transactions: Vec::new(),
244 transaction_history: Vec::new(),
245 }
246 }
247
248 pub fn validate(&mut self, transaction: &Transaction) -> ValidationResult {
250 let mut errors = Vec::new();
251 let mut warnings = Vec::new();
252 let mut compliance_checks = HashMap::new();
253 let mut risk_breakdown = RiskBreakdown::new();
254
255 if let Err(e) = self.validate_amount(transaction) {
257 errors.push(e);
258 }
259
260 risk_breakdown.amount_risk = self.calculate_amount_risk(transaction.amount);
262
263 if let Err(e) = self.validate_accounts(transaction) {
265 errors.push(e);
266 }
267
268 if self.config.enable_duplicate_check {
270 if self
271 .processed_transactions
272 .contains(&transaction.transaction_id)
273 {
274 errors.push(ValidationError::DuplicateTransaction(
275 transaction.transaction_id.clone(),
276 ));
277 } else {
278 self.processed_transactions
279 .push(transaction.transaction_id.clone());
280 }
281 }
282
283 let velocity_result = self.check_velocity(transaction);
285 risk_breakdown.velocity_risk = velocity_result.0;
286 if let Some(err) = velocity_result.1 {
287 errors.push(err);
288 }
289 if !velocity_result.2.is_empty() {
290 warnings.extend(velocity_result.2);
291 }
292
293 self.transaction_history.push(TransactionHistory {
295 user_id: transaction.user_id.clone(),
296 timestamp: transaction.timestamp,
297 amount: transaction.amount,
298 });
299
300 let fraud_checks = self.check_fraud_patterns(transaction);
302 risk_breakdown.pattern_risk = fraud_checks.0;
303 if !fraud_checks.1.is_empty() {
304 warnings.extend(fraud_checks.1);
305 }
306
307 risk_breakdown.time_risk = self.calculate_time_risk(&transaction.timestamp);
309
310 risk_breakdown.calculate_total();
312 let fraud_score = risk_breakdown.total_score;
313
314 if self.config.enable_aml_check {
316 let aml_result = self.check_aml_compliance(transaction);
317 compliance_checks.insert("AML".to_string(), aml_result);
318 if !aml_result {
319 errors.push(ValidationError::ComplianceFailed(
320 "AML compliance check failed".to_string(),
321 ));
322 }
323 }
324
325 if let Err(e) = self.check_business_rules(transaction) {
327 errors.push(e);
328 }
329
330 if fraud_score > self.config.fraud_threshold {
332 errors.push(ValidationError::RiskThresholdExceeded(format!(
333 "Risk score {} exceeds threshold {}",
334 fraud_score, self.config.fraud_threshold
335 )));
336 }
337
338 let is_valid = errors.is_empty();
339
340 ValidationResult {
341 transaction_id: transaction.transaction_id.clone(),
342 is_valid,
343 errors,
344 warnings,
345 fraud_score,
346 risk_breakdown,
347 compliance_checks,
348 validated_at: Utc::now(),
349 }
350 }
351
352 fn calculate_amount_risk(&self, amount: f64) -> u8 {
354 if amount > 100_000.0 {
355 40
356 } else if amount > 50_000.0 {
357 30
358 } else if amount > 10_000.0 {
359 15
360 } else {
361 0
362 }
363 }
364
365 fn calculate_time_risk(&self, timestamp: &DateTime<Utc>) -> u8 {
367 let hour = timestamp.hour();
368 if !(6..=22).contains(&hour) {
369 20 } else if !(9..=17).contains(&hour) {
371 10 } else {
373 0 }
375 }
376
377 fn check_velocity(
379 &self,
380 transaction: &Transaction,
381 ) -> (u8, Option<ValidationError>, Vec<String>) {
382 let mut risk_score = 0u8;
383 let mut error = None;
384 let mut warnings = Vec::new();
385
386 let window_start =
387 transaction.timestamp - Duration::minutes(self.config.velocity_check_window_minutes);
388
389 let recent_transactions: Vec<&TransactionHistory> = self
391 .transaction_history
392 .iter()
393 .filter(|h| h.user_id == transaction.user_id && h.timestamp >= window_start)
394 .collect();
395
396 let transaction_count = recent_transactions.len();
397 let total_amount: f64 =
398 recent_transactions.iter().map(|h| h.amount).sum::<f64>() + transaction.amount;
399
400 if transaction_count >= self.config.max_transactions_per_window {
402 risk_score = risk_score.saturating_add(30);
403 error = Some(ValidationError::VelocityViolation(format!(
404 "Too many transactions: {} in {} minutes",
405 transaction_count + 1,
406 self.config.velocity_check_window_minutes
407 )));
408 } else if transaction_count >= (self.config.max_transactions_per_window / 2) {
409 risk_score = risk_score.saturating_add(15);
410 warnings.push(format!(
411 "High transaction velocity: {} transactions in window",
412 transaction_count + 1
413 ));
414 }
415
416 if total_amount >= self.config.max_amount_per_window {
418 risk_score = risk_score.saturating_add(25);
419 error = Some(ValidationError::VelocityViolation(format!(
420 "Total amount ${:.2} exceeds window limit ${:.2}",
421 total_amount, self.config.max_amount_per_window
422 )));
423 } else if total_amount >= (self.config.max_amount_per_window * 0.75) {
424 risk_score = risk_score.saturating_add(10);
425 warnings.push(format!(
426 "Approaching amount limit: ${:.2} of ${:.2}",
427 total_amount, self.config.max_amount_per_window
428 ));
429 }
430
431 (risk_score, error, warnings)
432 }
433
434 fn validate_amount(&self, transaction: &Transaction) -> Result<(), ValidationError> {
436 if transaction.amount <= 0.0 {
437 return Err(ValidationError::InvalidAmount(
438 "Amount must be positive".to_string(),
439 ));
440 }
441
442 if transaction.amount < self.config.min_transaction_amount {
443 return Err(ValidationError::InvalidAmount(format!(
444 "Amount {} below minimum {}",
445 transaction.amount, self.config.min_transaction_amount
446 )));
447 }
448
449 if transaction.amount > self.config.max_transaction_amount {
450 return Err(ValidationError::InvalidAmount(format!(
451 "Amount {} exceeds maximum {}",
452 transaction.amount, self.config.max_transaction_amount
453 )));
454 }
455
456 Ok(())
457 }
458
459 fn validate_accounts(&self, transaction: &Transaction) -> Result<(), ValidationError> {
461 let account_regex =
462 Regex::new(r"^[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$").unwrap();
463
464 if let Some(ref from_account) = transaction.from_account {
465 if !account_regex.is_match(from_account) && !from_account.starts_with("****") {
466 return Err(ValidationError::InvalidAccount(format!(
467 "Invalid from_account format: {}",
468 from_account
469 )));
470 }
471 }
472
473 if let Some(ref to_account) = transaction.to_account {
474 if !account_regex.is_match(to_account) && !to_account.starts_with("****") {
475 return Err(ValidationError::InvalidAccount(format!(
476 "Invalid to_account format: {}",
477 to_account
478 )));
479 }
480 }
481
482 Ok(())
483 }
484
485 fn check_fraud_patterns(&self, transaction: &Transaction) -> (u8, Vec<String>) {
487 let mut score = 0u8;
488 let mut warnings = Vec::new();
489
490 if transaction.amount % 1000.0 == 0.0 && transaction.amount >= 10000.0 {
492 score += 20;
493 warnings.push("Large round number transaction".to_string());
494 }
495
496 if transaction.amount > 50000.0 {
498 score += 30;
499 warnings.push("High-value transaction requires review".to_string());
500 }
501
502 if transaction.transaction_type == TransactionType::WireTransfer {
504 score += 15;
505 warnings.push("Wire transfer flagged for review".to_string());
506 }
507
508 let hour = transaction.timestamp.hour();
510 if !(6..=22).contains(&hour) {
511 score += 10;
512 warnings.push("Transaction outside business hours".to_string());
513 }
514
515 (score, warnings)
516 }
517
518 fn check_aml_compliance(&self, transaction: &Transaction) -> bool {
520 if transaction.amount > 10000.0 {
525 return true; }
528
529 if transaction.transaction_type == TransactionType::WireTransfer {
531 return true; }
534
535 true
536 }
537
538 fn check_business_rules(&self, transaction: &Transaction) -> Result<(), ValidationError> {
540 if transaction.transaction_type == TransactionType::Transfer
542 && (transaction.from_account.is_none() || transaction.to_account.is_none())
543 {
544 return Err(ValidationError::BusinessRuleViolation(
545 "Transfers must specify both from and to accounts".to_string(),
546 ));
547 }
548
549 if transaction.transaction_type == TransactionType::Deposit
551 && transaction.to_account.is_none()
552 {
553 return Err(ValidationError::BusinessRuleViolation(
554 "Deposits must specify to_account".to_string(),
555 ));
556 }
557
558 if transaction.transaction_type == TransactionType::Withdrawal
560 && transaction.from_account.is_none()
561 {
562 return Err(ValidationError::BusinessRuleViolation(
563 "Withdrawals must specify from_account".to_string(),
564 ));
565 }
566
567 Ok(())
568 }
569
570 pub fn validate_batch(&mut self, transactions: &[Transaction]) -> Vec<ValidationResult> {
572 transactions.iter().map(|tx| self.validate(tx)).collect()
573 }
574
575 pub fn get_stats(&self) -> HashMap<String, usize> {
577 let mut stats = HashMap::new();
578 stats.insert(
579 "total_processed".to_string(),
580 self.processed_transactions.len(),
581 );
582 stats.insert(
583 "total_transactions_in_history".to_string(),
584 self.transaction_history.len(),
585 );
586 stats
587 }
588
589 pub fn clear_old_history(&mut self, before: DateTime<Utc>) {
591 self.transaction_history.retain(|h| h.timestamp >= before);
592 }
593}
594
595impl Default for TransactionValidator {
596 fn default() -> Self {
597 Self::new()
598 }
599}
600
601#[cfg(test)]
602mod tests {
603 use super::*;
604
605 fn create_valid_transaction() -> Transaction {
606 let now = Utc::now();
608 let timestamp = now
609 .date_naive()
610 .and_hms_opt(12, 0, 0)
611 .unwrap()
612 .and_utc();
613
614 Transaction {
615 transaction_id: "TXN-001".to_string(),
616 transaction_type: TransactionType::Transfer,
617 amount: 1000.0,
618 currency: "USD".to_string(),
619 from_account: Some("ACCT-1234-5678-9012".to_string()),
620 to_account: Some("ACCT-6789-0123-4567".to_string()),
621 timestamp,
622 user_id: "USER-001".to_string(),
623 metadata: None,
624 }
625 }
626
627 #[test]
628 fn test_valid_transaction() {
629 let mut validator = TransactionValidator::new();
630 let transaction = create_valid_transaction();
631 let result = validator.validate(&transaction);
632
633 if !result.is_valid {
635 eprintln!("Validation failed!");
636 eprintln!("Errors: {:?}", result.errors);
637 eprintln!("Fraud score: {}", result.fraud_score);
638 eprintln!("Risk breakdown: {:?}", result.risk_breakdown);
639 }
640
641 assert!(result.is_valid, "Transaction should be valid. Errors: {:?}, Fraud score: {}", result.errors, result.fraud_score);
642 assert!(result.errors.is_empty());
643 }
644
645 #[test]
646 fn test_invalid_amount() {
647 let mut validator = TransactionValidator::new();
648 let mut transaction = create_valid_transaction();
649 transaction.amount = -100.0;
650
651 let result = validator.validate(&transaction);
652 assert!(!result.is_valid);
653 assert!(!result.errors.is_empty());
654 }
655
656 #[test]
657 fn test_duplicate_detection() {
658 let mut validator = TransactionValidator::new();
659 let transaction = create_valid_transaction();
660
661 let result1 = validator.validate(&transaction);
662 assert!(result1.is_valid);
663
664 let result2 = validator.validate(&transaction);
665 assert!(!result2.is_valid);
666 assert!(result2
667 .errors
668 .iter()
669 .any(|e| matches!(e, ValidationError::DuplicateTransaction(_))));
670 }
671
672 #[test]
673 fn test_fraud_detection() {
674 let mut validator = TransactionValidator::new();
675 let mut transaction = create_valid_transaction();
676 transaction.amount = 100000.0; let result = validator.validate(&transaction);
679 assert!(result.fraud_score > 0);
680 assert!(!result.warnings.is_empty());
681 }
682
683 #[test]
684 fn test_business_rules() {
685 let mut validator = TransactionValidator::new();
686 let mut transaction = create_valid_transaction();
687 transaction.transaction_type = TransactionType::Transfer;
688 transaction.from_account = None; let result = validator.validate(&transaction);
691 assert!(!result.is_valid);
692 }
693
694 #[test]
695 fn test_velocity_check() {
696 let mut validator = TransactionValidator::new();
697 let user_id = "USER-VELOCITY-TEST".to_string();
698
699 for i in 0..5 {
701 let mut transaction = create_valid_transaction();
702 transaction.user_id = user_id.clone();
703 transaction.transaction_id = format!("TXN-{}", i);
704 transaction.amount = 5000.0;
705
706 let result = validator.validate(&transaction);
707 if i < 3 {
709 assert!(result.is_valid || !result.warnings.is_empty());
710 }
711 }
712
713 let stats = validator.get_stats();
715 assert_eq!(stats["total_transactions_in_history"], 5);
716 }
717
718 #[test]
719 fn test_risk_breakdown() {
720 let mut validator = TransactionValidator::new();
721 let mut transaction = create_valid_transaction();
722 transaction.amount = 150_000.0; let result = validator.validate(&transaction);
725
726 assert!(result.risk_breakdown.amount_risk > 0);
728 assert!(result.risk_breakdown.total_score > 0);
729 assert_eq!(result.risk_breakdown.total_score, result.fraud_score);
730 }
731
732 #[test]
733 fn test_time_based_risk() {
734 let mut validator = TransactionValidator::new();
735 let mut transaction = create_valid_transaction();
736
737 let late_night = Utc::now().date_naive().and_hms_opt(2, 0, 0).unwrap();
739 transaction.timestamp = DateTime::from_naive_utc_and_offset(late_night, Utc);
740
741 let result = validator.validate(&transaction);
742 assert!(result.risk_breakdown.time_risk > 0);
743 }
744
745 #[test]
746 fn test_risk_level_description() {
747 let mut validator = TransactionValidator::new();
748
749 let mut transaction = create_valid_transaction();
751 transaction.amount = 100.0;
752 let result = validator.validate(&transaction);
753 assert_eq!(result.risk_level(), "Low");
754
755 let mut transaction2 = create_valid_transaction();
757 transaction2.transaction_id = "TXN-002".to_string();
758 transaction2.amount = 200_000.0;
759 let result2 = validator.validate(&transaction2);
760 assert!(matches!(
761 result2.risk_level(),
762 "High" | "Critical" | "Medium"
763 ));
764 }
765
766 #[test]
767 fn test_manual_review_flag() {
768 let mut validator = TransactionValidator::new();
769 let mut transaction = create_valid_transaction();
770 transaction.amount = 100_000.0; let result = validator.validate(&transaction);
773 assert!(result.requires_manual_review() || !result.warnings.is_empty());
775 }
776
777 #[test]
778 fn test_batch_validation() {
779 let mut validator = TransactionValidator::new();
780
781 let transactions = vec![
782 create_valid_transaction(),
783 {
784 let mut tx = create_valid_transaction();
785 tx.transaction_id = "TXN-002".to_string();
786 tx
787 },
788 {
789 let mut tx = create_valid_transaction();
790 tx.transaction_id = "TXN-003".to_string();
791 tx.amount = -100.0; tx
793 },
794 ];
795
796 let results = validator.validate_batch(&transactions);
797
798 assert_eq!(results.len(), 3);
799 assert!(results[0].is_valid);
800 assert!(results[1].is_valid);
801 assert!(!results[2].is_valid); }
803
804 #[test]
805 fn test_history_cleanup() {
806 let mut validator = TransactionValidator::new();
807
808 let old_time = Utc::now() - Duration::hours(48);
810 for i in 0..5 {
811 let mut transaction = create_valid_transaction();
812 transaction.transaction_id = format!("TXN-{}", i);
813 transaction.timestamp = old_time;
814 validator.validate(&transaction);
815 }
816
817 let cutoff = Utc::now() - Duration::hours(24);
818 validator.clear_old_history(cutoff);
819
820 let stats = validator.get_stats();
821 assert_eq!(stats["total_transactions_in_history"], 0);
822 }
823
824 #[test]
825 fn test_is_approved() {
826 let mut validator = TransactionValidator::new();
827
828 let mut transaction = create_valid_transaction();
830 transaction.amount = 500.0;
831 let result = validator.validate(&transaction);
832 assert!(result.is_approved());
833
834 let mut transaction2 = create_valid_transaction();
836 transaction2.transaction_id = "TXN-002".to_string();
837 transaction2.amount = 500_000.0;
838 let result2 = validator.validate(&transaction2);
839 assert!(!result2.is_approved() || result2.fraud_score < 50);
841 }
842
843 #[test]
844 fn test_velocity_amount_limit() {
845 let config = ValidatorConfig {
846 max_transaction_amount: 1_000_000.0,
847 min_transaction_amount: 0.01,
848 fraud_threshold: 70,
849 enable_duplicate_check: true,
850 enable_aml_check: true,
851 velocity_check_window_minutes: 60,
852 max_transactions_per_window: 10,
853 max_amount_per_window: 50_000.0, };
855
856 let mut validator = TransactionValidator::with_config(config);
857 let user_id = "USER-AMOUNT-TEST".to_string();
858
859 for i in 0..3 {
861 let mut transaction = create_valid_transaction();
862 transaction.user_id = user_id.clone();
863 transaction.transaction_id = format!("TXN-{}", i);
864 transaction.amount = 20_000.0; let result = validator.validate(&transaction);
867 if i >= 2 {
868 assert!(result.risk_breakdown.velocity_risk > 0);
870 }
871 }
872 }
873
874 #[test]
875 fn test_json_export() {
876 let mut validator = TransactionValidator::new();
877 let transaction = create_valid_transaction();
878 let result = validator.validate(&transaction);
879
880 let json = result.to_json();
881 assert!(json.is_ok());
882 let json_str = json.unwrap();
883 assert!(json_str.contains("TXN-001"));
884 assert!(json_str.contains("risk_breakdown"));
885 }
886}