Skip to main content

ecr17_protocol/
types.rs

1//! Request/result/config data model (serde), mirroring the reference `types/client.ts`
2//! field-for-field. Enums serialize to the same JSON string unions as the TS API and
3//! struct fields use `camelCase`, so the model maps cleanly onto the Tauri IPC. Absent
4//! optional fields deserialize to `None`; a `None` serializes as JSON `null` (the TS side
5//! treats `null`/absent equivalently for these optional fields).
6
7use serde::{Deserialize, Serialize};
8
9use crate::lrc::LrcMode;
10
11// ---------------------------------------------------------------------------
12// Enums (string unions)
13// ---------------------------------------------------------------------------
14
15/// Transport connection lifecycle.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
17#[serde(rename_all = "lowercase")]
18pub enum ConnectionState {
19    /// Not connected.
20    #[default]
21    Disconnected,
22    /// Connection in progress.
23    Connecting,
24    /// Connected and ready.
25    Connected,
26}
27
28/// Normalized transaction outcome (mapped from the raw 2-digit result code):
29/// `"00" -> ok`, `"01" -> ko`, `"05" -> cardNotPresent`, `"09" -> unknownTag`.
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
31#[serde(rename_all = "camelCase")]
32pub enum TransactionOutcome {
33    /// Approved.
34    Ok,
35    /// Declined / failed.
36    Ko,
37    /// The card was not present when required.
38    CardNotPresent,
39    /// The requested TAG was unknown to the terminal.
40    UnknownTag,
41    /// Unrecognized result code.
42    #[default]
43    Unknown,
44}
45
46/// Card category reported by the terminal (response "Card type": 1/2/3).
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
48#[serde(rename_all = "lowercase")]
49pub enum CardType {
50    /// Debit card.
51    Debit,
52    /// Credit card.
53    Credit,
54    /// Other card product.
55    Other,
56    /// Unknown / not reported.
57    #[default]
58    Unknown,
59}
60
61/// How the card was read (response "Transaction type": ICC/MAG/MAN/CLM/CLI).
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
63#[serde(rename_all = "camelCase")]
64pub enum TransactionEntryMode {
65    /// Chip (ICC).
66    Icc,
67    /// Magnetic stripe.
68    Mag,
69    /// Manual PAN entry.
70    Manual,
71    /// Contactless magstripe.
72    ClessMag,
73    /// Contactless chip.
74    ClessIcc,
75    /// Unknown / not reported.
76    #[default]
77    Unknown,
78}
79
80/// Requested card handling for a payment (request "Payment type": 0..3).
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
82#[serde(rename_all = "lowercase")]
83pub enum PaymentCardType {
84    /// Let the terminal decide (`'0'`).
85    #[default]
86    Auto,
87    /// Force debit (`'1'`).
88    Debit,
89    /// Force credit (`'2'`).
90    Credit,
91    /// Other (`'3'`).
92    Other,
93}
94
95impl PaymentCardType {
96    /// The single request digit (`'0'`..`'3'`) for this card handling.
97    #[must_use]
98    pub fn as_digit(self) -> char {
99        match self {
100            PaymentCardType::Auto => '0',
101            PaymentCardType::Debit => '1',
102            PaymentCardType::Credit => '2',
103            PaymentCardType::Other => '3',
104        }
105    }
106}
107
108/// Tokenization service requested alongside a payment/preauth/verification (command `U`).
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
110#[serde(rename_all = "camelCase")]
111pub enum TokenizationService {
112    /// Recurring (`0REC`).
113    Recurring,
114    /// Unscheduled / one-click (`0COF`).
115    UnscheduledOrOneClick,
116}
117
118impl TokenizationService {
119    /// Whether this maps to the recurring (`0REC`) mapping vs one-click (`0COF`).
120    #[must_use]
121    pub fn is_recurring(self) -> bool {
122        matches!(self, TokenizationService::Recurring)
123    }
124}
125
126/// Tokenization additional-data request (`U`).
127#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
128#[serde(rename_all = "camelCase")]
129pub struct TokenizationRequest {
130    /// Which tokenization mapping to request.
131    pub service: TokenizationService,
132    /// Unique contract code at merchant level, alphanumeric, up to 18 chars.
133    pub contract_code: String,
134}
135
136// ---------------------------------------------------------------------------
137// Configuration
138// ---------------------------------------------------------------------------
139
140/// Client/session configuration.
141#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
142#[serde(rename_all = "camelCase")]
143pub struct Ecr17Config {
144    /// POS terminal host (IP address).
145    pub host: String,
146    /// TCP port (terminal default when omitted).
147    pub port: Option<u16>,
148
149    /// Terminal identifier (max 8 chars).
150    pub terminal_id: String,
151    /// Cash-register identifier (max 8 chars).
152    pub cash_register_id: String,
153
154    /// LRC framing mode.
155    pub lrc_mode: Option<LrcMode>,
156
157    /// Keep the socket open between transactions.
158    pub keep_alive: Option<bool>,
159    /// Reconnect automatically on a mid-session drop (financial ops are still never
160    /// blindly replayed — recover via `send_last_result`).
161    pub auto_reconnect: Option<bool>,
162
163    /// Connection timeout (ms).
164    pub connection_timeout_ms: Option<u32>,
165    /// Application-response timeout (ms).
166    pub response_timeout_ms: Option<u32>,
167    /// ACK timeout (ms).
168    pub ack_timeout_ms: Option<u32>,
169
170    /// After a transaction result, keep forwarding `S` receipt lines until this many ms
171    /// of silence. `0`/`None` = off. Set when ECR-printing is on.
172    pub receipt_drain_ms: Option<u32>,
173
174    /// Retransmission attempts on ACK timeout / NAK.
175    pub retry_count: Option<u32>,
176    /// Delay between retransmissions (ms).
177    pub retry_delay_ms: Option<u32>,
178
179    /// Verbose debug logging.
180    pub debug: Option<bool>,
181}
182
183/// POS terminal status code (response `s`). Meanings are defined for `0`..=`6`; any other
184/// value (including the `-1` the parser uses for "not reported") maps to "Unknown".
185pub type PosTerminalStatus = i32;
186
187/// Human-readable message for a [`PosTerminalStatus`].
188#[must_use]
189pub fn pos_terminal_status_message(status: PosTerminalStatus) -> &'static str {
190    match status {
191        0 => "Terminal not configured",
192        1 => "Terminal configured, no DLL",
193        2 => "Terminal operative (after a DLL)",
194        3 => "Terminal not aligned (first DLL requested)",
195        4 => "KMPB/KPOS key corrupted (first DLL requested)",
196        5 => "DLL solicited by GT pending",
197        6 => "Remote SW updated request pending",
198        _ => "Unknown",
199    }
200}
201
202/// Terminal status response (`s`).
203#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
204#[serde(rename_all = "camelCase")]
205pub struct PosStatusResponse {
206    /// Terminal identifier echoed back.
207    pub terminal_id: String,
208    /// Terminal date/time as an **ISO 8601** string (e.g. `"2026-07-10T14:30:00"`),
209    /// parsed by the `response` layer from the terminal's raw `DDMMYYhhmm`. A string
210    /// (not a native datetime) keeps the library dependency-free; the frontend maps it
211    /// with `new Date(...)`, preserving the RN API's date contract.
212    pub terminal_date_time: String,
213    /// Status code (meanings defined for `0`..=`6`; other values map to "Unknown"); see
214    /// [`pos_terminal_status_message`].
215    pub status: PosTerminalStatus,
216    /// Firmware/software release string.
217    pub software_release: String,
218}
219
220// ---------------------------------------------------------------------------
221// Requests
222// ---------------------------------------------------------------------------
223
224/// Payment / extended-payment / pre-auth request.
225#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
226#[serde(rename_all = "camelCase")]
227pub struct PaymentRequest {
228    /// Amount in cents.
229    pub amount_cents: i64,
230    /// Overrides the configured cash-register id when set.
231    pub cash_register_id: Option<String>,
232    /// Requested card handling.
233    pub payment_type: Option<PaymentCardType>,
234    /// Start with the card already inserted in the terminal.
235    pub card_already_present: Option<bool>,
236    /// Text printed at the end of the receipt (max 128 chars).
237    pub receipt_text: Option<String>,
238    /// Attach tokenization additional data (`U`) to this transaction.
239    pub tokenization: Option<TokenizationRequest>,
240}
241
242/// Reversal request (`S`).
243#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
244#[serde(rename_all = "camelCase")]
245pub struct ReversalRequest {
246    /// Overrides the configured cash-register id when set.
247    pub cash_register_id: Option<String>,
248    /// STAN of the transaction to reverse. When omitted (`None`), the client uses
249    /// `"000000"`, which reverses the last payment with no STAN check.
250    pub stan: Option<String>,
251}
252
253/// Pre-auth request (`p`).
254#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
255#[serde(rename_all = "camelCase")]
256pub struct PreAuthRequest {
257    /// Amount in cents.
258    pub amount_cents: i64,
259    /// Overrides the configured cash-register id when set.
260    pub cash_register_id: Option<String>,
261    /// Requested card handling.
262    pub payment_type: Option<PaymentCardType>,
263    /// Start with the card already inserted.
264    pub card_already_present: Option<bool>,
265    /// Receipt text.
266    pub receipt_text: Option<String>,
267    /// Attach tokenization additional data (`U`).
268    pub tokenization: Option<TokenizationRequest>,
269}
270
271/// Incremental pre-auth request (`i`).
272#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
273#[serde(rename_all = "camelCase")]
274pub struct IncrementalAuthRequest {
275    /// Amount in cents.
276    pub amount_cents: i64,
277    /// Unique pre-authorization code from the original pre-auth response.
278    pub original_pre_auth_code: String,
279    /// Overrides the configured cash-register id when set.
280    pub cash_register_id: Option<String>,
281    /// Receipt text.
282    pub receipt_text: Option<String>,
283}
284
285/// Pre-auth closure request (`c`).
286#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
287#[serde(rename_all = "camelCase")]
288pub struct PreAuthClosureRequest {
289    /// Amount in cents.
290    pub amount_cents: i64,
291    /// Unique pre-authorization code from the original pre-auth response.
292    pub original_pre_auth_code: String,
293    /// Overrides the configured cash-register id when set.
294    pub cash_register_id: Option<String>,
295    /// Receipt text.
296    pub receipt_text: Option<String>,
297}
298
299/// Card-verification request (`H`).
300#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
301#[serde(rename_all = "camelCase")]
302pub struct CardVerificationRequest {
303    /// Overrides the configured cash-register id when set.
304    pub cash_register_id: Option<String>,
305    /// Requested card handling.
306    pub payment_type: Option<PaymentCardType>,
307    /// Attach tokenization additional data (`U`).
308    pub tokenization: Option<TokenizationRequest>,
309}
310
311// ---------------------------------------------------------------------------
312// Results
313// ---------------------------------------------------------------------------
314
315/// DCC / currency-exchange block (only meaningful when `applied == true`).
316#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
317#[serde(rename_all = "camelCase")]
318pub struct CurrencyExchange {
319    /// Whether DCC was applied.
320    pub applied: bool,
321    /// Exchange rate.
322    pub rate: Option<f64>,
323    /// Currency code.
324    pub currency_code: Option<String>,
325    /// Converted amount in cents.
326    pub amount_cents: Option<i64>,
327    /// Decimal precision.
328    pub precision: Option<i32>,
329}
330
331/// Payment / extended-payment / closure result.
332#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
333#[serde(rename_all = "camelCase")]
334pub struct PaymentResult {
335    /// Normalized outcome.
336    pub outcome: TransactionOutcome,
337    /// Raw result code (`"00"`/`"01"`/`"05"`/`"09"`).
338    pub result_code: String,
339    /// Masked PAN.
340    pub pan: Option<String>,
341    /// Entry mode.
342    pub entry_mode: Option<TransactionEntryMode>,
343    /// Authorization code.
344    pub auth_code: Option<String>,
345    /// Raw host date/time (`DDDHHMM` as received).
346    pub host_date_time: Option<String>,
347    /// Card type.
348    pub card_type: Option<CardType>,
349    /// Acquirer id.
350    pub acquirer_id: Option<String>,
351    /// STAN.
352    pub stan: Option<String>,
353    /// Online id.
354    pub online_id: Option<String>,
355    /// Error description (when declined).
356    pub error_description: Option<String>,
357    /// DCC block.
358    pub currency_exchange: Option<CurrencyExchange>,
359}
360
361/// Reversal result.
362#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
363#[serde(rename_all = "camelCase")]
364pub struct ReversalResult {
365    /// Normalized outcome.
366    pub outcome: TransactionOutcome,
367    /// Raw result code.
368    pub result_code: String,
369    /// Masked PAN.
370    pub pan: Option<String>,
371    /// Entry mode.
372    pub entry_mode: Option<TransactionEntryMode>,
373    /// Raw host date/time.
374    pub host_date_time: Option<String>,
375    /// Card type.
376    pub card_type: Option<CardType>,
377    /// Acquirer id.
378    pub acquirer_id: Option<String>,
379    /// STAN.
380    pub stan: Option<String>,
381    /// Online id.
382    pub online_id: Option<String>,
383    /// Action code.
384    pub action_code: Option<String>,
385    /// Error description.
386    pub error_description: Option<String>,
387}
388
389/// Pre-auth / incremental result.
390#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
391#[serde(rename_all = "camelCase")]
392pub struct PreAuthResult {
393    /// Normalized outcome.
394    pub outcome: TransactionOutcome,
395    /// Raw result code.
396    pub result_code: String,
397    /// Masked PAN.
398    pub pan: Option<String>,
399    /// Entry mode.
400    pub entry_mode: Option<TransactionEntryMode>,
401    /// Authorization code.
402    pub auth_code: Option<String>,
403    /// Pre-authorized amount in cents.
404    pub pre_authorized_amount_cents: Option<i64>,
405    /// Pre-auth code (for follow-up incremental/closure).
406    pub pre_auth_code: Option<String>,
407    /// Action code.
408    pub action_code: Option<String>,
409    /// Raw host date/time.
410    pub host_date_time: Option<String>,
411    /// Card type.
412    pub card_type: Option<CardType>,
413    /// Acquirer id.
414    pub acquirer_id: Option<String>,
415    /// STAN.
416    pub stan: Option<String>,
417    /// Online id.
418    pub online_id: Option<String>,
419    /// Error description.
420    pub error_description: Option<String>,
421}
422
423/// Card-verification result.
424#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
425#[serde(rename_all = "camelCase")]
426pub struct CardVerificationResult {
427    /// Normalized outcome.
428    pub outcome: TransactionOutcome,
429    /// Raw result code.
430    pub result_code: String,
431    /// Masked PAN.
432    pub pan: Option<String>,
433    /// Entry mode.
434    pub entry_mode: Option<TransactionEntryMode>,
435    /// Authorization code.
436    pub auth_code: Option<String>,
437    /// Raw host date/time.
438    pub host_date_time: Option<String>,
439    /// Card type.
440    pub card_type: Option<CardType>,
441    /// Acquirer id.
442    pub acquirer_id: Option<String>,
443    /// STAN.
444    pub stan: Option<String>,
445    /// Online id.
446    pub online_id: Option<String>,
447    /// Action code.
448    pub action_code: Option<String>,
449    /// Error description.
450    pub error_description: Option<String>,
451}
452
453/// Totals result (`T`).
454#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
455#[serde(rename_all = "camelCase")]
456pub struct TotalsResult {
457    /// Normalized outcome.
458    pub outcome: TransactionOutcome,
459    /// Raw result code.
460    pub result_code: String,
461    /// POS total in cents.
462    pub pos_total_cents: i64,
463}
464
465/// Close-session result (`C`).
466#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
467#[serde(rename_all = "camelCase")]
468pub struct CloseSessionResult {
469    /// Normalized outcome.
470    pub outcome: TransactionOutcome,
471    /// Raw result code.
472    pub result_code: String,
473    /// POS total in cents.
474    pub pos_total_cents: Option<i64>,
475    /// Host total in cents.
476    pub host_total_cents: Option<i64>,
477    /// Action code.
478    pub action_code: Option<String>,
479    /// Error description.
480    pub error_description: Option<String>,
481}
482
483/// VAS result (`K`).
484#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
485#[serde(rename_all = "camelCase")]
486pub struct VasResult {
487    /// Response id (`RESPID`; `"0"` = OK).
488    pub response_id: String,
489    /// Response message (`RESPMSG`).
490    pub response_message: String,
491    /// Order id when present.
492    pub order_id: Option<String>,
493    /// Full concatenated XML response.
494    pub raw_xml: String,
495}
496
497// ---------------------------------------------------------------------------
498// Events
499// ---------------------------------------------------------------------------
500
501/// Progress-update message shown on the ECR display during a procedure (`SOH` frame).
502#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
503#[serde(rename_all = "camelCase")]
504pub struct ProgressEvent {
505    /// Display message.
506    pub message: String,
507}
508
509/// A single receipt line streamed by the terminal (`S` message) when ECR printing is on.
510#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
511#[serde(rename_all = "camelCase")]
512pub struct ReceiptLine {
513    /// Receipt line text.
514    pub text: String,
515}
516
517#[cfg(test)]
518mod tests {
519    use super::*;
520
521    #[test]
522    fn enums_serialize_to_ts_string_unions() {
523        assert_eq!(
524            serde_json::to_string(&ConnectionState::Disconnected).unwrap(),
525            "\"disconnected\""
526        );
527        assert_eq!(
528            serde_json::to_string(&TransactionOutcome::CardNotPresent).unwrap(),
529            "\"cardNotPresent\""
530        );
531        assert_eq!(
532            serde_json::to_string(&TransactionOutcome::UnknownTag).unwrap(),
533            "\"unknownTag\""
534        );
535        assert_eq!(
536            serde_json::to_string(&CardType::Debit).unwrap(),
537            "\"debit\""
538        );
539        assert_eq!(
540            serde_json::to_string(&TransactionEntryMode::ClessMag).unwrap(),
541            "\"clessMag\""
542        );
543        assert_eq!(
544            serde_json::to_string(&PaymentCardType::Auto).unwrap(),
545            "\"auto\""
546        );
547        assert_eq!(
548            serde_json::to_string(&TokenizationService::UnscheduledOrOneClick).unwrap(),
549            "\"unscheduledOrOneClick\""
550        );
551    }
552
553    #[test]
554    fn payment_card_type_digit_mapping() {
555        assert_eq!(PaymentCardType::Auto.as_digit(), '0');
556        assert_eq!(PaymentCardType::Debit.as_digit(), '1');
557        assert_eq!(PaymentCardType::Credit.as_digit(), '2');
558        assert_eq!(PaymentCardType::Other.as_digit(), '3');
559    }
560
561    #[test]
562    fn request_deserializes_camel_case_and_defaults_missing_options() {
563        // Only the required field present -> optionals default to None.
564        let r: PaymentRequest = serde_json::from_str(r#"{"amountCents":650}"#).unwrap();
565        assert_eq!(r.amount_cents, 650);
566        assert_eq!(r.payment_type, None);
567        assert_eq!(r.card_already_present, None);
568
569        let full: PaymentRequest = serde_json::from_str(
570            r#"{"amountCents":100,"paymentType":"credit","cardAlreadyPresent":true,"receiptText":"x"}"#,
571        )
572        .unwrap();
573        assert_eq!(full.payment_type, Some(PaymentCardType::Credit));
574        assert_eq!(full.card_already_present, Some(true));
575        assert_eq!(full.receipt_text.as_deref(), Some("x"));
576    }
577
578    #[test]
579    fn tokenization_request_round_trip() {
580        let t = TokenizationRequest {
581            service: TokenizationService::Recurring,
582            contract_code: "ABC".into(),
583        };
584        let json = serde_json::to_string(&t).unwrap();
585        assert_eq!(json, r#"{"service":"recurring","contractCode":"ABC"}"#);
586        assert_eq!(
587            serde_json::from_str::<TokenizationRequest>(&json).unwrap(),
588            t
589        );
590        assert!(t.service.is_recurring());
591    }
592
593    #[test]
594    fn status_message_lookup() {
595        assert_eq!(
596            pos_terminal_status_message(2),
597            "Terminal operative (after a DLL)"
598        );
599        assert_eq!(pos_terminal_status_message(-1), "Unknown");
600        assert_eq!(pos_terminal_status_message(99), "Unknown");
601    }
602
603    #[test]
604    fn config_round_trip_camel_case() {
605        let json =
606            r#"{"host":"10.0.0.5","terminalId":"12345678","cashRegisterId":"1","lrcMode":"stx"}"#;
607        let cfg: Ecr17Config = serde_json::from_str(json).unwrap();
608        assert_eq!(cfg.host, "10.0.0.5");
609        assert_eq!(cfg.lrc_mode, Some(LrcMode::Stx));
610        assert_eq!(cfg.port, None);
611    }
612}