Skip to main content

iso8583_core/
processing_code.rs

1//! ISO 8583 Processing Codes
2//!
3//! Processing codes indicate the type of transaction and account types involved.
4//! Format: TTFFTT where:
5//! - TT (positions 1-2): Transaction type
6//! - FF (positions 3-4): From account type
7//! - TT (positions 5-6): To account type
8
9use std::fmt;
10
11/// Processing Code (6 digits)
12#[allow(missing_docs)]
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14pub struct ProcessingCode {
15    pub transaction_type: TransactionType,
16    pub from_account: AccountType,
17    pub to_account: AccountType,
18}
19
20/// Transaction Type (first 2 digits)
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22pub enum TransactionType {
23    /// Purchase (00)
24    Purchase = 0,
25    /// Cash withdrawal (01)
26    CashWithdrawal = 1,
27    /// Debit adjustment (02)
28    DebitAdjustment = 2,
29    /// Check guarantee (03)
30    CheckGuarantee = 3,
31    /// Check verification (04)
32    CheckVerification = 4,
33    /// Eurocheque (05)
34    Eurocheque = 5,
35    /// Travelers check (06)
36    TravelersCheck = 6,
37    /// Letter of credit (07)
38    LetterOfCredit = 7,
39    /// Giro (08)
40    Giro = 8,
41    /// Cash deposit (21)
42    CashDeposit = 21,
43    /// Check deposit (22)
44    CheckDeposit = 22,
45    /// Balance inquiry (31)
46    BalanceInquiry = 31,
47    /// Mini statement (38)
48    MiniStatement = 38,
49    /// Transfer from checking to savings (40)
50    TransferCheckingToSavings = 40,
51    /// Transfer from savings to checking (41)
52    TransferSavingsToChecking = 41,
53    /// Refund (20)
54    Refund = 20,
55    /// Payment (50)
56    Payment = 50,
57}
58
59/// Account Type (positions 3-4 and 5-6)
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
61pub enum AccountType {
62    /// Default/Unspecified (00)
63    Default = 0,
64    /// Savings account (10)
65    Savings = 10,
66    /// Checking account (20)
67    Checking = 20,
68    /// Credit account (30)
69    Credit = 30,
70    /// Universal account (40)
71    Universal = 40,
72    /// Investment account (50)
73    Investment = 50,
74}
75
76impl ProcessingCode {
77    /// Common processing codes as constants
78    ///
79    /// Purchase from default account (000000)
80    pub const PURCHASE: Self = Self {
81        transaction_type: TransactionType::Purchase,
82        from_account: AccountType::Default,
83        to_account: AccountType::Default,
84    };
85
86    /// Cash withdrawal from checking (010000)
87    pub const WITHDRAWAL_CHECKING: Self = Self {
88        transaction_type: TransactionType::CashWithdrawal,
89        from_account: AccountType::Default,
90        to_account: AccountType::Default,
91    };
92
93    /// Cash withdrawal from savings (011000)
94    pub const WITHDRAWAL_SAVINGS: Self = Self {
95        transaction_type: TransactionType::CashWithdrawal,
96        from_account: AccountType::Savings,
97        to_account: AccountType::Default,
98    };
99
100    /// Deposit to checking (210000)
101    pub const DEPOSIT_CHECKING: Self = Self {
102        transaction_type: TransactionType::CashDeposit,
103        from_account: AccountType::Default,
104        to_account: AccountType::Default,
105    };
106
107    /// Deposit to savings (211000)
108    pub const DEPOSIT_SAVINGS: Self = Self {
109        transaction_type: TransactionType::CashDeposit,
110        from_account: AccountType::Savings,
111        to_account: AccountType::Default,
112    };
113
114    /// Balance inquiry checking (310000)
115    pub const BALANCE_INQUIRY_CHECKING: Self = Self {
116        transaction_type: TransactionType::BalanceInquiry,
117        from_account: AccountType::Default,
118        to_account: AccountType::Default,
119    };
120
121    /// Balance inquiry savings (311000)
122    pub const BALANCE_INQUIRY_SAVINGS: Self = Self {
123        transaction_type: TransactionType::BalanceInquiry,
124        from_account: AccountType::Savings,
125        to_account: AccountType::Default,
126    };
127
128    /// Refund (200000)
129    pub const REFUND: Self = Self {
130        transaction_type: TransactionType::Refund,
131        from_account: AccountType::Default,
132        to_account: AccountType::Default,
133    };
134
135    /// Transfer checking to savings (401020)
136    pub const TRANSFER_CHECKING_TO_SAVINGS: Self = Self {
137        transaction_type: TransactionType::TransferCheckingToSavings,
138        from_account: AccountType::Savings,
139        to_account: AccountType::Checking,
140    };
141
142    /// Create new processing code
143    pub fn new(
144        transaction_type: TransactionType,
145        from_account: AccountType,
146        to_account: AccountType,
147    ) -> Self {
148        Self {
149            transaction_type,
150            from_account,
151            to_account,
152        }
153    }
154
155    /// Get transaction description
156    pub fn description(&self) -> String {
157        let txn_desc = match self.transaction_type {
158            TransactionType::Purchase => "Purchase",
159            TransactionType::CashWithdrawal => "Cash Withdrawal",
160            TransactionType::CashDeposit => "Deposit",
161            TransactionType::BalanceInquiry => "Balance Inquiry",
162            TransactionType::Refund => "Refund",
163            TransactionType::Payment => "Payment",
164            TransactionType::TransferCheckingToSavings => "Transfer",
165            TransactionType::TransferSavingsToChecking => "Transfer",
166            _ => "Transaction",
167        };
168
169        let from_desc = match self.from_account {
170            AccountType::Savings => " from Savings",
171            AccountType::Checking => " from Checking",
172            AccountType::Credit => " from Credit",
173            AccountType::Default => "",
174            _ => " from Account",
175        };
176
177        let to_desc = match self.to_account {
178            AccountType::Savings => " to Savings",
179            AccountType::Checking => " to Checking",
180            AccountType::Credit => " to Credit",
181            AccountType::Default => "",
182            _ => " to Account",
183        };
184
185        format!("{}{}{}", txn_desc, from_desc, to_desc)
186    }
187
188    /// Check if this is a balance inquiry
189    pub fn is_inquiry(&self) -> bool {
190        matches!(
191            self.transaction_type,
192            TransactionType::BalanceInquiry | TransactionType::MiniStatement
193        )
194    }
195
196    /// Check if this is a cash transaction
197    pub fn is_cash(&self) -> bool {
198        matches!(
199            self.transaction_type,
200            TransactionType::CashWithdrawal | TransactionType::CashDeposit
201        )
202    }
203
204    /// Check if this is a transfer
205    pub fn is_transfer(&self) -> bool {
206        matches!(
207            self.transaction_type,
208            TransactionType::TransferCheckingToSavings | TransactionType::TransferSavingsToChecking
209        )
210    }
211}
212
213impl std::str::FromStr for ProcessingCode {
214    type Err = ();
215
216    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
217        if s.len() != 6 {
218            return Err(());
219        }
220
221        let tt = s[0..2].parse::<u8>().map_err(|_| ())?;
222        let from = s[2..4].parse::<u8>().map_err(|_| ())?;
223        let to = s[4..6].parse::<u8>().map_err(|_| ())?;
224
225        Ok(Self {
226            transaction_type: TransactionType::from_code(tt).ok_or(())?,
227            from_account: AccountType::from_code(from).ok_or(())?,
228            to_account: AccountType::from_code(to).ok_or(())?,
229        })
230    }
231}
232
233#[allow(missing_docs)]
234impl TransactionType {
235    pub fn from_code(code: u8) -> Option<Self> {
236        match code {
237            0 => Some(Self::Purchase),
238            1 => Some(Self::CashWithdrawal),
239            2 => Some(Self::DebitAdjustment),
240            20 => Some(Self::Refund),
241            21 => Some(Self::CashDeposit),
242            22 => Some(Self::CheckDeposit),
243            31 => Some(Self::BalanceInquiry),
244            38 => Some(Self::MiniStatement),
245            40 => Some(Self::TransferCheckingToSavings),
246            41 => Some(Self::TransferSavingsToChecking),
247            50 => Some(Self::Payment),
248            _ => None,
249        }
250    }
251
252    pub fn to_code(&self) -> u8 {
253        match self {
254            Self::Purchase => 0,
255            Self::CashWithdrawal => 1,
256            Self::DebitAdjustment => 2,
257            Self::Refund => 20,
258            Self::CashDeposit => 21,
259            Self::CheckDeposit => 22,
260            Self::BalanceInquiry => 31,
261            Self::MiniStatement => 38,
262            Self::TransferCheckingToSavings => 40,
263            Self::TransferSavingsToChecking => 41,
264            Self::Payment => 50,
265            _ => 0,
266        }
267    }
268}
269
270#[allow(missing_docs)]
271impl AccountType {
272    pub fn from_code(code: u8) -> Option<Self> {
273        match code {
274            0 => Some(Self::Default),
275            10 => Some(Self::Savings),
276            20 => Some(Self::Checking),
277            30 => Some(Self::Credit),
278            40 => Some(Self::Universal),
279            50 => Some(Self::Investment),
280            _ => Some(Self::Default), // Default for unrecognized codes
281        }
282    }
283
284    pub fn to_code(&self) -> u8 {
285        *self as u8
286    }
287}
288
289impl fmt::Display for ProcessingCode {
290    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
291        write!(
292            f,
293            "{:02}{:02}{:02}",
294            self.transaction_type.to_code(),
295            self.from_account.to_code(),
296            self.to_account.to_code()
297        )
298    }
299}
300
301#[cfg(test)]
302mod tests {
303    use super::*;
304
305    #[test]
306    fn test_processing_codes() {
307        assert_eq!(ProcessingCode::PURCHASE.to_string(), "000000");
308        assert_eq!(ProcessingCode::WITHDRAWAL_CHECKING.to_string(), "010000");
309        assert_eq!(
310            ProcessingCode::BALANCE_INQUIRY_SAVINGS.to_string(),
311            "311000"
312        );
313    }
314
315    #[test]
316    fn test_from_string() {
317        let code = "000000".parse::<ProcessingCode>().unwrap();
318        assert_eq!(code, ProcessingCode::PURCHASE);
319
320        let code = "010000".parse::<ProcessingCode>().unwrap();
321        assert_eq!(code, ProcessingCode::WITHDRAWAL_CHECKING);
322    }
323
324    #[test]
325    fn test_descriptions() {
326        assert_eq!(ProcessingCode::PURCHASE.description(), "Purchase");
327        assert_eq!(
328            ProcessingCode::WITHDRAWAL_SAVINGS.description(),
329            "Cash Withdrawal from Savings"
330        );
331        assert_eq!(
332            ProcessingCode::BALANCE_INQUIRY_CHECKING.description(),
333            "Balance Inquiry"
334        );
335    }
336
337    #[test]
338    fn test_predicates() {
339        assert!(ProcessingCode::BALANCE_INQUIRY_CHECKING.is_inquiry());
340        assert!(ProcessingCode::WITHDRAWAL_CHECKING.is_cash());
341        assert!(!ProcessingCode::PURCHASE.is_cash());
342    }
343}