Skip to main content

ecr17_protocol/
session.rs

1//! [`Ecr17Session`] — drives one ECR17 request/response exchange over a [`Transport`].
2//!
3//! It frames the request, runs the physical `ACK`/`NAK` handshake with retransmission,
4//! waits for the application response while forwarding progress (`SOH`) and receipt (`S`)
5//! messages, and `ACK`/`NAK`s incoming frames per their LRC validity. One exchange runs at
6//! a time (ECR17 is one transaction per terminal). Port of the reference C++ `Ecr17Session`.
7//!
8//! 💰 The session errors on a mid-exchange drop (propagating the transport's error, e.g.
9//! [`Ecr17Error::Disconnected`] or [`Ecr17Error::Transport`]) and resets its
10//! per-transaction state at the start of every exchange, so it is reusable across
11//! reconnects. It never blindly re-sends: the money-safe retry decision lives in
12//! [`crate::retry`] and is applied by the client, and a lost response is recovered via
13//! `send_last_result` (`G`), never by retransmitting a financial request.
14
15use std::collections::VecDeque;
16use std::time::{Duration, Instant};
17
18use crate::codec::{DecodedPacket, PacketCodec, PacketType, ACK, ETX, NAK, SOH, STX};
19use crate::error::{Ecr17Error, Result};
20use crate::lrc::LrcMode;
21use crate::transport::Transport;
22
23/// Session timing/retry configuration.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub struct SessionConfig {
26    /// LRC framing mode.
27    pub lrc_mode: LrcMode,
28    /// Wait for the physical `ACK`/`NAK` (ms).
29    pub ack_timeout_ms: u64,
30    /// Wait for the application response (ms).
31    pub response_timeout_ms: u64,
32    /// Retransmissions on `NAK`/timeout (spec: up to 3).
33    pub retry_count: u32,
34    /// Delay between retransmissions (ms).
35    pub retry_delay_ms: u64,
36    /// After the result, keep forwarding `S` receipt lines until this many ms of silence
37    /// (`0` = off).
38    pub receipt_drain_ms: u64,
39}
40
41impl Default for SessionConfig {
42    fn default() -> Self {
43        Self {
44            lrc_mode: LrcMode::Std,
45            ack_timeout_ms: 2000,
46            response_timeout_ms: 60_000,
47            retry_count: 3,
48            retry_delay_ms: 200,
49            receipt_drain_ms: 0,
50        }
51    }
52}
53
54type EventCallback = Box<dyn Fn(String) + Send + 'static>;
55
56/// Outcome of waiting for the next frame.
57enum WaitOutcome {
58    Frame(DecodedPacket),
59    Timeout,
60    /// The transport errored (a disconnect OR any `Ecr17Error::Transport { .. }`); carries
61    /// the underlying error so transport-level details are preserved, not flattened.
62    TransportError(Ecr17Error),
63}
64
65/// Drives ECR17 exchanges over a [`Transport`] `T`.
66pub struct Ecr17Session<T: Transport> {
67    transport: T,
68    config: SessionConfig,
69    codec: PacketCodec,
70    /// Inbound byte buffer. A `VecDeque` so resyncing (dropping junk from the front) and
71    /// draining a frame are O(1)/O(frame) rather than O(n) shifts of a `Vec`.
72    rx_buffer: VecDeque<u8>,
73    /// Holds an application response that arrived during the ACK handshake (some terminals
74    /// send the result before/without a physical ACK). Consumed by `wait_for_result` so a
75    /// completed transaction's result is never dropped.
76    pending_result: Option<DecodedPacket>,
77    on_progress: Option<EventCallback>,
78    on_receipt_line: Option<EventCallback>,
79}
80
81impl<T: Transport> std::fmt::Debug for Ecr17Session<T> {
82    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83        f.debug_struct("Ecr17Session")
84            .field("config", &self.config)
85            .field("rx_buffered", &self.rx_buffer.len())
86            .field("has_pending_result", &self.pending_result.is_some())
87            .finish_non_exhaustive()
88    }
89}
90
91impl<T: Transport> Ecr17Session<T> {
92    /// Creates a session over `transport` with `config`.
93    pub fn new(transport: T, config: SessionConfig) -> Self {
94        let codec = PacketCodec::new(config.lrc_mode);
95        Self {
96            transport,
97            config,
98            codec,
99            rx_buffer: VecDeque::new(),
100            pending_result: None,
101            on_progress: None,
102            on_receipt_line: None,
103        }
104    }
105
106    /// Sets the progress-update callback (`SOH` frames during a procedure).
107    pub fn set_on_progress(&mut self, cb: impl Fn(String) + Send + 'static) {
108        self.on_progress = Some(Box::new(cb));
109    }
110
111    /// Sets the receipt-line callback (`S` messages when ECR printing is on).
112    pub fn set_on_receipt_line(&mut self, cb: impl Fn(String) + Send + 'static) {
113        self.on_receipt_line = Some(Box::new(cb));
114    }
115
116    /// Borrows the underlying transport for read-only inspection (state, test counters).
117    /// To reconnect it, use [`transport_mut`](Self::transport_mut) or [`connect`](Self::connect).
118    pub fn transport(&self) -> &T {
119        &self.transport
120    }
121
122    /// Mutably borrows the underlying transport (e.g. to reconnect it).
123    pub fn transport_mut(&mut self) -> &mut T {
124        &mut self.transport
125    }
126
127    /// Opens the transport connection.
128    pub async fn connect(&mut self) -> Result<()> {
129        self.transport.connect().await
130    }
131
132    /// Closes the transport connection.
133    pub async fn disconnect(&mut self) {
134        self.transport.disconnect().await;
135    }
136
137    /// Whether the transport currently believes it is connected.
138    pub fn is_connected(&self) -> bool {
139        self.transport.is_connected()
140    }
141
142    /// Sends `request_payload` (the application message, without STX/ETX) and returns the
143    /// decoded application result. Errors on retransmission exhaustion, ACK/response
144    /// timeout, or transport disconnect.
145    pub async fn exchange(&mut self, request_payload: &str) -> Result<DecodedPacket> {
146        self.reset_for_new_transaction();
147        self.ack_handshake(request_payload).await?;
148        self.wait_for_result().await
149    }
150
151    /// Like [`exchange`](Self::exchange) but sends an extra additional-data message (`U`,
152    /// tokenization) after the main request is ACKed, before the result:
153    /// `request(flag=1) -> ACK -> 'U' -> ACK -> result`.
154    pub async fn exchange_with_additional_data(
155        &mut self,
156        request_payload: &str,
157        additional_payload: &str,
158    ) -> Result<DecodedPacket> {
159        self.reset_for_new_transaction();
160        self.ack_handshake(request_payload).await?;
161        // Only send the additional-data ('U') message if the terminal is still waiting for
162        // it. If the result already arrived during the main handshake — either decoded
163        // (`pending_result`) or still buffered right after the ACK in the same chunk
164        // (`rx_buffer` non-empty) — the transaction has moved past the 'U' step, so sending
165        // it now would be moot/late; go straight to reading the result.
166        if self.pending_result.is_none() && self.rx_buffer.is_empty() {
167            self.ack_handshake(additional_payload).await?;
168        }
169        self.wait_for_result().await
170    }
171
172    /// For commands whose only reply is the physical `ACK` (e.g. enable/disable ECR
173    /// printing `E`): performs the ACK handshake with retransmission and returns once ACK
174    /// is received; does NOT wait for an application response.
175    pub async fn send_ack_only(&mut self, request_payload: &str) -> Result<()> {
176        self.reset_for_new_transaction();
177        self.ack_handshake(request_payload).await
178    }
179
180    // --- internals ---
181
182    /// Clears stale RX bytes and any stashed result so the session is reusable across
183    /// reconnects (a new transaction starts from a clean state).
184    fn reset_for_new_transaction(&mut self) {
185        self.rx_buffer.clear();
186        self.pending_result = None;
187    }
188
189    /// Sends a request and completes the physical ACK handshake (with retransmission).
190    async fn ack_handshake(&mut self, request_payload: &str) -> Result<()> {
191        let frame = self.codec.encode_application(request_payload.as_bytes());
192        self.transport.send(&frame).await?;
193        let ack_timeout = Duration::from_millis(self.config.ack_timeout_ms);
194        let mut attempts: u32 = 1;
195        let mut deadline = Instant::now() + ack_timeout;
196
197        loop {
198            let remaining = deadline.saturating_duration_since(Instant::now());
199            if remaining.is_zero() {
200                if attempts > self.config.retry_count {
201                    return Err(Ecr17Error::AckTimeout { attempts });
202                }
203                self.retransmit(&frame, &mut attempts, &mut deadline, ack_timeout)
204                    .await?;
205                continue;
206            }
207            match self.wait_for_frame(remaining).await {
208                WaitOutcome::Frame(pkt) => match pkt.packet_type {
209                    PacketType::Ack => return Ok(()),
210                    PacketType::Nak => {
211                        if attempts > self.config.retry_count {
212                            return Err(Ecr17Error::Nak { attempts });
213                        }
214                        self.retransmit(&frame, &mut attempts, &mut deadline, ack_timeout)
215                            .await?;
216                    }
217                    PacketType::Application => {
218                        // Some terminals send the result before/without a physical ACK.
219                        self.pending_result = Some(pkt);
220                        return Ok(());
221                    }
222                    // Ignore progress / unknown frames that may precede the ACK.
223                    _ => {}
224                },
225                WaitOutcome::TransportError(e) => return Err(e),
226                // Timed out waiting; loop re-checks the deadline and retransmits.
227                WaitOutcome::Timeout => {}
228            }
229        }
230    }
231
232    async fn retransmit(
233        &mut self,
234        frame: &[u8],
235        attempts: &mut u32,
236        deadline: &mut Instant,
237        ack_timeout: Duration,
238    ) -> Result<()> {
239        tokio::time::sleep(Duration::from_millis(self.config.retry_delay_ms)).await;
240        self.transport.send(frame).await?;
241        *attempts += 1;
242        *deadline = Instant::now() + ack_timeout;
243        Ok(())
244    }
245
246    /// Waits for the application result after the ACK handshake, forwarding progress and
247    /// receipt frames and NAKing invalid-LRC frames.
248    async fn wait_for_result(&mut self) -> Result<DecodedPacket> {
249        let deadline = Instant::now() + Duration::from_millis(self.config.response_timeout_ms);
250        loop {
251            let pkt = if let Some(p) = self.pending_result.take() {
252                p
253            } else {
254                let remaining = deadline.saturating_duration_since(Instant::now());
255                if remaining.is_zero() {
256                    return Err(Ecr17Error::ResponseTimeout);
257                }
258                match self.wait_for_frame(remaining).await {
259                    WaitOutcome::Frame(p) => p,
260                    WaitOutcome::TransportError(e) => return Err(e),
261                    WaitOutcome::Timeout => continue,
262                }
263            };
264            match pkt.packet_type {
265                PacketType::Progress => self.emit_progress(&pkt.payload),
266                PacketType::Application => {
267                    if !pkt.valid_lrc {
268                        self.send_control(NAK).await;
269                    } else {
270                        self.send_control(ACK).await;
271                        if Self::is_receipt(&pkt.payload) {
272                            self.emit_receipt(&pkt.payload);
273                        } else {
274                            self.drain_receipts().await;
275                            return Ok(pkt);
276                        }
277                    }
278                }
279                // Stray confirmation frames are ignored.
280                PacketType::Ack | PacketType::Nak => {}
281                PacketType::Unknown => self.send_control(NAK).await,
282            }
283        }
284    }
285
286    /// After the result, keep forwarding `S` receipt lines that arrive until the terminal
287    /// is quiet for `receipt_drain_ms` (`0` = off).
288    async fn drain_receipts(&mut self) {
289        if self.config.receipt_drain_ms == 0 {
290            return;
291        }
292        let drain = Duration::from_millis(self.config.receipt_drain_ms);
293        loop {
294            match self.wait_for_frame(drain).await {
295                WaitOutcome::Frame(pkt) => match pkt.packet_type {
296                    PacketType::Application => {
297                        if pkt.valid_lrc {
298                            self.send_control(ACK).await;
299                            if Self::is_receipt(&pkt.payload) {
300                                self.emit_receipt(&pkt.payload);
301                            }
302                        } else {
303                            self.send_control(NAK).await;
304                        }
305                    }
306                    PacketType::Progress => self.emit_progress(&pkt.payload),
307                    _ => {}
308                },
309                // Idle (no more receipts) or dropped: stop draining.
310                WaitOutcome::Timeout | WaitOutcome::TransportError(_) => return,
311            }
312        }
313    }
314
315    /// Extracts one complete frame from the front of `rx_buffer`, dropping leading junk
316    /// bytes to resynchronize. `None` if no complete frame is available yet.
317    fn extract_frame(&mut self) -> Option<Vec<u8>> {
318        while let Some(&first) = self.rx_buffer.front() {
319            if first == ACK || first == NAK {
320                if self.rx_buffer.len() < 3 {
321                    return None; // wait for ETX + LRC
322                }
323                // A well-formed control frame is `ctrl + ETX + LRC`. Validate the FULL
324                // frame (ETX and the control-frame LRC) before draining it: a stray/
325                // desynced or corrupted sequence that merely starts with 0x06/0x15 must not
326                // complete a handshake. If it doesn't validate, drop the lead byte and
327                // resync. (The session is the gatekeeper here; `codec::decode` recognizes
328                // control frames by lead byte only — see docs/LESSON.md.)
329                if self.rx_buffer[1] != ETX
330                    || self.rx_buffer[2] != self.codec.lrc_mode().compute(&[first])
331                {
332                    self.rx_buffer.pop_front();
333                    continue;
334                }
335                return Some(self.rx_buffer.drain(0..3).collect());
336            }
337            if first == STX {
338                let Some(etx) = self.rx_buffer.iter().position(|&b| b == ETX) else {
339                    return None; // wait for ETX
340                };
341                if etx + 1 >= self.rx_buffer.len() {
342                    return None; // wait for the trailing LRC
343                }
344                return Some(self.rx_buffer.drain(0..=etx + 1).collect());
345            }
346            if first == SOH {
347                let Some(eot) = self.rx_buffer.iter().position(|&b| b == crate::codec::EOT) else {
348                    return None; // wait for EOT
349                };
350                return Some(self.rx_buffer.drain(0..=eot).collect());
351            }
352            // Unrecognized lead byte: drop it and resynchronize.
353            self.rx_buffer.pop_front();
354        }
355        None
356    }
357
358    /// Waits up to `timeout` for the next decodable frame, buffering inbound bytes.
359    async fn wait_for_frame(&mut self, timeout: Duration) -> WaitOutcome {
360        let deadline = Instant::now() + timeout;
361        loop {
362            if let Some(frame) = self.extract_frame() {
363                return WaitOutcome::Frame(self.codec.decode(&frame));
364            }
365            let remaining = deadline.saturating_duration_since(Instant::now());
366            if remaining.is_zero() {
367                return WaitOutcome::Timeout;
368            }
369            match tokio::time::timeout(remaining, self.transport.recv()).await {
370                Ok(Ok(bytes)) => self.rx_buffer.extend(bytes),
371                Ok(Err(err)) => return WaitOutcome::TransportError(err),
372                Err(_elapsed) => return WaitOutcome::Timeout,
373            }
374        }
375    }
376
377    async fn send_control(&mut self, control: u8) {
378        let frame = self.codec.encode_control(control);
379        // Best effort: an ACK/NAK is a courtesy confirmation. If the send fails the
380        // transport is down; a later read in the same exchange surfaces it, and for the
381        // final result-ACK we deliberately keep the already-obtained result rather than
382        // failing a completed transaction on a lost ACK.
383        let _ = self.transport.send(&frame).await;
384    }
385
386    fn emit_progress(&self, payload: &[u8]) {
387        if let Some(cb) = &self.on_progress {
388            cb(String::from_utf8_lossy(payload).into_owned());
389        }
390    }
391
392    fn emit_receipt(&self, payload: &[u8]) {
393        if let Some(cb) = &self.on_receipt_line {
394            cb(String::from_utf8_lossy(payload).into_owned());
395        }
396    }
397
398    /// A send-ticket ('S') message has code `'S'` at position 10 (0-indexed 9).
399    fn is_receipt(payload: &[u8]) -> bool {
400        payload.get(9) == Some(&b'S')
401    }
402}
403
404#[cfg(test)]
405mod tests {
406    use super::*;
407    use crate::transport::FakeTransport;
408    use std::sync::{Arc, Mutex};
409
410    fn fast_config() -> SessionConfig {
411        SessionConfig {
412            lrc_mode: LrcMode::Std,
413            ack_timeout_ms: 40,
414            response_timeout_ms: 40,
415            retry_count: 2,
416            retry_delay_ms: 1,
417            receipt_drain_ms: 0,
418        }
419    }
420
421    fn codec() -> PacketCodec {
422        PacketCodec::new(LrcMode::Std)
423    }
424
425    fn concat(mut a: Vec<u8>, b: Vec<u8>) -> Vec<u8> {
426        a.extend(b);
427        a
428    }
429
430    fn progress_frame(msg20: &str) -> Vec<u8> {
431        let mut f = vec![SOH];
432        f.extend_from_slice(msg20.as_bytes());
433        f.push(crate::codec::EOT);
434        f
435    }
436
437    const RESULT: &str = "123456780E0000DATA"; // code 'E' at pos 10 -> result
438    const RECEIPT: &str = "123456780SLINE 1"; // code 'S' at pos 10 -> receipt
439
440    fn sent_any(t: &FakeTransport, lead: u8) -> bool {
441        t.sent_frames().iter().any(|f| f.first() == Some(&lead))
442    }
443
444    #[tokio::test]
445    async fn happy_path_returns_result_and_acks() {
446        let c = codec();
447        let mut t = FakeTransport::new();
448        t.enqueue_response(concat(
449            c.encode_control(ACK),
450            c.encode_application(RESULT.as_bytes()),
451        ));
452        let mut session = Ecr17Session::new(t, fast_config());
453
454        let result = session.exchange("123456780P...").await.unwrap();
455        assert_eq!(result.packet_type, PacketType::Application);
456        assert!(result.valid_lrc);
457        assert_eq!(result.payload, RESULT.as_bytes());
458        assert_eq!(session.transport().application_request_count(), 1);
459        assert!(sent_any(session.transport(), ACK));
460    }
461
462    // A stray 0x06 byte (not followed by ETX) must be resynced past, not mistaken for an
463    // ACK that prematurely completes the handshake.
464    #[tokio::test]
465    async fn stray_ack_byte_is_resynced_not_a_false_ack() {
466        let c = codec();
467        let mut response = vec![ACK, 0xFF]; // stray lead byte, NOT ETX-framed
468        response.extend(c.encode_control(ACK)); // the real ACK frame
469        response.extend(c.encode_application(RESULT.as_bytes()));
470        let mut t = FakeTransport::new();
471        t.enqueue_response(response);
472        let mut session = Ecr17Session::new(t, fast_config());
473
474        let result = session.exchange("123456780P...").await.unwrap();
475        assert_eq!(result.payload, RESULT.as_bytes());
476    }
477
478    // A control byte sequence with a valid ETX but a WRONG LRC is corrupt/desynced — it
479    // must be resynced past, not accepted as an ACK.
480    #[tokio::test]
481    async fn control_frame_with_bad_lrc_is_resynced() {
482        let c = codec();
483        let mut response = vec![ACK, ETX, 0x00]; // ETX ok, LRC wrong
484        response.extend(c.encode_control(ACK)); // the real, well-formed ACK
485        response.extend(c.encode_application(RESULT.as_bytes()));
486        let mut t = FakeTransport::new();
487        t.enqueue_response(response);
488        let mut session = Ecr17Session::new(t, fast_config());
489
490        let result = session.exchange("123456780P...").await.unwrap();
491        assert_eq!(result.payload, RESULT.as_bytes());
492    }
493
494    #[tokio::test]
495    async fn nak_triggers_retransmit_then_succeeds() {
496        let c = codec();
497        let mut t = FakeTransport::new();
498        t.enqueue_response(c.encode_control(NAK)); // reply to attempt 1
499        t.enqueue_response(concat(
500            c.encode_control(ACK),
501            c.encode_application(RESULT.as_bytes()),
502        ));
503        let mut session = Ecr17Session::new(t, fast_config());
504
505        let result = session.exchange("123456780P...").await.unwrap();
506        assert_eq!(result.payload, RESULT.as_bytes());
507        assert_eq!(session.transport().application_request_count(), 2); // initial + 1 retransmit
508    }
509
510    #[tokio::test]
511    async fn ack_timeout_exhausts_retries_then_errors() {
512        let mut session = Ecr17Session::new(FakeTransport::new(), fast_config());
513        let err = session.exchange("123456780P...").await.unwrap_err();
514        assert!(matches!(err, Ecr17Error::AckTimeout { attempts: 3 }));
515        assert_eq!(session.transport().application_request_count(), 3); // initial + retry_count
516    }
517
518    #[tokio::test]
519    async fn bad_lrc_response_sends_nak() {
520        let c = codec();
521        let mut t = FakeTransport::new();
522        let mut bad = c.encode_application(RESULT.as_bytes());
523        *bad.last_mut().unwrap() ^= 0xFF; // corrupt LRC
524        t.enqueue_response(concat(c.encode_control(ACK), bad));
525        let mut session = Ecr17Session::new(t, fast_config());
526
527        let err = session.exchange("123456780P...").await.unwrap_err();
528        assert!(matches!(err, Ecr17Error::ResponseTimeout));
529        assert!(sent_any(session.transport(), NAK));
530    }
531
532    #[tokio::test]
533    async fn progress_messages_forwarded() {
534        let c = codec();
535        let mut response = c.encode_control(ACK);
536        response.extend(progress_frame("ATTENDERE PREGO     "));
537        response.extend(c.encode_application(RESULT.as_bytes()));
538        let mut t = FakeTransport::new();
539        t.enqueue_response(response);
540
541        let progress = Arc::new(Mutex::new(Vec::<String>::new()));
542        let sink = Arc::clone(&progress);
543        let mut session = Ecr17Session::new(t, fast_config());
544        session.set_on_progress(move |m| sink.lock().unwrap().push(m));
545
546        let result = session.exchange("123456780P...").await.unwrap();
547        assert_eq!(result.payload, RESULT.as_bytes());
548        assert_eq!(
549            *progress.lock().unwrap(),
550            vec!["ATTENDERE PREGO     ".to_string()]
551        );
552    }
553
554    #[tokio::test]
555    async fn receipt_lines_forwarded_before_result() {
556        let c = codec();
557        let mut response = c.encode_control(ACK);
558        response.extend(c.encode_application(RECEIPT.as_bytes()));
559        response.extend(c.encode_application(RESULT.as_bytes()));
560        let mut t = FakeTransport::new();
561        t.enqueue_response(response);
562
563        let receipts = Arc::new(Mutex::new(Vec::<String>::new()));
564        let sink = Arc::clone(&receipts);
565        let mut session = Ecr17Session::new(t, fast_config());
566        session.set_on_receipt_line(move |l| sink.lock().unwrap().push(l));
567
568        let result = session.exchange("123456780P...").await.unwrap();
569        assert_eq!(result.payload, RESULT.as_bytes());
570        assert_eq!(*receipts.lock().unwrap(), vec![RECEIPT.to_string()]);
571    }
572
573    // Regression: some terminals send the RESULT before (or instead of) the physical ACK.
574    #[tokio::test]
575    async fn result_before_ack_is_not_lost() {
576        let c = codec();
577        let mut t = FakeTransport::new();
578        t.enqueue_response(c.encode_application(RESULT.as_bytes())); // result, no leading ACK
579        let mut session = Ecr17Session::new(t, fast_config());
580
581        let result = session.exchange("123456780P...").await.unwrap();
582        assert_eq!(result.payload, RESULT.as_bytes());
583        assert_eq!(session.transport().application_request_count(), 1); // no spurious retransmit
584        assert!(sent_any(session.transport(), ACK));
585    }
586
587    #[tokio::test]
588    async fn response_timeout_after_ack_errors() {
589        let c = codec();
590        let mut t = FakeTransport::new();
591        t.enqueue_response(c.encode_control(ACK)); // ACK only, no result
592        let mut session = Ecr17Session::new(t, fast_config());
593        assert!(matches!(
594            session.exchange("123456780P...").await,
595            Err(Ecr17Error::ResponseTimeout)
596        ));
597    }
598
599    #[tokio::test]
600    async fn disconnect_during_exchange_errors() {
601        let mut t = FakeTransport::new();
602        t.disconnect_on_next_request();
603        let mut session = Ecr17Session::new(t, fast_config());
604        assert!(matches!(
605            session.exchange("123456780P...").await,
606            Err(Ecr17Error::Disconnected)
607        ));
608    }
609
610    // 💰 The session must recover after a drop — no stale disconnected state.
611    #[tokio::test]
612    async fn recovers_and_succeeds_after_reconnect() {
613        let mut t = FakeTransport::new();
614        t.disconnect_on_next_request();
615        let mut session = Ecr17Session::new(t, fast_config());
616
617        assert!(matches!(
618            session.exchange("123456780P...").await,
619            Err(Ecr17Error::Disconnected)
620        ));
621
622        let c = codec();
623        session.transport_mut().rearm();
624        session.transport_mut().enqueue_response(concat(
625            c.encode_control(ACK),
626            c.encode_application(RESULT.as_bytes()),
627        ));
628
629        let result = session.exchange("123456780P...").await.unwrap();
630        assert_eq!(result.payload, RESULT.as_bytes());
631    }
632
633    #[tokio::test]
634    async fn send_ack_only_returns_on_ack() {
635        let c = codec();
636        let mut t = FakeTransport::new();
637        t.enqueue_response(c.encode_control(ACK));
638        let mut session = Ecr17Session::new(t, fast_config());
639        session.send_ack_only("123456780E1").await.unwrap();
640        assert_eq!(session.transport().application_request_count(), 1);
641    }
642
643    #[tokio::test]
644    async fn send_ack_only_retransmits_on_nak() {
645        let c = codec();
646        let mut t = FakeTransport::new();
647        t.enqueue_response(c.encode_control(NAK));
648        t.enqueue_response(c.encode_control(ACK));
649        let mut session = Ecr17Session::new(t, fast_config());
650        session.send_ack_only("123456780E0").await.unwrap();
651        assert_eq!(session.transport().application_request_count(), 2);
652    }
653
654    #[tokio::test]
655    async fn send_ack_only_times_out() {
656        let mut session = Ecr17Session::new(FakeTransport::new(), fast_config());
657        assert!(matches!(
658            session.send_ack_only("123456780E1").await,
659            Err(Ecr17Error::AckTimeout { .. })
660        ));
661    }
662
663    #[tokio::test]
664    async fn exchange_with_additional_data_sends_two_requests() {
665        let c = codec();
666        let mut t = FakeTransport::new();
667        t.enqueue_response(c.encode_control(ACK)); // ACK for the main 'P'
668        t.enqueue_response(concat(
669            c.encode_control(ACK),
670            c.encode_application(RESULT.as_bytes()),
671        ));
672        let mut session = Ecr17Session::new(t, fast_config());
673
674        let result = session
675            .exchange_with_additional_data("123456780P...", "123456780U...")
676            .await
677            .unwrap();
678        assert_eq!(result.payload, RESULT.as_bytes());
679        assert_eq!(session.transport().application_request_count(), 2); // P + U
680    }
681
682    // If the terminal returns ACK + result together for the main request (skipping the
683    // 'U' step), the additional-data message must NOT be sent — the transaction is done.
684    #[tokio::test]
685    async fn exchange_with_additional_data_skips_u_when_result_already_arrived() {
686        let c = codec();
687        let mut t = FakeTransport::new();
688        t.enqueue_response(concat(
689            c.encode_control(ACK),
690            c.encode_application(RESULT.as_bytes()),
691        ));
692        let mut session = Ecr17Session::new(t, fast_config());
693
694        let result = session
695            .exchange_with_additional_data("123456780P...", "123456780U...")
696            .await
697            .unwrap();
698        assert_eq!(result.payload, RESULT.as_bytes());
699        assert_eq!(session.transport().application_request_count(), 1); // only P, no U
700    }
701
702    #[tokio::test]
703    async fn receipt_drain_forwards_receipts_after_result() {
704        let c = codec();
705        let mut response = c.encode_control(ACK);
706        response.extend(c.encode_application(RESULT.as_bytes())); // result first
707        response.extend(c.encode_application(RECEIPT.as_bytes())); // then a receipt line
708        let mut t = FakeTransport::new();
709        t.enqueue_response(response);
710
711        let mut cfg = fast_config();
712        cfg.receipt_drain_ms = 30;
713        let receipts = Arc::new(Mutex::new(Vec::<String>::new()));
714        let sink = Arc::clone(&receipts);
715        let mut session = Ecr17Session::new(t, cfg);
716        session.set_on_receipt_line(move |l| sink.lock().unwrap().push(l));
717
718        let result = session.exchange("123456780P...").await.unwrap();
719        assert_eq!(result.payload, RESULT.as_bytes());
720        assert_eq!(*receipts.lock().unwrap(), vec![RECEIPT.to_string()]);
721    }
722}