1use std::sync::{Arc, Mutex};
11
12use crate::codec::DecodedPacket;
13use crate::error::Result;
14use crate::response;
15use crate::retry::should_retry_after_reconnect;
16use crate::session::{Ecr17Session, SessionConfig};
17use crate::transport::Transport;
18use crate::types::{
19 CardType, CardVerificationRequest, CardVerificationResult, CloseSessionResult, ConnectionState,
20 CurrencyExchange, Ecr17Config, IncrementalAuthRequest, PaymentRequest, PaymentResult,
21 PosStatusResponse, PreAuthClosureRequest, PreAuthRequest, PreAuthResult, ProgressEvent,
22 ReceiptLine, ReversalRequest, ReversalResult, TokenizationRequest, TotalsResult,
23 TransactionEntryMode, VasResult,
24};
25
26type SharedCb<T> = Arc<Mutex<Option<Arc<dyn Fn(T) + Send + Sync + 'static>>>>;
30
31pub struct Ecr17Client<T: Transport> {
33 config: Ecr17Config,
34 session: Ecr17Session<T>,
35 on_progress: SharedCb<ProgressEvent>,
36 on_receipt_line: SharedCb<ReceiptLine>,
37 on_connection_state_change: SharedCb<ConnectionState>,
38}
39
40impl<T: Transport> std::fmt::Debug for Ecr17Client<T> {
41 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42 f.debug_struct("Ecr17Client")
43 .field("config", &self.config)
44 .finish_non_exhaustive()
45 }
46}
47
48impl<T: Transport> Ecr17Client<T> {
49 pub fn new(transport: T, config: Ecr17Config) -> Self {
52 let mut session = Ecr17Session::new(transport, session_config(&config));
53
54 let on_progress: SharedCb<ProgressEvent> = Arc::new(Mutex::new(None));
55 let on_receipt_line: SharedCb<ReceiptLine> = Arc::new(Mutex::new(None));
56
57 let p = Arc::clone(&on_progress);
58 session.set_on_progress(move |message| {
59 let cb = p.lock().unwrap().clone(); if let Some(cb) = cb {
61 cb(ProgressEvent { message });
62 }
63 });
64 let r = Arc::clone(&on_receipt_line);
65 session.set_on_receipt_line(move |text| {
66 let cb = r.lock().unwrap().clone();
67 if let Some(cb) = cb {
68 cb(ReceiptLine { text });
69 }
70 });
71
72 Self {
73 config,
74 session,
75 on_progress,
76 on_receipt_line,
77 on_connection_state_change: Arc::new(Mutex::new(None)),
78 }
79 }
80
81 #[must_use]
83 pub fn configuration(&self) -> &Ecr17Config {
84 &self.config
85 }
86
87 pub fn set_on_progress(&self, cb: impl Fn(ProgressEvent) + Send + Sync + 'static) {
89 *self.on_progress.lock().unwrap() = Some(Arc::new(cb));
90 }
91
92 pub fn set_on_receipt_line(&self, cb: impl Fn(ReceiptLine) + Send + Sync + 'static) {
94 *self.on_receipt_line.lock().unwrap() = Some(Arc::new(cb));
95 }
96
97 pub fn set_on_connection_state_change(
99 &self,
100 cb: impl Fn(ConnectionState) + Send + Sync + 'static,
101 ) {
102 *self.on_connection_state_change.lock().unwrap() = Some(Arc::new(cb));
103 }
104
105 pub fn transport(&self) -> &T {
107 self.session.transport()
108 }
109
110 pub async fn connect(&mut self) -> Result<()> {
112 self.ensure_connected().await
113 }
114
115 pub async fn disconnect(&mut self) {
117 self.session.disconnect().await;
118 self.emit_state(ConnectionState::Disconnected);
119 }
120
121 #[must_use]
123 pub fn is_connected(&self) -> bool {
124 self.session.is_connected()
125 }
126
127 pub async fn status(&mut self) -> Result<PosStatusResponse> {
131 self.ensure_connected().await?;
132 let payload = crate::protocol::build_status(&self.config.terminal_id)?;
133 let pkt = self.run_transaction(&payload, None, true).await?;
134 Ok(map_status(&response::parse_status(&as_str(&pkt))))
135 }
136
137 pub async fn pay(&mut self, request: &PaymentRequest) -> Result<PaymentResult> {
139 self.ensure_connected().await?;
140 let payload = self.payment_payload('P', request)?;
141 let pkt = self
142 .run_transaction(&payload, request.tokenization.as_ref(), false)
143 .await?;
144 Ok(map_payment(&response::parse_payment(&as_str(&pkt))))
145 }
146
147 pub async fn pay_extended(&mut self, request: &PaymentRequest) -> Result<PaymentResult> {
149 self.ensure_connected().await?;
150 let payload = self.payment_payload('X', request)?;
151 let pkt = self
152 .run_transaction(&payload, request.tokenization.as_ref(), false)
153 .await?;
154 Ok(map_payment(&response::parse_payment(&as_str(&pkt))))
155 }
156
157 pub async fn reverse(&mut self, request: &ReversalRequest) -> Result<ReversalResult> {
159 self.ensure_connected().await?;
160 let payload = crate::protocol::build_reversal(
161 &self.config.terminal_id,
162 self.cash_register_id_or(request.cash_register_id.as_deref()),
163 request.stan.as_deref().unwrap_or("000000"),
164 )?;
165 let pkt = self.run_transaction(&payload, None, false).await?;
166 Ok(map_reversal(&response::parse_payment(&as_str(&pkt))))
167 }
168
169 pub async fn pre_auth(&mut self, request: &PreAuthRequest) -> Result<PreAuthResult> {
171 self.ensure_connected().await?;
172 let payload = crate::protocol::build_pre_auth(
173 &self.config.terminal_id,
174 self.cash_register_id_or(request.cash_register_id.as_deref()),
175 request.amount_cents,
176 request.payment_type.unwrap_or_default().as_digit(),
177 request.card_already_present.unwrap_or(false),
178 request.tokenization.is_some(),
179 request.receipt_text.as_deref().unwrap_or(""),
180 )?;
181 let pkt = self
182 .run_transaction(&payload, request.tokenization.as_ref(), false)
183 .await?;
184 Ok(map_pre_auth(&response::parse_pre_auth(&as_str(&pkt))))
185 }
186
187 pub async fn incremental_auth(
189 &mut self,
190 request: &IncrementalAuthRequest,
191 ) -> Result<PreAuthResult> {
192 self.ensure_connected().await?;
193 let payload = crate::protocol::build_incremental(
194 &self.config.terminal_id,
195 self.cash_register_id_or(request.cash_register_id.as_deref()),
196 request.amount_cents,
197 &request.original_pre_auth_code,
198 false,
199 request.receipt_text.as_deref().unwrap_or(""),
200 )?;
201 let pkt = self.run_transaction(&payload, None, false).await?;
202 Ok(map_pre_auth(&response::parse_pre_auth(&as_str(&pkt))))
203 }
204
205 pub async fn pre_auth_closure(
207 &mut self,
208 request: &PreAuthClosureRequest,
209 ) -> Result<PaymentResult> {
210 self.ensure_connected().await?;
211 let payload = crate::protocol::build_pre_auth_closure(
212 &self.config.terminal_id,
213 self.cash_register_id_or(request.cash_register_id.as_deref()),
214 request.amount_cents,
215 &request.original_pre_auth_code,
216 false,
217 request.receipt_text.as_deref().unwrap_or(""),
218 )?;
219 let pkt = self.run_transaction(&payload, None, false).await?;
220 Ok(map_payment(&response::parse_payment(&as_str(&pkt))))
221 }
222
223 pub async fn verify_card(
225 &mut self,
226 request: &CardVerificationRequest,
227 ) -> Result<CardVerificationResult> {
228 self.ensure_connected().await?;
229 let payload = crate::protocol::build_card_verification(
230 &self.config.terminal_id,
231 self.cash_register_id_or(request.cash_register_id.as_deref()),
232 request.payment_type.unwrap_or_default().as_digit(),
233 request.tokenization.is_some(),
234 )?;
235 let pkt = self
236 .run_transaction(&payload, request.tokenization.as_ref(), false)
237 .await?;
238 Ok(map_card_verify(&response::parse_payment(&as_str(&pkt))))
239 }
240
241 pub async fn close_session(&mut self) -> Result<CloseSessionResult> {
243 self.ensure_connected().await?;
244 let payload = crate::protocol::build_close_session(
245 &self.config.terminal_id,
246 &self.config.cash_register_id,
247 false,
248 )?;
249 let pkt = self.run_transaction(&payload, None, false).await?;
250 Ok(map_close(&response::parse_close(&as_str(&pkt))))
251 }
252
253 pub async fn totals(&mut self) -> Result<TotalsResult> {
255 self.ensure_connected().await?;
256 let payload = crate::protocol::build_totals(
257 &self.config.terminal_id,
258 &self.config.cash_register_id,
259 false,
260 )?;
261 let pkt = self.run_transaction(&payload, None, true).await?;
262 Ok(map_totals(&response::parse_totals(&as_str(&pkt))))
263 }
264
265 pub async fn send_last_result(&mut self) -> Result<PaymentResult> {
267 self.ensure_connected().await?;
268 let payload = crate::protocol::build_send_last_result(
269 &self.config.terminal_id,
270 &self.config.cash_register_id,
271 false,
272 )?;
273 let pkt = self.run_transaction(&payload, None, true).await?;
274 Ok(map_payment(&response::parse_payment(&as_str(&pkt))))
275 }
276
277 pub async fn enable_ecr_printing(&mut self, enabled: bool) -> Result<()> {
279 self.ensure_connected().await?;
280 let payload = crate::protocol::build_enable_ecr_print(&self.config.terminal_id, enabled)?;
281 self.run_ack_only(&payload, true).await
282 }
283
284 pub async fn reprint(&mut self, to_ecr: bool) -> Result<()> {
286 self.ensure_connected().await?;
287 let payload = crate::protocol::build_reprint(&self.config.terminal_id, to_ecr, '0')?;
288 self.run_ack_only(&payload, false).await
289 }
290
291 pub async fn vas(&mut self, xml_request: &str) -> Result<VasResult> {
297 self.ensure_connected().await?;
298 let payload = crate::protocol::build_vas(
299 &self.config.terminal_id,
300 &self.config.cash_register_id,
301 xml_request,
302 )?;
303 let pkt = self.run_transaction(&payload, None, false).await?;
304 Ok(map_vas(&response::parse_vas(&as_str(&pkt))))
305 }
306
307 fn cash_register_id_or<'a>(&'a self, override_id: Option<&'a str>) -> &'a str {
310 override_id.unwrap_or(&self.config.cash_register_id)
311 }
312
313 fn payment_payload(&self, code: char, request: &PaymentRequest) -> Result<String> {
314 let builder = if code == 'X' {
315 crate::protocol::build_extended_payment
316 } else {
317 crate::protocol::build_payment
318 };
319 builder(
320 &self.config.terminal_id,
321 self.cash_register_id_or(request.cash_register_id.as_deref()),
322 request.amount_cents,
323 request.payment_type.unwrap_or_default().as_digit(),
324 request.card_already_present.unwrap_or(false),
325 request.tokenization.is_some(),
326 request.receipt_text.as_deref().unwrap_or(""),
327 )
328 }
329
330 fn emit_state(&self, state: ConnectionState) {
331 let cb = self.on_connection_state_change.lock().unwrap().clone();
332 if let Some(cb) = cb {
333 cb(state);
334 }
335 }
336
337 async fn ensure_connected(&mut self) -> Result<()> {
342 if self.session.is_connected() {
343 return Ok(());
344 }
345 self.emit_state(ConnectionState::Connecting);
346 match self.session.connect().await {
347 Ok(()) => {
348 self.emit_state(ConnectionState::Connected);
349 Ok(())
350 }
351 Err(e) => {
352 self.emit_state(ConnectionState::Disconnected);
353 Err(e)
354 }
355 }
356 }
357
358 async fn do_exchange(
359 &mut self,
360 main_payload: &str,
361 tokenization: Option<&TokenizationRequest>,
362 ) -> Result<DecodedPacket> {
363 if let Some(tok) = tokenization {
364 let tag = crate::protocol::format_tokenization_tag(
365 tok.service.is_recurring(),
366 &tok.contract_code,
367 )?;
368 let additional = crate::protocol::build_additional_tags(
369 &self.config.terminal_id,
370 &tag,
371 "62",
372 "DF8D01",
373 )?;
374 self.session
375 .exchange_with_additional_data(main_payload, &additional)
376 .await
377 } else {
378 self.session.exchange(main_payload).await
379 }
380 }
381
382 async fn run_transaction(
386 &mut self,
387 main_payload: &str,
388 tokenization: Option<&TokenizationRequest>,
389 safe_to_retry: bool,
390 ) -> Result<DecodedPacket> {
391 match self.do_exchange(main_payload, tokenization).await {
392 Ok(pkt) => Ok(pkt),
393 Err(original) => {
394 if self.recover_after_error(safe_to_retry).await {
395 self.do_exchange(main_payload, tokenization).await
396 } else {
397 Err(original)
398 }
399 }
400 }
401 }
402
403 async fn run_ack_only(&mut self, payload: &str, safe_to_retry: bool) -> Result<()> {
406 match self.session.send_ack_only(payload).await {
407 Ok(()) => Ok(()),
408 Err(original) => {
409 if self.recover_after_error(safe_to_retry).await {
410 self.session.send_ack_only(payload).await
411 } else {
412 Err(original)
413 }
414 }
415 }
416 }
417
418 async fn recover_after_error(&mut self, safe_to_retry: bool) -> bool {
422 let auto_reconnect = self.config.auto_reconnect.unwrap_or(false);
423 let dropped = !self.session.is_connected();
424 if auto_reconnect && dropped && self.ensure_connected().await.is_err() {
425 return false; }
427 should_retry_after_reconnect(auto_reconnect, dropped, safe_to_retry)
428 }
429}
430
431fn session_config(config: &Ecr17Config) -> SessionConfig {
432 SessionConfig {
433 lrc_mode: config.lrc_mode.unwrap_or_default(),
434 ack_timeout_ms: u64::from(config.ack_timeout_ms.unwrap_or(2000)),
435 response_timeout_ms: u64::from(config.response_timeout_ms.unwrap_or(60_000)),
436 retry_count: config.retry_count.unwrap_or(3),
437 retry_delay_ms: u64::from(config.retry_delay_ms.unwrap_or(200)),
438 receipt_drain_ms: u64::from(config.receipt_drain_ms.unwrap_or(0)),
439 }
440}
441
442fn as_str(pkt: &DecodedPacket) -> String {
443 String::from_utf8_lossy(&pkt.payload).into_owned()
444}
445
446fn opt_str(s: &str) -> Option<String> {
449 if s.is_empty() {
450 None
451 } else {
452 Some(s.to_string())
453 }
454}
455
456fn opt_i64(s: &str) -> Option<i64> {
457 s.trim().parse::<i64>().ok()
458}
459
460fn opt_f64(s: &str) -> Option<f64> {
461 s.trim().parse::<f64>().ok()
462}
463
464fn opt_i32(s: &str) -> Option<i32> {
465 s.trim().parse::<i32>().ok()
466}
467
468fn map_card_type(raw: &str) -> Option<CardType> {
469 match raw {
470 "1" => Some(CardType::Debit),
471 "2" => Some(CardType::Credit),
472 "3" => Some(CardType::Other),
473 _ => None,
474 }
475}
476
477fn map_entry_mode(raw: &str) -> Option<TransactionEntryMode> {
478 match raw {
479 "ICC" => Some(TransactionEntryMode::Icc),
480 "MAG" => Some(TransactionEntryMode::Mag),
481 "MAN" => Some(TransactionEntryMode::Manual),
482 "CLM" => Some(TransactionEntryMode::ClessMag),
483 "CLI" => Some(TransactionEntryMode::ClessIcc),
484 _ => None,
485 }
486}
487
488fn map_payment(p: &response::PaymentResponse) -> PaymentResult {
489 PaymentResult {
490 outcome: p.outcome,
491 result_code: p.result_code.clone(),
492 pan: opt_str(&p.pan),
493 entry_mode: map_entry_mode(&p.transaction_type),
494 auth_code: opt_str(&p.auth_code),
495 host_date_time: opt_str(&p.host_date_time),
496 card_type: map_card_type(&p.card_type),
497 acquirer_id: opt_str(&p.acquirer_id),
498 stan: opt_str(&p.stan),
499 online_id: opt_str(&p.online_id),
500 error_description: opt_str(&p.error_description),
501 currency_exchange: p.currency.applied.then(|| CurrencyExchange {
506 applied: true,
507 rate: opt_f64(&p.currency.rate),
508 currency_code: opt_str(&p.currency.currency_code),
509 amount_cents: opt_i64(&p.currency.amount),
510 precision: opt_i32(&p.currency.precision),
511 }),
512 }
513}
514
515fn map_reversal(p: &response::PaymentResponse) -> ReversalResult {
516 ReversalResult {
517 outcome: p.outcome,
518 result_code: p.result_code.clone(),
519 pan: opt_str(&p.pan),
520 entry_mode: map_entry_mode(&p.transaction_type),
521 host_date_time: opt_str(&p.host_date_time),
522 card_type: map_card_type(&p.card_type),
523 acquirer_id: opt_str(&p.acquirer_id),
524 stan: opt_str(&p.stan),
525 online_id: opt_str(&p.online_id),
526 action_code: None,
527 error_description: opt_str(&p.error_description),
528 }
529}
530
531fn map_card_verify(p: &response::PaymentResponse) -> CardVerificationResult {
532 CardVerificationResult {
533 outcome: p.outcome,
534 result_code: p.result_code.clone(),
535 pan: opt_str(&p.pan),
536 entry_mode: map_entry_mode(&p.transaction_type),
537 auth_code: opt_str(&p.auth_code),
538 host_date_time: opt_str(&p.host_date_time),
539 card_type: map_card_type(&p.card_type),
540 acquirer_id: opt_str(&p.acquirer_id),
541 stan: opt_str(&p.stan),
542 online_id: opt_str(&p.online_id),
543 action_code: None,
544 error_description: opt_str(&p.error_description),
545 }
546}
547
548fn map_pre_auth(p: &response::PreAuthResponse) -> PreAuthResult {
549 PreAuthResult {
550 outcome: p.outcome,
551 result_code: p.result_code.clone(),
552 pan: opt_str(&p.pan),
553 entry_mode: map_entry_mode(&p.transaction_type),
554 auth_code: opt_str(&p.auth_code),
555 pre_authorized_amount_cents: opt_i64(&p.pre_authorized_amount),
556 pre_auth_code: opt_str(&p.pre_auth_code),
557 action_code: opt_str(&p.action_code),
558 host_date_time: opt_str(&p.host_date_time),
559 card_type: map_card_type(&p.card_type),
560 acquirer_id: opt_str(&p.acquirer_id),
561 stan: opt_str(&p.stan),
562 online_id: opt_str(&p.online_id),
563 error_description: opt_str(&p.error_description),
564 }
565}
566
567fn map_status(s: &response::StatusResponse) -> PosStatusResponse {
568 PosStatusResponse {
569 terminal_id: s.terminal_id.clone(),
570 terminal_date_time: raw_datetime_to_iso(&s.date_time_raw),
571 status: s.status,
572 software_release: s.software_release.clone(),
573 }
574}
575
576fn map_totals(t: &response::TotalsResponse) -> TotalsResult {
577 TotalsResult {
578 outcome: t.outcome,
579 result_code: t.result_code.clone(),
580 pos_total_cents: opt_i64(&t.pos_total).unwrap_or(0),
581 }
582}
583
584fn map_close(c: &response::CloseResponse) -> CloseSessionResult {
585 CloseSessionResult {
586 outcome: c.outcome,
587 result_code: c.result_code.clone(),
588 pos_total_cents: opt_i64(&c.pos_total),
589 host_total_cents: opt_i64(&c.host_total),
590 action_code: opt_str(&c.action_code),
591 error_description: opt_str(&c.error_description),
592 }
593}
594
595fn map_vas(v: &response::VasResponse) -> VasResult {
596 VasResult {
597 response_id: v.response_id.clone(),
598 response_message: v.response_message.clone(),
599 order_id: opt_str(&v.order_id),
600 raw_xml: v.raw_xml.clone(),
601 }
602}
603
604fn raw_datetime_to_iso(raw: &str) -> String {
607 if raw.len() < 10 {
608 return String::new();
609 }
610 let (dd, mm, yy, hh, min) = (&raw[0..2], &raw[2..4], &raw[4..6], &raw[6..8], &raw[8..10]);
611 format!("20{yy}-{mm}-{dd}T{hh}:{min}:00")
612}
613
614#[cfg(test)]
615mod tests {
616 use super::*;
617 use crate::codec::{PacketCodec, ACK, EOT, SOH};
618 use crate::error::Ecr17Error;
619 use crate::lrc::LrcMode;
620 use crate::transport::FakeTransport;
621 use crate::types::PaymentCardType;
622
623 fn config() -> Ecr17Config {
624 Ecr17Config {
625 host: "127.0.0.1".into(),
626 port: None,
627 terminal_id: "12345678".into(),
628 cash_register_id: "00000001".into(),
629 lrc_mode: Some(LrcMode::Std),
630 keep_alive: None,
631 auto_reconnect: Some(true),
632 connection_timeout_ms: None,
633 response_timeout_ms: Some(40),
634 ack_timeout_ms: Some(40),
635 receipt_drain_ms: None,
636 retry_count: Some(2),
637 retry_delay_ms: Some(1),
638 debug: None,
639 }
640 }
641
642 fn ok_payment_result() -> Vec<u8> {
644 let c = PacketCodec::new(LrcMode::Std);
645 let mut payload = String::from("123456780E00");
648 payload.push_str(&"0".repeat(19)); payload.push_str("ICC");
650 payload.push_str("AUTH01");
651 payload.push_str("2111520");
652 payload.push('2');
653 payload.push_str("ACQ ");
654 payload.push_str("000042");
655 payload.push_str("000099");
656 c.encode_application(payload.as_bytes())
657 }
658
659 fn ack_then(result: Vec<u8>) -> Vec<u8> {
660 let mut r = PacketCodec::new(LrcMode::Std).encode_control(ACK);
661 r.extend(result);
662 r
663 }
664
665 #[tokio::test]
666 async fn pay_maps_an_approved_result() {
667 let mut t = FakeTransport::new();
668 t.enqueue_response(ack_then(ok_payment_result()));
669 let mut client = Ecr17Client::new(t, config());
670
671 let req = PaymentRequest {
672 amount_cents: 650,
673 cash_register_id: None,
674 payment_type: Some(PaymentCardType::Credit),
675 card_already_present: None,
676 receipt_text: None,
677 tokenization: None,
678 };
679 let result = client.pay(&req).await.unwrap();
680 assert_eq!(result.outcome, crate::types::TransactionOutcome::Ok);
681 assert_eq!(result.result_code, "00");
682 assert_eq!(result.entry_mode, Some(TransactionEntryMode::Icc));
683 assert_eq!(result.card_type, Some(CardType::Credit));
684 assert_eq!(result.auth_code.as_deref(), Some("AUTH01"));
685 assert_eq!(result.stan.as_deref(), Some("000042"));
686 }
687
688 #[tokio::test]
689 async fn client_forwards_progress_to_listener() {
690 let c = PacketCodec::new(LrcMode::Std);
691 let mut response = c.encode_control(ACK);
692 let mut progress_frame = vec![SOH];
693 progress_frame.extend_from_slice(b"ATTENDERE PREGO ");
694 progress_frame.push(EOT);
695 response.extend(progress_frame);
696 response.extend(ok_payment_result());
697 let mut t = FakeTransport::new();
698 t.enqueue_response(response);
699 let mut client = Ecr17Client::new(t, config());
700
701 let seen = Arc::new(Mutex::new(Vec::<String>::new()));
702 let sink = Arc::clone(&seen);
703 client.set_on_progress(move |e| sink.lock().unwrap().push(e.message));
704
705 let req = PaymentRequest {
706 amount_cents: 650,
707 cash_register_id: None,
708 payment_type: None,
709 card_already_present: None,
710 receipt_text: None,
711 tokenization: None,
712 };
713 client.pay(&req).await.unwrap();
714 assert_eq!(
715 *seen.lock().unwrap(),
716 vec!["ATTENDERE PREGO ".to_string()]
717 );
718 }
719
720 #[tokio::test]
721 async fn status_maps_iso_datetime() {
722 let c = PacketCodec::new(LrcMode::Std);
723 let mut payload = String::from("123456780s");
725 payload.push_str(&"0".repeat(10));
726 payload.push_str("0102251530");
727 payload.push('2');
728 payload.push_str("V1.2.3");
729 let mut t = FakeTransport::new();
730 t.enqueue_response(ack_then(c.encode_application(payload.as_bytes())));
731 let mut client = Ecr17Client::new(t, config());
732
733 let s = client.status().await.unwrap();
734 assert_eq!(s.terminal_id, "12345678");
735 assert_eq!(s.status, 2);
736 assert_eq!(s.terminal_date_time, "2025-02-01T15:30:00");
737 assert_eq!(s.software_release, "V1.2.3");
738 }
739
740 #[tokio::test]
743 async fn financial_command_not_replayed_on_drop() {
744 let mut t = FakeTransport::new();
745 t.disconnect_on_next_request();
746 let mut client = Ecr17Client::new(t, config()); let req = PaymentRequest {
749 amount_cents: 1000,
750 cash_register_id: None,
751 payment_type: None,
752 card_already_present: None,
753 receipt_text: None,
754 tokenization: None,
755 };
756 let err = client.pay(&req).await.unwrap_err();
757 assert!(matches!(err, Ecr17Error::Disconnected));
758 assert_eq!(client.transport().application_request_count(), 1);
760 }
761
762 #[tokio::test]
764 async fn safe_command_retried_after_reconnect() {
765 let c = PacketCodec::new(LrcMode::Std);
766 let mut t = FakeTransport::new();
767 t.disconnect_on_next_request(); let mut payload = String::from("123456780s");
770 payload.push_str(&"0".repeat(10));
771 payload.push_str("0102251530");
772 payload.push('2');
773 payload.push_str("V1");
774 t.enqueue_response(ack_then(c.encode_application(payload.as_bytes())));
775 let mut client = Ecr17Client::new(t, config());
776
777 let s = client.status().await.unwrap();
778 assert_eq!(s.status, 2);
779 assert_eq!(client.transport().application_request_count(), 2);
781 }
782
783 #[test]
784 fn iso_datetime_conversion() {
785 assert_eq!(raw_datetime_to_iso("0102251530"), "2025-02-01T15:30:00");
786 assert_eq!(raw_datetime_to_iso("short"), "");
787 }
788}