Skip to main content

ecr17_protocol/
error.rs

1//! Crate error type.
2
3use thiserror::Error;
4
5/// Errors produced while building or parsing ECR17 messages (and, under the
6/// `tokio-transport` feature, while talking to a terminal).
7///
8/// The type intentionally derives `Clone`/`PartialEq`/`Eq` (ergonomic for callers and
9/// tests). To keep those derives as new variants are added, error data is modeled with
10/// `Clone + Eq` types — e.g. transport variants will carry a [`std::io::ErrorKind`] plus a
11/// message `String` rather than a non-`Clone`/non-`Eq` `std::io::Error`.
12#[derive(Debug, Clone, PartialEq, Eq, Error)]
13#[non_exhaustive]
14pub enum Ecr17Error {
15    /// A value is longer than its fixed-width field. ECR17 fields have a fixed
16    /// length, so an oversized value would shift every following field and corrupt
17    /// the frame — building it is refused.
18    #[error("ECR17: value '{value}' exceeds fixed field width of {width} bytes")]
19    FieldOverflow {
20        /// The offending value.
21        value: String,
22        /// The field width it overflowed.
23        width: usize,
24    },
25
26    /// A monetary amount was negative.
27    #[error("ECR17: amount must be non-negative")]
28    NegativeAmount,
29
30    /// The payment-type digit was not one of `'0'` (auto), `'1'` (debit), `'2'` (credit),
31    /// `'3'` (other). Refused so a malformed frame is never sent to a card-charging terminal.
32    #[error("ECR17: invalid payment type '{value}' (expected '0'..'3')")]
33    InvalidPaymentType {
34        /// The offending character.
35        value: char,
36    },
37
38    /// A VAS request payload exceeded the 1024-byte limit.
39    #[error("ECR17: VAS request exceeds 1024 bytes")]
40    VasTooLong,
41
42    /// The additional-data TAG content was empty, longer than 100 chars, or contained the
43    /// field separator (`0x1B`), which would prematurely terminate the field.
44    #[error("ECR17: additional TAG content must be 1..=100 chars with no field separator (0x1B)")]
45    TagContentInvalid,
46
47    /// The tokenization contract code was empty, longer than 18 chars, or contained a
48    /// non-alphanumeric character (it is interpolated into a structured TAG value).
49    #[error("ECR17: tokenization contract code must be 1..=18 alphanumeric chars")]
50    ContractCodeInvalid,
51
52    /// The transport dropped during an exchange. 💰 A financial command is NEVER blindly
53    /// re-sent after this — recover a lost response via `send_last_result` (`G`).
54    #[error("ECR17: transport disconnected during exchange")]
55    Disconnected,
56
57    /// No physical `ACK` was received after `attempts` send attempts.
58    #[error("ECR17: no ACK after {attempts} attempt(s)")]
59    AckTimeout {
60        /// Total send attempts made (initial + retransmissions).
61        attempts: u32,
62    },
63
64    /// The terminal `NAK`ed the request after `attempts` send attempts.
65    #[error("ECR17: NAK after {attempts} attempt(s)")]
66    Nak {
67        /// Total send attempts made (initial + retransmissions).
68        attempts: u32,
69    },
70
71    /// No application response arrived before the response timeout (after the ACK).
72    #[error("ECR17: no application response before timeout")]
73    ResponseTimeout,
74
75    /// A transport-layer I/O error (real TCP transport). Carries the [`std::io::ErrorKind`]
76    /// (`Clone + Eq`) plus a message rather than the non-`Clone` `std::io::Error`.
77    #[error("ECR17: transport error ({kind:?}): {message}")]
78    Transport {
79        /// The underlying I/O error kind.
80        kind: std::io::ErrorKind,
81        /// A human-readable message.
82        message: String,
83    },
84}
85
86#[cfg(feature = "tokio-transport")]
87impl From<std::io::Error> for Ecr17Error {
88    fn from(e: std::io::Error) -> Self {
89        Ecr17Error::Transport {
90            kind: e.kind(),
91            message: e.to_string(),
92        }
93    }
94}
95
96/// Convenience result alias.
97pub type Result<T> = core::result::Result<T, Ecr17Error>;