rustkernel_payments/
processing.rs

1//! Payment processing kernel.
2//!
3//! Ring-mode kernel for real-time payment transaction execution.
4
5use crate::types::*;
6use rustkernel_core::{domain::Domain, kernel::KernelMetadata, traits::GpuKernel};
7use std::collections::HashMap;
8
9// ============================================================================
10// PaymentProcessing Kernel
11// ============================================================================
12
13/// Payment processing kernel for real-time transaction execution.
14///
15/// Handles payment validation, routing, and execution across multiple
16/// payment rails (ACH, Wire, RealTime, Internal, Check, Card).
17#[derive(Debug, Clone)]
18pub struct PaymentProcessing {
19    metadata: KernelMetadata,
20}
21
22impl Default for PaymentProcessing {
23    fn default() -> Self {
24        Self::new()
25    }
26}
27
28impl PaymentProcessing {
29    /// Create a new payment processing kernel.
30    #[must_use]
31    pub fn new() -> Self {
32        Self {
33            metadata: KernelMetadata::ring("payments/processing", Domain::PaymentProcessing)
34                .with_description("Real-time payment transaction execution")
35                .with_throughput(50_000)
36                .with_latency_us(100.0),
37        }
38    }
39
40    /// Process a batch of payments.
41    pub fn process_payments(
42        payments: &[Payment],
43        accounts: &HashMap<String, PaymentAccount>,
44        config: &ProcessingConfig,
45    ) -> ProcessingResult {
46        let total_count = payments.len();
47
48        let mut processed = Vec::new();
49        let mut failed = Vec::new();
50        let mut pending = Vec::new();
51        let mut total_amount = 0.0;
52        let mut processing_times = Vec::new();
53
54        for payment in payments {
55            let payment_start = std::time::Instant::now();
56
57            match Self::process_single_payment(payment, accounts, config) {
58                PaymentOutcome::Processed => {
59                    processed.push(payment.id.clone());
60                    total_amount += payment.amount;
61                }
62                PaymentOutcome::Failed(reason) => {
63                    failed.push((payment.id.clone(), reason));
64                }
65                PaymentOutcome::Pending => {
66                    pending.push(payment.id.clone());
67                }
68            }
69
70            processing_times.push(payment_start.elapsed().as_micros() as f64);
71        }
72
73        let processed_count = processed.len();
74        let failed_count = failed.len();
75        let avg_processing_time_us = if !processing_times.is_empty() {
76            processing_times.iter().sum::<f64>() / processing_times.len() as f64
77        } else {
78            0.0
79        };
80
81        ProcessingResult {
82            processed,
83            failed,
84            pending,
85            stats: ProcessingStats {
86                total_count,
87                processed_count,
88                failed_count,
89                total_amount,
90                avg_processing_time_us,
91            },
92        }
93    }
94
95    /// Process a single payment transaction.
96    fn process_single_payment(
97        payment: &Payment,
98        accounts: &HashMap<String, PaymentAccount>,
99        config: &ProcessingConfig,
100    ) -> PaymentOutcome {
101        // 1. Validate payment
102        if let Err(reason) = Self::validate_payment(payment, accounts, config) {
103            return PaymentOutcome::Failed(reason);
104        }
105
106        // 2. Check payment type routing
107        if !Self::is_payment_type_enabled(payment.payment_type, config) {
108            return PaymentOutcome::Failed("Payment type not enabled".to_string());
109        }
110
111        // 3. Check processing windows for non-real-time payments
112        if !Self::is_within_processing_window(payment, config) {
113            return PaymentOutcome::Pending;
114        }
115
116        // 4. Apply fraud checks
117        if config.fraud_check_enabled {
118            if let Err(reason) = Self::fraud_check(payment, config) {
119                return PaymentOutcome::Failed(reason);
120            }
121        }
122
123        // 5. Route payment
124        match payment.payment_type {
125            PaymentType::RealTime | PaymentType::Internal => PaymentOutcome::Processed,
126            PaymentType::Wire if payment.priority >= PaymentPriority::High => {
127                PaymentOutcome::Processed
128            }
129            _ => {
130                // Batch processing for ACH, Check, low-priority Wire
131                if config.batch_mode {
132                    PaymentOutcome::Pending
133                } else {
134                    PaymentOutcome::Processed
135                }
136            }
137        }
138    }
139
140    /// Validate a payment.
141    fn validate_payment(
142        payment: &Payment,
143        accounts: &HashMap<String, PaymentAccount>,
144        config: &ProcessingConfig,
145    ) -> std::result::Result<(), String> {
146        // Check amount
147        if payment.amount <= 0.0 {
148            return Err("Invalid amount: must be positive".to_string());
149        }
150
151        // Check payer account
152        let payer = accounts
153            .get(&payment.payer_account)
154            .ok_or_else(|| "Payer account not found".to_string())?;
155
156        // Check account status
157        if payer.status != AccountStatus::Active {
158            return Err("Payer account is not active".to_string());
159        }
160
161        // Check sufficient balance
162        if payer.available_balance < payment.amount {
163            return Err("Insufficient funds".to_string());
164        }
165
166        // Check daily limit
167        if let Some(limit) = payer.daily_limit {
168            if payer.daily_used + payment.amount > limit {
169                return Err("Daily limit exceeded".to_string());
170            }
171        }
172
173        // Check payee account exists
174        if !accounts.contains_key(&payment.payee_account) {
175            return Err("Payee account not found".to_string());
176        }
177
178        // Check currency match
179        if payer.currency != payment.currency {
180            return Err("Currency mismatch".to_string());
181        }
182
183        // Check amount limits
184        if let Some(min) = config.min_amount {
185            if payment.amount < min {
186                return Err(format!("Amount below minimum: {}", min));
187            }
188        }
189
190        if let Some(max) = config.max_amount {
191            if payment.amount > max {
192                return Err(format!("Amount exceeds maximum: {}", max));
193            }
194        }
195
196        Ok(())
197    }
198
199    /// Check if payment type is enabled.
200    fn is_payment_type_enabled(payment_type: PaymentType, config: &ProcessingConfig) -> bool {
201        config.enabled_payment_types.contains(&payment_type)
202    }
203
204    /// Check if payment is within processing window.
205    fn is_within_processing_window(payment: &Payment, config: &ProcessingConfig) -> bool {
206        // Real-time and internal payments always process
207        if matches!(
208            payment.payment_type,
209            PaymentType::RealTime | PaymentType::Internal
210        ) {
211            return true;
212        }
213
214        // If process_outside_hours is true, always allow processing
215        if config.process_outside_hours {
216            return true;
217        }
218
219        // Get current time - use payment submission timestamp if available,
220        // otherwise use current system time
221        let timestamp = payment
222            .attributes
223            .get("submission_time")
224            .and_then(|s| s.parse::<u64>().ok())
225            .unwrap_or_else(|| {
226                std::time::SystemTime::now()
227                    .duration_since(std::time::UNIX_EPOCH)
228                    .map(|d| d.as_secs())
229                    .unwrap_or(0)
230            });
231
232        // Extract hour and day of week from timestamp
233        // UTC-based calculation, adjusted by timezone offset if configured
234        let adjusted_timestamp = timestamp as i64 + config.timezone_offset_seconds;
235        let seconds_in_day = 86400i64;
236        let seconds_since_epoch_start = adjusted_timestamp;
237
238        // Unix epoch (1970-01-01) was a Thursday (day 4)
239        // Calculate day of week (0 = Sunday, 6 = Saturday)
240        let days_since_epoch = seconds_since_epoch_start / seconds_in_day;
241        let day_of_week = ((days_since_epoch + 4) % 7) as u8;
242
243        // Calculate hour of day (0-23)
244        let seconds_into_day = (seconds_since_epoch_start % seconds_in_day) as u32;
245        let hour = (seconds_into_day / 3600) as u8;
246
247        // Check if it's a weekday (Monday=1 through Friday=5)
248        let is_weekday = (1..=5).contains(&day_of_week);
249
250        // Check if within business hours
251        let is_business_hours =
252            hour >= config.business_hours_start && hour < config.business_hours_end;
253
254        // Must be a weekday within business hours
255        is_weekday && is_business_hours
256    }
257
258    /// Run fraud checks on a payment.
259    fn fraud_check(
260        payment: &Payment,
261        config: &ProcessingConfig,
262    ) -> std::result::Result<(), String> {
263        // Check velocity limits
264        if let Some(velocity_limit) = config.velocity_limit {
265            // In a real implementation, this would check recent transaction count
266            if payment.amount > velocity_limit * 10.0 {
267                return Err("Velocity check failed".to_string());
268            }
269        }
270
271        // Check for suspicious patterns - large real-time payments need review
272        if payment.payment_type == PaymentType::RealTime {
273            if let Some(threshold) = config.large_payment_threshold {
274                if payment.amount > threshold {
275                    return Err("Large payment requires manual review".to_string());
276                }
277            }
278        }
279
280        Ok(())
281    }
282
283    /// Validate a payment without processing.
284    pub fn validate_only(
285        payment: &Payment,
286        accounts: &HashMap<String, PaymentAccount>,
287        config: &ProcessingConfig,
288    ) -> ValidationResult {
289        let mut errors = Vec::new();
290        let mut warnings = Vec::new();
291
292        // Run standard validation
293        if let Err(msg) = Self::validate_payment(payment, accounts, config) {
294            errors.push(ValidationError {
295                code: "VALIDATION_ERROR".to_string(),
296                message: msg,
297                field: None,
298            });
299        }
300
301        // Check for warnings
302        if payment.amount > 5000.0 {
303            warnings.push(ValidationWarning {
304                code: "LARGE_AMOUNT".to_string(),
305                message: "Payment amount is above reporting threshold".to_string(),
306            });
307        }
308
309        if payment.payment_type == PaymentType::Check {
310            warnings.push(ValidationWarning {
311                code: "SLOW_PAYMENT".to_string(),
312                message: "Check payments may take 3-5 business days".to_string(),
313            });
314        }
315
316        ValidationResult {
317            is_valid: errors.is_empty(),
318            errors,
319            warnings,
320        }
321    }
322
323    /// Get payment routing information.
324    pub fn get_routing(payment: &Payment) -> PaymentRouting {
325        let (rail, estimated_settlement) = match payment.payment_type {
326            PaymentType::RealTime => ("RTP".to_string(), 0),
327            PaymentType::Wire => (
328                "FEDWIRE".to_string(),
329                if payment.priority >= PaymentPriority::High {
330                    0
331                } else {
332                    1
333                },
334            ),
335            PaymentType::ACH => ("ACH".to_string(), 2),
336            PaymentType::Internal => ("INTERNAL".to_string(), 0),
337            PaymentType::Check => ("CHECK".to_string(), 5),
338            PaymentType::Card => ("CARD_NETWORK".to_string(), 1),
339        };
340
341        let requires_approval =
342            payment.amount > 10000.0 || payment.priority >= PaymentPriority::Urgent;
343
344        PaymentRouting {
345            payment_id: payment.id.clone(),
346            rail,
347            estimated_settlement_days: estimated_settlement,
348            requires_approval,
349            fees: Self::calculate_fees(payment),
350        }
351    }
352
353    /// Calculate payment fees.
354    fn calculate_fees(payment: &Payment) -> f64 {
355        match payment.payment_type {
356            PaymentType::RealTime => 0.50, // Flat fee
357            PaymentType::Wire => {
358                if payment.priority >= PaymentPriority::Urgent {
359                    25.0
360                } else {
361                    15.0
362                }
363            }
364            PaymentType::ACH => payment.amount * 0.001, // 0.1%
365            PaymentType::Internal => 0.0,
366            PaymentType::Check => 1.0,
367            PaymentType::Card => payment.amount * 0.029 + 0.30, // 2.9% + $0.30
368        }
369    }
370
371    /// Process payments by priority.
372    pub fn process_by_priority(
373        payments: &[Payment],
374        accounts: &HashMap<String, PaymentAccount>,
375        config: &ProcessingConfig,
376    ) -> Vec<ProcessingResult> {
377        // Group payments by priority
378        let mut priority_groups: HashMap<PaymentPriority, Vec<&Payment>> = HashMap::new();
379        for payment in payments {
380            priority_groups
381                .entry(payment.priority)
382                .or_default()
383                .push(payment);
384        }
385
386        // Process in priority order (highest first)
387        let mut results = Vec::new();
388        let priorities = [
389            PaymentPriority::Urgent,
390            PaymentPriority::High,
391            PaymentPriority::Normal,
392            PaymentPriority::Low,
393        ];
394
395        for priority in priorities {
396            if let Some(group) = priority_groups.get(&priority) {
397                let payments_vec: Vec<Payment> = group.iter().map(|p| (*p).clone()).collect();
398                let result = Self::process_payments(&payments_vec, accounts, config);
399                results.push(result);
400            }
401        }
402
403        results
404    }
405}
406
407impl GpuKernel for PaymentProcessing {
408    fn metadata(&self) -> &KernelMetadata {
409        &self.metadata
410    }
411}
412
413// ============================================================================
414// Helper Types
415// ============================================================================
416
417/// Payment processing outcome.
418enum PaymentOutcome {
419    Processed,
420    Failed(String),
421    Pending,
422}
423
424/// Payment processing configuration.
425#[derive(Debug, Clone)]
426pub struct ProcessingConfig {
427    /// Enabled payment types.
428    pub enabled_payment_types: Vec<PaymentType>,
429    /// Minimum payment amount.
430    pub min_amount: Option<f64>,
431    /// Maximum payment amount.
432    pub max_amount: Option<f64>,
433    /// Enable fraud checks.
434    pub fraud_check_enabled: bool,
435    /// Velocity limit (transactions per hour).
436    pub velocity_limit: Option<f64>,
437    /// Large payment threshold for manual review.
438    pub large_payment_threshold: Option<f64>,
439    /// Process outside business hours.
440    pub process_outside_hours: bool,
441    /// Batch mode (queue non-urgent payments).
442    pub batch_mode: bool,
443    /// Business hours start (hour of day, 0-23).
444    pub business_hours_start: u8,
445    /// Business hours end (hour of day, 0-23).
446    pub business_hours_end: u8,
447    /// Timezone offset in seconds from UTC (e.g., -18000 for EST/UTC-5).
448    pub timezone_offset_seconds: i64,
449}
450
451impl Default for ProcessingConfig {
452    fn default() -> Self {
453        Self {
454            enabled_payment_types: vec![
455                PaymentType::ACH,
456                PaymentType::Wire,
457                PaymentType::RealTime,
458                PaymentType::Internal,
459                PaymentType::Check,
460                PaymentType::Card,
461            ],
462            min_amount: Some(0.01),
463            max_amount: Some(1_000_000.0),
464            fraud_check_enabled: true,
465            velocity_limit: Some(100.0),
466            large_payment_threshold: Some(50_000.0),
467            process_outside_hours: true,
468            batch_mode: false,
469            business_hours_start: 9,    // 9 AM
470            business_hours_end: 17,     // 5 PM
471            timezone_offset_seconds: 0, // UTC by default
472        }
473    }
474}
475
476/// Payment routing information.
477#[derive(Debug, Clone)]
478pub struct PaymentRouting {
479    /// Payment ID.
480    pub payment_id: String,
481    /// Payment rail.
482    pub rail: String,
483    /// Estimated settlement time in days.
484    pub estimated_settlement_days: u32,
485    /// Requires manual approval.
486    pub requires_approval: bool,
487    /// Calculated fees.
488    pub fees: f64,
489}
490
491// ============================================================================
492// Tests
493// ============================================================================
494
495#[cfg(test)]
496mod tests {
497    use super::*;
498
499    fn create_test_accounts() -> HashMap<String, PaymentAccount> {
500        let mut accounts = HashMap::new();
501        accounts.insert(
502            "ACC001".to_string(),
503            PaymentAccount {
504                id: "ACC001".to_string(),
505                account_type: AccountType::Checking,
506                balance: 10000.0,
507                available_balance: 9500.0,
508                currency: "USD".to_string(),
509                status: AccountStatus::Active,
510                daily_limit: Some(25000.0),
511                daily_used: 1000.0,
512            },
513        );
514        accounts.insert(
515            "ACC002".to_string(),
516            PaymentAccount {
517                id: "ACC002".to_string(),
518                account_type: AccountType::Checking,
519                balance: 5000.0,
520                available_balance: 5000.0,
521                currency: "USD".to_string(),
522                status: AccountStatus::Active,
523                daily_limit: None,
524                daily_used: 0.0,
525            },
526        );
527        accounts.insert(
528            "ACC003".to_string(),
529            PaymentAccount {
530                id: "ACC003".to_string(),
531                account_type: AccountType::Savings,
532                balance: 20000.0,
533                available_balance: 20000.0,
534                currency: "USD".to_string(),
535                status: AccountStatus::Frozen,
536                daily_limit: None,
537                daily_used: 0.0,
538            },
539        );
540        accounts
541    }
542
543    fn create_test_payment(id: &str, payer: &str, payee: &str, amount: f64) -> Payment {
544        Payment {
545            id: id.to_string(),
546            payer_account: payer.to_string(),
547            payee_account: payee.to_string(),
548            amount,
549            currency: "USD".to_string(),
550            payment_type: PaymentType::ACH,
551            status: PaymentStatus::Initiated,
552            initiated_at: 1000,
553            completed_at: None,
554            reference: format!("REF-{}", id),
555            priority: PaymentPriority::Normal,
556            attributes: HashMap::new(),
557        }
558    }
559
560    #[test]
561    fn test_process_valid_payment() {
562        let accounts = create_test_accounts();
563        let config = ProcessingConfig::default();
564
565        let payments = vec![create_test_payment("P001", "ACC001", "ACC002", 100.0)];
566        let result = PaymentProcessing::process_payments(&payments, &accounts, &config);
567
568        assert_eq!(result.stats.total_count, 1);
569        assert_eq!(result.stats.processed_count, 1);
570        assert_eq!(result.stats.failed_count, 0);
571        assert!(result.processed.contains(&"P001".to_string()));
572    }
573
574    #[test]
575    fn test_insufficient_funds() {
576        let accounts = create_test_accounts();
577        let config = ProcessingConfig::default();
578
579        let payments = vec![create_test_payment("P001", "ACC001", "ACC002", 15000.0)];
580        let result = PaymentProcessing::process_payments(&payments, &accounts, &config);
581
582        assert_eq!(result.stats.failed_count, 1);
583        assert!(
584            result
585                .failed
586                .iter()
587                .any(|(id, reason)| { id == "P001" && reason.contains("Insufficient funds") })
588        );
589    }
590
591    #[test]
592    fn test_frozen_account() {
593        let accounts = create_test_accounts();
594        let config = ProcessingConfig::default();
595
596        let payments = vec![create_test_payment("P001", "ACC003", "ACC002", 100.0)];
597        let result = PaymentProcessing::process_payments(&payments, &accounts, &config);
598
599        assert_eq!(result.stats.failed_count, 1);
600        assert!(
601            result
602                .failed
603                .iter()
604                .any(|(id, reason)| { id == "P001" && reason.contains("not active") })
605        );
606    }
607
608    #[test]
609    fn test_daily_limit_exceeded() {
610        let mut accounts = create_test_accounts();
611        // Increase available balance to allow for the amount, so daily limit is the blocker
612        accounts.get_mut("ACC001").unwrap().available_balance = 30000.0;
613        accounts.get_mut("ACC001").unwrap().balance = 30000.0;
614        let config = ProcessingConfig::default();
615
616        // ACC001 has $25000 daily limit and $1000 already used, so $24001+ would exceed
617        let payments = vec![create_test_payment("P001", "ACC001", "ACC002", 24500.0)];
618        let result = PaymentProcessing::process_payments(&payments, &accounts, &config);
619
620        assert_eq!(result.stats.failed_count, 1);
621        assert!(
622            result
623                .failed
624                .iter()
625                .any(|(id, reason)| { id == "P001" && reason.contains("Daily limit") })
626        );
627    }
628
629    #[test]
630    fn test_account_not_found() {
631        let accounts = create_test_accounts();
632        let config = ProcessingConfig::default();
633
634        let payments = vec![create_test_payment("P001", "ACC999", "ACC002", 100.0)];
635        let result = PaymentProcessing::process_payments(&payments, &accounts, &config);
636
637        assert_eq!(result.stats.failed_count, 1);
638        assert!(
639            result
640                .failed
641                .iter()
642                .any(|(_, reason)| { reason.contains("not found") })
643        );
644    }
645
646    #[test]
647    fn test_validate_only() {
648        let accounts = create_test_accounts();
649        let config = ProcessingConfig::default();
650
651        let payment = create_test_payment("P001", "ACC001", "ACC002", 100.0);
652        let result = PaymentProcessing::validate_only(&payment, &accounts, &config);
653
654        assert!(result.is_valid);
655        assert!(result.errors.is_empty());
656    }
657
658    #[test]
659    fn test_validate_with_warnings() {
660        let accounts = create_test_accounts();
661        let config = ProcessingConfig::default();
662
663        let payment = create_test_payment("P001", "ACC001", "ACC002", 6000.0);
664        let result = PaymentProcessing::validate_only(&payment, &accounts, &config);
665
666        assert!(result.is_valid);
667        assert!(result.warnings.iter().any(|w| w.code == "LARGE_AMOUNT"));
668    }
669
670    #[test]
671    fn test_payment_routing_realtime() {
672        let mut payment = create_test_payment("P001", "ACC001", "ACC002", 100.0);
673        payment.payment_type = PaymentType::RealTime;
674
675        let routing = PaymentProcessing::get_routing(&payment);
676
677        assert_eq!(routing.rail, "RTP");
678        assert_eq!(routing.estimated_settlement_days, 0);
679        assert_eq!(routing.fees, 0.50);
680    }
681
682    #[test]
683    fn test_payment_routing_wire() {
684        let mut payment = create_test_payment("P001", "ACC001", "ACC002", 10000.0);
685        payment.payment_type = PaymentType::Wire;
686        payment.priority = PaymentPriority::Normal;
687
688        let routing = PaymentProcessing::get_routing(&payment);
689
690        assert_eq!(routing.rail, "FEDWIRE");
691        assert_eq!(routing.estimated_settlement_days, 1);
692        assert_eq!(routing.fees, 15.0);
693    }
694
695    #[test]
696    fn test_payment_routing_urgent_wire() {
697        let mut payment = create_test_payment("P001", "ACC001", "ACC002", 10000.0);
698        payment.payment_type = PaymentType::Wire;
699        payment.priority = PaymentPriority::Urgent;
700
701        let routing = PaymentProcessing::get_routing(&payment);
702
703        assert_eq!(routing.estimated_settlement_days, 0);
704        assert_eq!(routing.fees, 25.0);
705        assert!(routing.requires_approval);
706    }
707
708    #[test]
709    fn test_batch_payments() {
710        let accounts = create_test_accounts();
711        let config = ProcessingConfig::default();
712
713        let payments = vec![
714            create_test_payment("P001", "ACC001", "ACC002", 100.0),
715            create_test_payment("P002", "ACC001", "ACC002", 200.0),
716            create_test_payment("P003", "ACC001", "ACC002", 300.0),
717        ];
718
719        let result = PaymentProcessing::process_payments(&payments, &accounts, &config);
720
721        assert_eq!(result.stats.total_count, 3);
722        assert_eq!(result.stats.processed_count, 3);
723        assert_eq!(result.stats.total_amount, 600.0);
724    }
725
726    #[test]
727    fn test_process_by_priority() {
728        let accounts = create_test_accounts();
729        let config = ProcessingConfig::default();
730
731        let mut p1 = create_test_payment("P001", "ACC001", "ACC002", 100.0);
732        p1.priority = PaymentPriority::Low;
733
734        let mut p2 = create_test_payment("P002", "ACC001", "ACC002", 200.0);
735        p2.priority = PaymentPriority::Urgent;
736
737        let mut p3 = create_test_payment("P003", "ACC001", "ACC002", 300.0);
738        p3.priority = PaymentPriority::Normal;
739
740        let payments = vec![p1, p2, p3];
741        let results = PaymentProcessing::process_by_priority(&payments, &accounts, &config);
742
743        // Should have results for 3 priority levels (Urgent, Normal, Low)
744        assert_eq!(results.len(), 3);
745        // First result should be Urgent
746        assert_eq!(results[0].stats.total_count, 1);
747        assert!(results[0].processed.contains(&"P002".to_string()));
748    }
749
750    #[test]
751    fn test_fraud_check_large_payment() {
752        let accounts = create_test_accounts();
753        let config = ProcessingConfig {
754            large_payment_threshold: Some(5000.0),
755            max_amount: Some(100000.0),
756            velocity_limit: Some(1000.0), // Increase to avoid velocity check triggering
757            ..ProcessingConfig::default()
758        };
759
760        let mut payment = create_test_payment("P001", "ACC001", "ACC002", 8000.0);
761        payment.payment_type = PaymentType::RealTime;
762
763        let payments = vec![payment];
764        let result = PaymentProcessing::process_payments(&payments, &accounts, &config);
765
766        assert_eq!(result.stats.failed_count, 1);
767        assert!(
768            result
769                .failed
770                .iter()
771                .any(|(_, reason)| { reason.contains("manual review") })
772        );
773    }
774
775    #[test]
776    fn test_payment_type_disabled() {
777        let accounts = create_test_accounts();
778        let config = ProcessingConfig {
779            enabled_payment_types: vec![PaymentType::ACH], // Only ACH enabled
780            ..ProcessingConfig::default()
781        };
782
783        let mut payment = create_test_payment("P001", "ACC001", "ACC002", 100.0);
784        payment.payment_type = PaymentType::Wire;
785
786        let payments = vec![payment];
787        let result = PaymentProcessing::process_payments(&payments, &accounts, &config);
788
789        assert_eq!(result.stats.failed_count, 1);
790        assert!(
791            result
792                .failed
793                .iter()
794                .any(|(_, reason)| { reason.contains("not enabled") })
795        );
796    }
797
798    #[test]
799    fn test_amount_limits() {
800        let accounts = create_test_accounts();
801        let config = ProcessingConfig {
802            min_amount: Some(10.0),
803            max_amount: Some(1000.0),
804            ..ProcessingConfig::default()
805        };
806
807        // Test below minimum
808        let p1 = create_test_payment("P001", "ACC001", "ACC002", 5.0);
809        let result = PaymentProcessing::process_payments(&[p1], &accounts, &config);
810        assert!(
811            result
812                .failed
813                .iter()
814                .any(|(_, r)| r.contains("below minimum"))
815        );
816
817        // Test above maximum
818        let p2 = create_test_payment("P002", "ACC001", "ACC002", 2000.0);
819        let result = PaymentProcessing::process_payments(&[p2], &accounts, &config);
820        assert!(
821            result
822                .failed
823                .iter()
824                .any(|(_, r)| r.contains("exceeds maximum"))
825        );
826    }
827}