skp_validator_rules/financial/
credit_card.rs

1//! Credit card validation rule using Luhn algorithm.
2
3use skp_validator_core::{Rule, ValidationContext, ValidationErrors, ValidationError, ValidationResult};
4
5/// Credit card validation rule using Luhn algorithm.
6///
7/// # Example
8///
9/// ```rust
10/// use skp_validator_rules::financial::credit_card::CreditCardRule;
11/// use skp_validator_core::{Rule, ValidationContext};
12///
13/// let rule = CreditCardRule::new();
14/// let ctx = ValidationContext::default();
15///
16/// assert!(rule.validate("4532015112830366", &ctx).is_ok()); // Valid test card
17/// assert!(rule.validate("1234567890123456", &ctx).is_err());
18/// ```
19#[derive(Debug, Clone, Default)]
20pub struct CreditCardRule {
21    /// Custom error message
22    pub message: Option<String>,
23}
24
25impl CreditCardRule {
26    /// Create a new credit_card rule.
27    pub fn new() -> Self {
28        Self::default()
29    }
30
31    /// Set custom error message.
32    pub fn message(mut self, msg: impl Into<String>) -> Self {
33        self.message = Some(msg.into());
34        self
35    }
36
37    fn get_message(&self) -> String {
38        self.message.clone().unwrap_or_else(|| "Must be a valid credit card number".to_string())
39    }
40
41    /// Validate using Luhn algorithm
42    fn luhn_check(&self, number: &str) -> bool {
43        // Remove spaces and dashes
44        let digits: String = number.chars().filter(|c| c.is_ascii_digit()).collect();
45
46        // Must be between 13 and 19 digits
47        if digits.len() < 13 || digits.len() > 19 {
48            return false;
49        }
50
51        let mut sum = 0;
52        let mut double = false;
53
54        // Process from right to left
55        for c in digits.chars().rev() {
56            if let Some(digit) = c.to_digit(10) {
57                let mut value = digit;
58                if double {
59                    value *= 2;
60                    if value > 9 {
61                        value -= 9;
62                    }
63                }
64                sum += value;
65                double = !double;
66            } else {
67                return false;
68            }
69        }
70
71        sum % 10 == 0
72    }
73}
74
75impl Rule<str> for CreditCardRule {
76    fn validate(&self, value: &str, _ctx: &ValidationContext) -> ValidationResult<()> {
77        if value.is_empty() {
78            return Ok(());
79        }
80
81        if self.luhn_check(value) {
82            Ok(())
83        } else {
84            Err(ValidationErrors::from_iter([
85                ValidationError::root("credit_card", self.get_message())
86            ]))
87        }
88    }
89
90    fn name(&self) -> &'static str {
91        "credit_card"
92    }
93
94    fn default_message(&self) -> String {
95        self.get_message()
96    }
97}
98
99impl Rule<String> for CreditCardRule {
100    fn validate(&self, value: &String, ctx: &ValidationContext) -> ValidationResult<()> {
101        <Self as Rule<str>>::validate(self, value.as_str(), ctx)
102    }
103
104    fn name(&self) -> &'static str {
105        "credit_card"
106    }
107
108    fn default_message(&self) -> String {
109        self.get_message()
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn test_valid_cards() {
119        let rule = CreditCardRule::new();
120        let ctx = ValidationContext::default();
121
122        // Valid test credit card numbers (Luhn-valid)
123        assert!(rule.validate("4532015112830366", &ctx).is_ok()); // Visa
124        assert!(rule.validate("5425233430109903", &ctx).is_ok()); // Mastercard
125        assert!(rule.validate("378282246310005", &ctx).is_ok());  // Amex
126        assert!(rule.validate("4111111111111111", &ctx).is_ok()); // Test Visa
127    }
128
129    #[test]
130    fn test_invalid_cards() {
131        let rule = CreditCardRule::new();
132        let ctx = ValidationContext::default();
133
134        assert!(rule.validate("1234567890123456", &ctx).is_err());
135        assert!(rule.validate("4111111111111112", &ctx).is_err()); // Changed last digit
136    }
137
138    #[test]
139    fn test_with_spaces() {
140        let rule = CreditCardRule::new();
141        let ctx = ValidationContext::default();
142
143        // Should handle spaces
144        assert!(rule.validate("4532 0151 1283 0366", &ctx).is_ok());
145    }
146
147    #[test]
148    fn test_with_dashes() {
149        let rule = CreditCardRule::new();
150        let ctx = ValidationContext::default();
151
152        // Should handle dashes
153        assert!(rule.validate("4532-0151-1283-0366", &ctx).is_ok());
154    }
155
156    #[test]
157    fn test_empty_is_valid() {
158        let rule = CreditCardRule::new();
159        let ctx = ValidationContext::default();
160
161        assert!(rule.validate("", &ctx).is_ok());
162    }
163
164    #[test]
165    fn test_too_short() {
166        let rule = CreditCardRule::new();
167        let ctx = ValidationContext::default();
168
169        assert!(rule.validate("123456789012", &ctx).is_err()); // 12 digits
170    }
171}