Skip to main content

iso8583_core/
mti.rs

1//! Message Type Indicator (MTI) definitions and parsing
2//!
3//! The MTI is a 4-digit numeric field that indicates the message's purpose:
4//! - Position 1: Version (0-9)
5//! - Position 2: Message Class (Authorization, Financial, etc.)
6//! - Position 3: Message Function (Request, Response, Advice, etc.)
7//! - Position 4: Message Origin (Acquirer, Issuer, etc.)
8
9use crate::error::{ISO8583Error, Result};
10use std::fmt;
11
12/// ISO 8583 Message Type Indicator
13#[allow(missing_docs)]
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub struct MessageType {
16    pub version: u8,
17    pub class: MessageClass,
18    pub function: MessageFunction,
19    pub origin: MessageOrigin,
20}
21
22/// Message Class (2nd digit of MTI)
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
24pub enum MessageClass {
25    /// Reserved for ISO use (0xxx)
26    Reserved = 0,
27    /// Authorization (01xx, 02xx)
28    Authorization = 1,
29    /// Financial transactions (02xx)
30    Financial = 2,
31    /// File actions (03xx)
32    FileActions = 3,
33    /// Reversal/Chargeback (04xx)
34    Reversal = 4,
35    /// Reconciliation (05xx)
36    Reconciliation = 5,
37    /// Administrative (06xx)
38    Administrative = 6,
39    /// Fee collection (07xx)
40    FeeCollection = 7,
41    /// Network management (08xx)
42    NetworkManagement = 8,
43    /// Reserved for ISO use (09xx)
44    ReservedISO = 9,
45}
46
47/// Message Function (3rd digit of MTI)
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
49pub enum MessageFunction {
50    /// Request (xx0x)
51    Request = 0,
52    /// Request response (xx1x)
53    Response = 1,
54    /// Advice (xx2x)
55    Advice = 2,
56    /// Advice response (xx3x)
57    AdviceResponse = 3,
58    /// Notification (xx4x)
59    Notification = 4,
60    /// Notification acknowledgement (xx5x)
61    NotificationAck = 5,
62    /// Instruction (xx6x)
63    Instruction = 6,
64    /// Instruction acknowledgement (xx7x)
65    InstructionAck = 7,
66    /// Reserved (xx8x)
67    Reserved8 = 8,
68    /// Reserved (xx9x)
69    Reserved9 = 9,
70}
71
72/// Message Origin (4th digit of MTI)
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
74pub enum MessageOrigin {
75    /// Acquirer (xxx0)
76    Acquirer = 0,
77    /// Acquirer repeat (xxx1)
78    AcquirerRepeat = 1,
79    /// Issuer (xxx2)
80    Issuer = 2,
81    /// Issuer repeat (xxx3)
82    IssuerRepeat = 3,
83    /// Other (xxx4)
84    Other = 4,
85    /// Other repeat (xxx5)
86    OtherRepeat = 5,
87    /// Reserved (xxx6)
88    Reserved6 = 6,
89    /// Reserved (xxx7)
90    Reserved7 = 7,
91    /// Reserved (xxx8)
92    Reserved8 = 8,
93    /// Reserved (xxx9)
94    Reserved9 = 9,
95}
96
97impl MessageType {
98    /// Common message types as constants
99    ///
100    /// Authorization request (0100)
101    pub const AUTHORIZATION_REQUEST: Self = Self {
102        version: 0,
103        class: MessageClass::Authorization,
104        function: MessageFunction::Request,
105        origin: MessageOrigin::Acquirer,
106    };
107
108    /// Authorization response (0110)
109    pub const AUTHORIZATION_RESPONSE: Self = Self {
110        version: 0,
111        class: MessageClass::Authorization,
112        function: MessageFunction::Response,
113        origin: MessageOrigin::Acquirer,
114    };
115
116    /// Authorization advice (0120)
117    pub const AUTHORIZATION_ADVICE: Self = Self {
118        version: 0,
119        class: MessageClass::Authorization,
120        function: MessageFunction::Advice,
121        origin: MessageOrigin::Acquirer,
122    };
123
124    /// Authorization advice response (0130)
125    pub const AUTHORIZATION_ADVICE_RESPONSE: Self = Self {
126        version: 0,
127        class: MessageClass::Authorization,
128        function: MessageFunction::AdviceResponse,
129        origin: MessageOrigin::Acquirer,
130    };
131
132    /// Financial request (0200)
133    pub const FINANCIAL_REQUEST: Self = Self {
134        version: 0,
135        class: MessageClass::Financial,
136        function: MessageFunction::Request,
137        origin: MessageOrigin::Acquirer,
138    };
139
140    /// Financial response (0210)
141    pub const FINANCIAL_RESPONSE: Self = Self {
142        version: 0,
143        class: MessageClass::Financial,
144        function: MessageFunction::Response,
145        origin: MessageOrigin::Acquirer,
146    };
147
148    /// Financial advice (0220)
149    pub const FINANCIAL_ADVICE: Self = Self {
150        version: 0,
151        class: MessageClass::Financial,
152        function: MessageFunction::Advice,
153        origin: MessageOrigin::Acquirer,
154    };
155
156    /// Financial advice response (0230)
157    pub const FINANCIAL_ADVICE_RESPONSE: Self = Self {
158        version: 0,
159        class: MessageClass::Financial,
160        function: MessageFunction::AdviceResponse,
161        origin: MessageOrigin::Acquirer,
162    };
163
164    /// Reversal request (0400)
165    pub const REVERSAL_REQUEST: Self = Self {
166        version: 0,
167        class: MessageClass::Reversal,
168        function: MessageFunction::Request,
169        origin: MessageOrigin::Acquirer,
170    };
171
172    /// Reversal response (0410)
173    pub const REVERSAL_RESPONSE: Self = Self {
174        version: 0,
175        class: MessageClass::Reversal,
176        function: MessageFunction::Response,
177        origin: MessageOrigin::Acquirer,
178    };
179
180    /// Reversal advice (0420)
181    pub const REVERSAL_ADVICE: Self = Self {
182        version: 0,
183        class: MessageClass::Reversal,
184        function: MessageFunction::Advice,
185        origin: MessageOrigin::Acquirer,
186    };
187
188    /// Reversal advice response (0430)
189    pub const REVERSAL_ADVICE_RESPONSE: Self = Self {
190        version: 0,
191        class: MessageClass::Reversal,
192        function: MessageFunction::AdviceResponse,
193        origin: MessageOrigin::Acquirer,
194    };
195
196    /// Network management request (0800)
197    pub const NETWORK_MANAGEMENT_REQUEST: Self = Self {
198        version: 0,
199        class: MessageClass::NetworkManagement,
200        function: MessageFunction::Request,
201        origin: MessageOrigin::Acquirer,
202    };
203
204    /// Network management response (0810)
205    pub const NETWORK_MANAGEMENT_RESPONSE: Self = Self {
206        version: 0,
207        class: MessageClass::NetworkManagement,
208        function: MessageFunction::Response,
209        origin: MessageOrigin::Acquirer,
210    };
211
212    /// Network management advice (0820)
213    pub const NETWORK_MANAGEMENT_ADVICE: Self = Self {
214        version: 0,
215        class: MessageClass::NetworkManagement,
216        function: MessageFunction::Advice,
217        origin: MessageOrigin::Acquirer,
218    };
219
220    /// Create a new MTI from components
221    pub fn new(
222        version: u8,
223        class: MessageClass,
224        function: MessageFunction,
225        origin: MessageOrigin,
226    ) -> Self {
227        Self {
228            version,
229            class,
230            function,
231            origin,
232        }
233    }
234
235    /// Parse MTI from bytes
236    pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
237        if bytes.len() < 4 {
238            return Err(ISO8583Error::InvalidMTI(format!(
239                "MTI must be at least 4 bytes, got {}",
240                bytes.len()
241            )));
242        }
243
244        let s = std::str::from_utf8(&bytes[..4])
245            .map_err(|e| ISO8583Error::InvalidMTI(format!("Invalid UTF-8: {}", e)))?;
246
247        s.parse()
248    }
249
250    /// Convert to bytes
251    pub fn to_bytes(&self) -> Vec<u8> {
252        self.to_string().into_bytes()
253    }
254
255    /// Check if this is a request message
256    pub fn is_request(&self) -> bool {
257        matches!(self.function, MessageFunction::Request)
258    }
259
260    /// Check if this is a response message
261    pub fn is_response(&self) -> bool {
262        matches!(self.function, MessageFunction::Response)
263    }
264
265    /// Check if this is an advice message
266    pub fn is_advice(&self) -> bool {
267        matches!(
268            self.function,
269            MessageFunction::Advice | MessageFunction::AdviceResponse
270        )
271    }
272
273    /// Get the corresponding response MTI for a request
274    pub fn to_response(&self) -> Result<Self> {
275        if !self.is_request() {
276            return Err(ISO8583Error::InvalidMTI(
277                "Can only convert request to response".to_string(),
278            ));
279        }
280
281        Ok(Self {
282            version: self.version,
283            class: self.class,
284            function: MessageFunction::Response,
285            origin: self.origin,
286        })
287    }
288}
289
290impl std::str::FromStr for MessageType {
291    type Err = ISO8583Error;
292
293    fn from_str(s: &str) -> Result<Self> {
294        if s.len() != 4 {
295            return Err(ISO8583Error::InvalidMTI(format!(
296                "MTI must be 4 digits, got {}",
297                s.len()
298            )));
299        }
300
301        let digits: Vec<u8> = s
302            .chars()
303            .map(|c| {
304                c.to_digit(10)
305                    .map(|d| d as u8)
306                    .ok_or_else(|| ISO8583Error::InvalidMTI(format!("Invalid digit: {}", c)))
307            })
308            .collect::<Result<Vec<_>>>()?;
309
310        Ok(Self {
311            version: digits[0],
312            class: MessageClass::from_digit(digits[1])?,
313            function: MessageFunction::from_digit(digits[2])?,
314            origin: MessageOrigin::from_digit(digits[3])?,
315        })
316    }
317}
318
319impl MessageClass {
320    fn from_digit(digit: u8) -> Result<Self> {
321        match digit {
322            0 => Ok(Self::Reserved),
323            1 => Ok(Self::Authorization),
324            2 => Ok(Self::Financial),
325            3 => Ok(Self::FileActions),
326            4 => Ok(Self::Reversal),
327            5 => Ok(Self::Reconciliation),
328            6 => Ok(Self::Administrative),
329            7 => Ok(Self::FeeCollection),
330            8 => Ok(Self::NetworkManagement),
331            9 => Ok(Self::ReservedISO),
332            _ => Err(ISO8583Error::InvalidMessageClass(format!(
333                "Invalid message class digit: {}",
334                digit
335            ))),
336        }
337    }
338
339    fn to_digit(self) -> u8 {
340        self as u8
341    }
342}
343
344impl MessageFunction {
345    fn from_digit(digit: u8) -> Result<Self> {
346        match digit {
347            0 => Ok(Self::Request),
348            1 => Ok(Self::Response),
349            2 => Ok(Self::Advice),
350            3 => Ok(Self::AdviceResponse),
351            4 => Ok(Self::Notification),
352            5 => Ok(Self::NotificationAck),
353            6 => Ok(Self::Instruction),
354            7 => Ok(Self::InstructionAck),
355            8 => Ok(Self::Reserved8),
356            9 => Ok(Self::Reserved9),
357            _ => Err(ISO8583Error::InvalidMessageFunction(format!(
358                "Invalid message function digit: {}",
359                digit
360            ))),
361        }
362    }
363
364    fn to_digit(self) -> u8 {
365        self as u8
366    }
367}
368
369impl MessageOrigin {
370    fn from_digit(digit: u8) -> Result<Self> {
371        match digit {
372            0 => Ok(Self::Acquirer),
373            1 => Ok(Self::AcquirerRepeat),
374            2 => Ok(Self::Issuer),
375            3 => Ok(Self::IssuerRepeat),
376            4 => Ok(Self::Other),
377            5 => Ok(Self::OtherRepeat),
378            6 => Ok(Self::Reserved6),
379            7 => Ok(Self::Reserved7),
380            8 => Ok(Self::Reserved8),
381            9 => Ok(Self::Reserved9),
382            _ => Err(ISO8583Error::InvalidMessageOrigin(format!(
383                "Invalid message origin digit: {}",
384                digit
385            ))),
386        }
387    }
388
389    fn to_digit(self) -> u8 {
390        self as u8
391    }
392}
393
394impl fmt::Display for MessageType {
395    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
396        write!(
397            f,
398            "{}{}{}{}",
399            self.version,
400            self.class.to_digit(),
401            self.function.to_digit(),
402            self.origin.to_digit()
403        )
404    }
405}
406
407#[cfg(test)]
408mod tests {
409    use super::*;
410
411    #[test]
412    fn test_mti_parsing() {
413        let mti: MessageType = "0100".parse().unwrap();
414        assert_eq!(mti.version, 0);
415        assert_eq!(mti.class, MessageClass::Authorization);
416        assert_eq!(mti.function, MessageFunction::Request);
417        assert_eq!(mti.origin, MessageOrigin::Acquirer);
418        assert_eq!(mti.to_string(), "0100");
419    }
420
421    #[test]
422    fn test_mti_constants() {
423        assert_eq!(MessageType::AUTHORIZATION_REQUEST.to_string(), "0100");
424        assert_eq!(MessageType::AUTHORIZATION_RESPONSE.to_string(), "0110");
425        assert_eq!(MessageType::FINANCIAL_REQUEST.to_string(), "0200");
426        assert_eq!(MessageType::FINANCIAL_RESPONSE.to_string(), "0210");
427        assert_eq!(MessageType::REVERSAL_REQUEST.to_string(), "0400");
428        assert_eq!(MessageType::NETWORK_MANAGEMENT_REQUEST.to_string(), "0800");
429    }
430
431    #[test]
432    fn test_mti_predicates() {
433        let request = MessageType::AUTHORIZATION_REQUEST;
434        assert!(request.is_request());
435        assert!(!request.is_response());
436        assert!(!request.is_advice());
437
438        let response = MessageType::AUTHORIZATION_RESPONSE;
439        assert!(!response.is_request());
440        assert!(response.is_response());
441        assert!(!response.is_advice());
442
443        let advice = MessageType::AUTHORIZATION_ADVICE;
444        assert!(!advice.is_request());
445        assert!(!advice.is_response());
446        assert!(advice.is_advice());
447    }
448
449    #[test]
450    fn test_to_response() {
451        let request = MessageType::AUTHORIZATION_REQUEST;
452        let response = request.to_response().unwrap();
453        assert_eq!(response, MessageType::AUTHORIZATION_RESPONSE);
454
455        // Cannot convert response to response
456        let err = response.to_response();
457        assert!(err.is_err());
458    }
459
460    #[test]
461    fn test_invalid_mti() {
462        assert!("123".parse::<MessageType>().is_err()); // Too short
463        assert!("12345".parse::<MessageType>().is_err()); // Too long
464        assert!("abcd".parse::<MessageType>().is_err()); // Invalid chars
465    }
466}