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>;