Skip to main content

iso8583_core/
response_code.rs

1//! ISO 8583 Response Codes
2//!
3//! Standard response codes used in authorization and financial responses.
4//! These indicate the outcome of a transaction request.
5
6use std::fmt;
7
8/// ISO 8583 Response Code
9#[allow(missing_docs)]
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11pub struct ResponseCode(pub u8, pub u8);
12
13#[allow(missing_docs)]
14impl ResponseCode {
15    // Approval codes
16    pub const APPROVED: Self = Self(0, 0);
17    pub const APPROVED_WITH_ID: Self = Self(0, 1);
18    pub const APPROVED_PARTIAL: Self = Self(0, 2);
19
20    // Referral codes
21    pub const REFER_TO_ISSUER: Self = Self(0, 1);
22    pub const REFER_SPECIAL: Self = Self(0, 2);
23    pub const INVALID_MERCHANT: Self = Self(0, 3);
24    pub const PICK_UP_CARD: Self = Self(0, 4);
25
26    // Decline codes
27    pub const DO_NOT_HONOR: Self = Self(0, 5);
28    pub const ERROR: Self = Self(0, 6);
29    pub const PICK_UP_SPECIAL: Self = Self(0, 7);
30    pub const HONOR_WITH_ID: Self = Self(0, 8);
31
32    // Format/validity errors
33    pub const INVALID_TRANSACTION: Self = Self(1, 2);
34    pub const INVALID_AMOUNT: Self = Self(1, 3);
35    pub const INVALID_CARD_NUMBER: Self = Self(1, 4);
36    pub const NO_SUCH_ISSUER: Self = Self(1, 5);
37
38    // Card/Account issues
39    pub const CUSTOMER_CANCELLATION: Self = Self(1, 7);
40    pub const DUPLICATE_TRANSACTION: Self = Self(1, 8);
41    pub const RE_ENTER_TRANSACTION: Self = Self(1, 9);
42    pub const INVALID_RESPONSE: Self = Self(2, 0);
43    pub const NO_ACTION_TAKEN: Self = Self(2, 1);
44    pub const SUSPECTED_MALFUNCTION: Self = Self(2, 2);
45    pub const UNACCEPTABLE_TRANSACTION_FEE: Self = Self(2, 3);
46    pub const FILE_UPDATE_NOT_SUPPORTED: Self = Self(2, 4);
47    pub const UNABLE_TO_LOCATE_RECORD: Self = Self(2, 5);
48    pub const DUPLICATE_RECORD: Self = Self(2, 6);
49    pub const FILE_UPDATE_EDIT_ERROR: Self = Self(2, 7);
50    pub const FILE_UPDATE_FILE_LOCKED: Self = Self(2, 8);
51    pub const FILE_UPDATE_FAILED: Self = Self(2, 9);
52    pub const FORMAT_ERROR: Self = Self(3, 0);
53
54    // Security/Authorization issues
55    pub const BANK_NOT_SUPPORTED: Self = Self(3, 1);
56    pub const COMPLETED_PARTIALLY: Self = Self(3, 2);
57    pub const EXPIRED_CARD_PICKUP: Self = Self(3, 3);
58    pub const SUSPECTED_FRAUD: Self = Self(3, 4);
59    pub const RESTRICTED_CARD: Self = Self(3, 6);
60    pub const CONTACT_ACQUIRER_SECURITY: Self = Self(3, 7);
61    pub const LOST_CARD: Self = Self(4, 1);
62    pub const STOLEN_CARD: Self = Self(4, 3);
63
64    // Insufficient funds/limits
65    pub const INSUFFICIENT_FUNDS: Self = Self(5, 1);
66    pub const NO_CHECKING_ACCOUNT: Self = Self(5, 2);
67    pub const NO_SAVINGS_ACCOUNT: Self = Self(5, 3);
68    pub const EXPIRED_CARD: Self = Self(5, 4);
69    pub const INCORRECT_PIN: Self = Self(5, 5);
70    pub const NO_CARD_RECORD: Self = Self(5, 6);
71    pub const TRANSACTION_NOT_PERMITTED: Self = Self(5, 7);
72    pub const TRANSACTION_NOT_PERMITTED_TERMINAL: Self = Self(5, 8);
73    pub const SUSPECTED_FRAUD_DECLINE: Self = Self(5, 9);
74    pub const CONTACT_ACQUIRER: Self = Self(6, 0);
75    pub const EXCEEDS_WITHDRAWAL_LIMIT: Self = Self(6, 1);
76    pub const RESTRICTED_CARD_DECLINE: Self = Self(6, 2);
77    pub const SECURITY_VIOLATION: Self = Self(6, 3);
78    pub const EXCEEDS_WITHDRAWAL_FREQUENCY: Self = Self(6, 5);
79
80    // PIN issues
81    pub const PIN_REQUIRED: Self = Self(7, 5);
82    pub const PIN_VALIDATION_NOT_POSSIBLE: Self = Self(7, 6);
83    pub const PIN_TRIES_EXCEEDED: Self = Self(7, 7);
84
85    // System/Network issues
86    pub const CRYPTOGRAPHIC_FAILURE: Self = Self(8, 0);
87    pub const CRYPTOGRAPHIC_KEY_SYNC_ERROR: Self = Self(8, 1);
88    pub const CVV_FAILURE: Self = Self(8, 2);
89    pub const CANT_VERIFY_PIN: Self = Self(8, 3);
90    pub const MESSAGE_FLOW_ERROR: Self = Self(8, 5);
91    pub const CUTOVER_IN_PROGRESS: Self = Self(9, 0);
92    pub const ISSUER_UNAVAILABLE: Self = Self(9, 1);
93    pub const ROUTING_ERROR: Self = Self(9, 2);
94    pub const DUPLICATE_TRANSMISSION: Self = Self(9, 4);
95    pub const RECONCILE_ERROR: Self = Self(9, 5);
96    pub const SYSTEM_MALFUNCTION: Self = Self(9, 6);
97
98    // Special conditions
99    pub const MAC_ERROR: Self = Self(9, 7);
100    pub const FAILED_SECURITY_CHECK: Self = Self(9, 8);
101
102    /// Create from two digits
103    pub fn new(first: u8, second: u8) -> Self {
104        Self(first, second)
105    }
106
107    /// Get human-readable description
108    pub fn description(&self) -> &'static str {
109        match (self.0, self.1) {
110            (0, 0) => "Approved or completed successfully",
111            (0, 1) => "Refer to card issuer",
112            (0, 2) => "Refer to card issuer, special condition",
113            (0, 3) => "Invalid merchant",
114            (0, 4) => "Pick up card",
115            (0, 5) => "Do not honor",
116            (0, 6) => "Error",
117            (0, 7) => "Pick up card, special condition",
118            (1, 2) => "Invalid transaction",
119            (1, 3) => "Invalid amount",
120            (1, 4) => "Invalid card number",
121            (1, 5) => "No such issuer",
122            (3, 0) => "Format error",
123            (4, 1) => "Lost card, pick up",
124            (4, 3) => "Stolen card, pick up",
125            (5, 1) => "Insufficient funds",
126            (5, 4) => "Expired card",
127            (5, 5) => "Incorrect PIN",
128            (5, 7) => "Transaction not permitted to cardholder",
129            (5, 8) => "Transaction not permitted to terminal",
130            (6, 1) => "Exceeds withdrawal amount limit",
131            (7, 5) => "PIN required",
132            (7, 7) => "PIN tries exceeded",
133            (9, 1) => "Issuer or switch inoperative",
134            (9, 6) => "System malfunction",
135            _ => "Unknown response code",
136        }
137    }
138
139    /// Check if the response indicates approval
140    pub fn is_approved(&self) -> bool {
141        self.0 == 0 && self.1 == 0
142    }
143
144    /// Check if response indicates a decline
145    pub fn is_declined(&self) -> bool {
146        !self.is_approved() && !self.is_referral() && !self.is_system_error()
147    }
148
149    /// Check if response indicates referral to issuer
150    pub fn is_referral(&self) -> bool {
151        matches!((self.0, self.1), (0, 1) | (0, 2))
152    }
153
154    /// Check if response indicates a system/network error
155    pub fn is_system_error(&self) -> bool {
156        matches!(self.0, 9 | 8) || matches!((self.0, self.1), (3, 0) | (0, 6))
157    }
158
159    /// Check if response indicates card should be retained
160    pub fn should_retain_card(&self) -> bool {
161        matches!((self.0, self.1), (0, 4) | (0, 7) | (4, 1) | (4, 3))
162    }
163
164    /// Get response category
165    pub fn category(&self) -> ResponseCategory {
166        match (self.0, self.1) {
167            (0, 0..=2) => ResponseCategory::Approved,
168            (0, 1..=4) | (0, 7) => ResponseCategory::Referral,
169            (4, 1) | (4, 3) => ResponseCategory::CardRetention,
170            (5, 1) | (6, 1) | (6, 5) => ResponseCategory::InsufficientFunds,
171            (5, 4) => ResponseCategory::ExpiredCard,
172            (5, 5) | (7, 5) | (7, 7) => ResponseCategory::PINError,
173            (9, _) | (8, _) => ResponseCategory::SystemError,
174            _ => ResponseCategory::Declined,
175        }
176    }
177}
178
179impl std::str::FromStr for ResponseCode {
180    type Err = ();
181
182    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
183        if s.len() != 2 {
184            return Err(());
185        }
186
187        let first = s.chars().nth(0).ok_or(())?.to_digit(10).ok_or(())? as u8;
188        let second = s.chars().nth(1).ok_or(())?.to_digit(10).ok_or(())? as u8;
189
190        Ok(Self(first, second))
191    }
192}
193
194/// Response code category
195#[allow(missing_docs)]
196#[derive(Debug, Clone, Copy, PartialEq, Eq)]
197pub enum ResponseCategory {
198    Approved,
199    Declined,
200    Referral,
201    CardRetention,
202    InsufficientFunds,
203    ExpiredCard,
204    PINError,
205    SystemError,
206}
207
208impl fmt::Display for ResponseCode {
209    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
210        write!(f, "{:02}", self.0 * 10 + self.1)
211    }
212}
213
214impl fmt::Display for ResponseCategory {
215    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
216        match self {
217            Self::Approved => write!(f, "Approved"),
218            Self::Declined => write!(f, "Declined"),
219            Self::Referral => write!(f, "Referral"),
220            Self::CardRetention => write!(f, "Card Retention"),
221            Self::InsufficientFunds => write!(f, "Insufficient Funds"),
222            Self::ExpiredCard => write!(f, "Expired Card"),
223            Self::PINError => write!(f, "PIN Error"),
224            Self::SystemError => write!(f, "System Error"),
225        }
226    }
227}
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232
233    #[test]
234    fn test_response_codes() {
235        assert!(ResponseCode::APPROVED.is_approved());
236        assert!(!ResponseCode::DO_NOT_HONOR.is_approved());
237        assert!(ResponseCode::INSUFFICIENT_FUNDS.is_declined());
238        assert!(ResponseCode::REFER_TO_ISSUER.is_referral());
239        assert!(ResponseCode::ISSUER_UNAVAILABLE.is_system_error());
240    }
241
242    #[test]
243    fn test_card_retention() {
244        assert!(ResponseCode::LOST_CARD.should_retain_card());
245        assert!(ResponseCode::STOLEN_CARD.should_retain_card());
246        assert!(!ResponseCode::INSUFFICIENT_FUNDS.should_retain_card());
247    }
248
249    #[test]
250    fn test_response_categories() {
251        assert_eq!(
252            ResponseCode::APPROVED.category(),
253            ResponseCategory::Approved
254        );
255        assert_eq!(
256            ResponseCode::INSUFFICIENT_FUNDS.category(),
257            ResponseCategory::InsufficientFunds
258        );
259        assert_eq!(
260            ResponseCode::INCORRECT_PIN.category(),
261            ResponseCategory::PINError
262        );
263    }
264
265    #[test]
266    fn test_from_string() {
267        let code = "00".parse::<ResponseCode>().unwrap();
268        assert_eq!(code, ResponseCode::APPROVED);
269
270        let code = "51".parse::<ResponseCode>().unwrap();
271        assert_eq!(code, ResponseCode::INSUFFICIENT_FUNDS);
272    }
273
274    #[test]
275    fn test_to_string() {
276        assert_eq!(ResponseCode::APPROVED.to_string(), "00");
277        assert_eq!(ResponseCode::INSUFFICIENT_FUNDS.to_string(), "51");
278    }
279}