Skip to content

feat: ECR17 transport, orchestration, response parsing & full command set#4

Merged
lopadova merged 32 commits into
mainfrom
feat/ecr17-transport-and-commands
May 28, 2026
Merged

feat: ECR17 transport, orchestration, response parsing & full command set#4
lopadova merged 32 commits into
mainfrom
feat/ecr17-transport-and-commands

Conversation

@lopadova
Copy link
Copy Markdown
Contributor

@lopadova lopadova commented May 27, 2026

Overview

Implements the full ECR17 stack on top of the framing/LRC core: TCP/LAN transport,
ACK/NAK + retransmit orchestration, response parsing, the complete command set,
an async Promise API with events, and payment-safety throughout. Base: main.

CI: cpp-tests 82/82 ✅ · ts-checks ✅ · android-build ✅ (manual dispatch).
iOS Swift transport is written best-effort (no macOS CI runner — see roadmap).

What's included

  • Builders (Ecr17Protocol): all commands — P X p i c H U C T G E R K s S
    fixed-width, validated, byte-exact vs spec.
  • Parsers (Ecr17Response): E/V/s/T/C/e/K incl. DCC; defensive (no UB).
  • Orchestration (Ecr17Session): physical ACK/NAK handshake, retransmit-up-to-3,
    timeouts, progress (SOH) + receipt (S) forwarding, stream framer; tested
    against an in-memory FakeTransport.
  • Async client (HybridEcr17Client): connect/status/pay/payExtended/
    reverse/preAuth/incrementalAuth/preAuthClosure/verifyCard/closeSession/
    totals/sendLastResult/enableEcrPrinting/reprint/vas + progress/receipt/
    connection events. Auto-connect; tokenization (U) flow; configurable receipt drain.
  • Native transport: Kotlin TCP (HybridEcr17Transport.kt, CI-built) + Swift
    Network.framework (best-effort). Nitro generates the C++↔Kotlin JNI bridge.

💰 Payment safety (money-critical)

  • No double charge: on a drop, autoReconnect restores the socket, but a
    financial command is never blindly re-sent — payments/reversals/pre-auths
    reconnect and surface the error; recover the outcome via sendLastResult()
    (spec command G). Only read-only/idempotent ops are retried. The decision is
    isolated in RetryPolicy.hpp and unit-tested (test_retry_policy.cpp).
  • LRC validated on every frame (NAK on failure); fixed-width validation on send.

Tests

82 C++ tests (LRC, codec edge cases, every builder layout, every parser, session
flows incl. tokenization/receipt-drain/NAK-retransmit/timeout/disconnect, and the
retry-safety invariants) + an opt-in real-terminal integration test
(ECR17_TERMINAL_HOST=… ctest -R Integration).

Process: each change went through local tests → local Copilot review → CI green.
Learnings in docs/LESSON.md, status in PROGRESS.md, workflow distilled into
root AGENTS.md.

Remaining (roadmap)

  • iOS native transport verification (needs a macOS CI runner).
  • Auto-reconnect-on-drop retries only safe commands by design (financial recover via G).

🤖 Generated with Claude Code

lopadova and others added 5 commits May 28, 2026 01:21
…Phase 0)

Redesign the public Nitro API for real end-to-end operation:
- Ecr17Client commands become async (Promise<T>): connect/status/pay/payExtended/
  reverse/preAuth/incrementalAuth/preAuthClosure/verifyCard/closeSession/totals/
  sendLastResult/enableEcrPrinting/reprint/vas; configure/configuration stay sync.
  Adds event setters: onProgress, onReceiptLine, onConnectionStateChange.
- New Ecr17Transport HybridObject spec (Swift/Kotlin) for the native LAN socket.
- New request/result types in types/client.ts mirroring the spec response tables
  (PaymentResult, ReversalResult, PreAuthResult, CardVerificationResult,
  TotalsResult, CloseSessionResult, VasResult, CurrencyExchange, tokenization, ...).
- nitro.json: register Ecr17Transport autolinking.
- Add self-contained package/tsconfig.ci.json so `tsc --noEmit` runs without the
  private @padosoft/config package (the repo's tsconfig extends it; not on npm).
- Add PROGRESS.md (resumable status) and docs/LESSON.md (accumulated learnings).

nitrogen regenerates 2 HybridObjects cleanly; standalone typecheck passes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PacketCodec: SOH branch now checks data.back() == EOT before accepting
the frame as PROGRESS. Previously any SOH-starting buffer was silently
classified as a valid progress update regardless of the terminator byte.
Adds a regression test (DecodeSohWithoutEotIsUnknown).

Ecr17Client: update status() (and all newly spec'd methods) to the async
Promise-based signatures that Nitrogen will generate from the updated
client.nitro.ts spec. The old PosStatusResponse status() return type
mismatched the new spec, which would have made HybridEcr17Client
abstract and non-compilable after nitrogen regeneration. All new command
and event methods are stubbed to reject/no-op pending Phase 1+
implementation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Verify & correct the autonomous Copilot review edits (it edits in --yolo mode):
- Ecr17Client.cpp: stubs used `Promise<T>::async([](auto& res){...})`, but the
  Nitro API is `async(std::function<T()>)` — lambda takes no args and returns T.
  Rewrote as `Promise<T>::async([]() -> T { throw ...; })` (void variant for
  connect/enableEcrPrinting/reprint) via a [[noreturn]] helper.
- Ecr17Client.hpp: setOnConnectionStateChange must match the generated spec
  exactly — ConnectionState is passed BY VALUE
  (`std::function<void(ConnectionState)>`), not `const ConnectionState&`, else the
  override doesn't match and the class stays abstract.
- Kept Copilot's genuine improvement: SOH decode now requires the final byte to
  be EOT (+ its regression test).
- LESSON.md: recorded that copilot --yolo edits autonomously and its nitro/C++
  signatures must be verified against the generated headers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ctionState)

Add std::function members and have the setters store the callbacks instead of
discarding them, so the session can fire them in later phases. Addresses the
local Copilot review finding. Still Phase-0 stubs otherwise.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extend Ecr17Protocol with every remaining command, fixed-width and validated:
- Payment family (167B): 'P' payment (now takes paymentType/cardPresent/
  additionalData/receiptText with back-compatible defaults), 'X' extended,
  'p' pre-auth (shared buildPaymentLike).
- Pre-auth follow-ups (176B): 'i' incremental, 'c' closure (shared layout with
  the original pre-auth code field).
- 'H' card verification (39B), 'C' close session + 'T' totals (26B),
  'G' send last result (22B), 'E' enable/disable ECR printing (11B),
  'R' reprint (22B).
- 'K' VAS (length-prefixed XML, max 1024) and 'U' additional-data/tokenization
  (privative TAG content + 0x1B terminator) + formatTokenizationTag helper
  (Intesa 0COF/0REC ... |0FNZ03 mapping).
- Reuse leftPad (overflow-throwing) + new rightPad; amount/field validation.

Adds test_protocol_commands.cpp asserting the exact byte layout and validation
of every builder. Existing payment/status/reversal tests remain green
(new params default to the previous behaviour).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New Ecr17Response unit (plain structs, Nitro-independent so it's unit-testable):
- parsePayment ('E'/'V'): result code -> outcome, positive (PAN/txType/authCode/
  hostDateTime), negative (description), common (cardType/acquirer/STAN/idOnline),
  and the DCC currency-exchange block for 'V'.
- parseStatus ('s'): terminalId, raw DDMMYYhhmm, numeric status, SW release.
- parseTotals ('T'), parseClose ('C', pos/host totals or description+actionCode).
- parsePreAuth ('e'): incl. preAuthCode + preAuthorizedAmount + actionCode.
- parseVas ('K'): concatenation flag, id message, raw XML + RESPID/RESPMSG/
  ORDER_ID extracted from the XML.
All parsing is defensive (out-of-range fields come back empty, never UB).

test_response.cpp synthesizes payloads field-by-field at the exact spec offsets
(width-guaranteed helpers) for positive/negative/DCC/short-payload cases.
Wired into the test build and android/CMakeLists.txt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
lopadova and others added 15 commits May 28, 2026 02:19
…hase 3)

Ecr17Session drives one request/response exchange over the abstract Transport:
- frames the request; physical ACK/NAK handshake with retransmission up to
  retryCount on NAK or ackTimeout (spec: up to 3);
- waits for the application response within responseTimeout, forwarding SOH
  progress messages (onProgress) and 'S' receipt lines (onReceiptLine seen
  before the result), ACKing valid frames and NAKing invalid-LRC ones;
- a stream framer (extractFrameLocked) splits the rx byte stream into ACK/NAK
  (3B), STX..ETX+LRC, and SOH..EOT frames, resynchronising past junk;
- thread-safe rx buffer fed by the transport data callback (mutex + condvar),
  throws on disconnect or timeout.

Adds header-only FakeTransport (deterministic, scriptable: each STX request pops
the next queued reply; control sends don't) and test_session.cpp covering happy
path, NAK->retransmit, ack-timeout exhaustion, bad-LRC->NAK, progress/receipt
forwarding, response timeout, and disconnect. Wired into both build files
(+Threads link for the test exe).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Process artifacts only (LESSON.md: copilot focused-prompt efficiency, Threads
link, LrcMode include, session/FakeTransport notes; PROGRESS.md: Phase 4 plan).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- New ts-checks.yml: bun install -> nitrogen codegen -> tsc --noEmit via the
  self-contained tsconfig.ci.json (deterministic gate for the TS specs/codegen).
- Bump actions/checkout@v4 -> v5 (Node20 deprecation warning).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The TS/codegen job failed because @padosoft/config (root devDep) is a private
GitHub Packages package; bunfig.toml maps the padosoft scope to
npm.pkg.github.com via $GESCAT_NPM_TOKEN. Provide the token from secrets
(GESCAT_NPM_TOKEN, falling back to GITHUB_TOKEN) with packages:read.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The job can't run: every `bun install` resolves the root devDep
@padosoft/config (private GitHub Packages) and gets 403 with the available
token. This also blocks any native build job. Re-add once a GESCAT_NPM_TOKEN
Actions secret with read:packages exists (or @padosoft/config is public).
The C++ unit-test CI (the real gate) stays green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…on user)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…h needed)

CI typecheck/nitrogen don't use @padosoft/config (we use tsconfig.ci.json and
skip biome), so strip it from the root devDeps + drop the lockfile before
bun install. Removes the private GitHub Packages 403 blocker entirely — no token
or package-visibility change required.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Expo prebuild + gradle assembleDebug compiles the nitro module's C++ (CMake) and
Kotlin. Uses the same @padosoft/config strip for install. Baselines current
(compiling) code so it catches errors when the client wiring + native transport
land. May need iteration (heavy Expo+NDK build).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…o PROGRESS

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ase 4)

- NativeTransportAdapter: bridges the Nitro Ecr17Transport HybridObject to the
  C++ Transport interface (ArrayBuffer<->vector); session uses it for I/O.
- Ecr17Session: add sendAckOnly() for ACK-only commands (enable-print/reprint),
  with fast FakeTransport tests (ack / NAK-retransmit / timeout).
- HybridEcr17Client: real implementation — lazily creates the transport via the
  Nitro registry, builds an Ecr17Session from the config, and implements every
  command (build -> exchange -> parse -> map to the generated result struct).
  connect/disconnect/isConnected + progress/receipt/connection events wired.
  Generated struct/enum field names verified against nitrogen output.
- Rename parser DCC struct CurrencyExchange -> DccInfo to avoid clashing with the
  generated CurrencyExchange in the same namespace.
- android/CMakeLists.txt: compile NativeTransportAdapter.cpp.

Client/adapter compile only in the native build (Phase 8b Android job); the
session additions are covered by the fast C++ unit CI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Android: HybridEcr17Transport.kt — java.net.Socket + daemon reader thread,
  forwards bytes to C++ via onData(ArrayBuffer.copy); Promise.parallel for
  connect. Nitro generates the C++<->Kotlin JNI bridge (no manual JNI).
- iOS: HybridEcr17Transport.swift — Network.framework NWConnection (best-effort;
  no iOS CI runner yet, must be verified on a real iOS build; mirrors Kotlin).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… review)

- disconnect() no longer triggers a spurious onDisconnect: the reader's finally
  suppresses the callback when the close was caller-initiated (intentionalDisconnect).
- connect() tears down any previous connection and joins the old reader thread
  before opening a new socket (no leaked thread/socket on reconnect).
Pure logic; uses the same APIs the green Android build already compiled.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ROGRESS

Captures the mandatory per-phase workflow, CI strategy, and verified nitro/build
know-how so future sessions respect them without rediscovery.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@lopadova lopadova changed the base branch from fix/ecr17-spec-compliance to main May 28, 2026 08:59
lopadova and others added 6 commits May 28, 2026 11:19
…in, opt-in terminal test

- Client auto-connects (ensureConnected blocks the worker on the transport's
  awaitable connect Promise); keepAlive keeps the socket open. No manual connect()
  required before commands.
- Tokenization (U) wired end-to-end: pay/payExtended/preAuth/verifyCard build with
  withAdditionalData when request.tokenization is set, then send the 'U' message via
  the new Ecr17Session::exchangeWithAdditionalData (P -> ACK -> U -> ACK -> result).
- Receipt streaming after the result: SessionConfig.receiptDrainMs (+ Ecr17Config
  field) forwards trailing 'S' receipt lines until the terminal goes quiet.
- Refactor exchange() = sendAckOnly + waitForResult (all existing session tests
  preserved); add tests for additional-data flow and receipt drain.
- Opt-in real-terminal integration test (PosixTcpTransport + test_integration_terminal,
  gated by ECR17_TERMINAL_HOST; skipped in CI, run locally against a terminal).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…view)

drainReceipts now NAKs bad-LRC application frames like waitForResult does,
instead of silently dropping them (which would stall the drain).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…uto-connect, terminal test)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
On a disconnect during a command, if config.autoReconnect is set the socket is
reconnected. Retry policy is SAFETY-AWARE to avoid double-charging:
- read-only / idempotent ops (status, totals, sendLastResult 'G', enableEcrPrinting)
  are retried after reconnect;
- financial ops (pay, payExtended, reverse, preAuth, incremental, preAuthClosure,
  verifyCard, vas, closeSession, reprint) reconnect but RE-THROW — the caller
  recovers the lost result via sendLastResult ('G'), never a blind re-send.
All command exchanges routed through runTransaction()/runAckOnly().

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extract the retry decision into shouldRetryAfterReconnect() (Session/RetryPolicy.hpp)
and lock the invariants in test_retry_policy.cpp: a financial command is NEVER
replayed (no double-charge), a safe/idempotent command is replayed only on
autoReconnect after a real drop. Client routes its decision through this function.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nt 82

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
lopadova and others added 4 commits May 28, 2026 12:01
…mains

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…r RN/native

Add a 'Why this exists' section: ECR17 specs are NDA-gated, no RN/native
open-source option existed; positions this as the most complete one for React
Native & native mobile, with an approachable async API.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…lish, untrack Nexi doc

- FIX: Ecr17Session never reset disconnected_ after a drop, so a reconnected
  transport still threw "disconnected" on the next command — auto-reconnect did
  not actually recover. Add resetForNewTransaction() (clear disconnected_ +
  rxBuffer_) at the start of every transaction; extract ackHandshake() so
  exchange/exchangeWithAdditionalData reset once. Regression test
  Session.RecoversAndSucceedsAfterReconnect (FakeTransport drop -> rearm).
- README: official PUBLIC source is the Nexi developer portal (linked, not
  vendored); position as the most complete open-source ECR17 toolkit for
  RN/native; highlight the C++<->Kotlin JNI bridge; iOS verified-on-device;
  drop the completed Roadmap; 83 tests.
- Untrack the full Nexi doc (git rm --cached + .gitignore): public docs aren't
  free to republish without permission; kept local/private, official URL linked.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Fix Session.RecoversAndSucceedsAfterReconnect: it built the response frame from
  two different temporaries' begin()/end() (UB / wrong range); use append().
- Add root CLAUDE.md and a README "Vibe-coding batteries included" section:
  the repo ships AGENTS.md / CLAUDE.md / docs/LESSON.md / PROGRESS.md so AI
  assistants get accurate context and respect the payment-safety invariants.
- AGENTS.md: add the money-critical never-retry-financial rule up top.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@lopadova lopadova force-pushed the feat/ecr17-transport-and-commands branch from fde8e62 to 00100c4 Compare May 28, 2026 12:48
@lopadova lopadova marked this pull request as ready for review May 28, 2026 14:10
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 00100c408d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +274 to +275
try {
return doExchange();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Serialize command exchanges on the shared session

Every public command runs inside Promise::async, so two JS calls can enter runTransaction at the same time and share the same session_/native callbacks. Because Ecr17Session::exchange() resets the receive buffer and then both exchanges wait on the same callback stream, concurrent calls such as pay() plus status() can clear or consume each other's frames, ACK the wrong response, or return a mismatched result. Add a transaction-level mutex around all runTransaction/runAckOnly exchanges so only one ECR17 command is in flight per client.

Useful? React with 👍 / 👎.

Comment on lines +93 to +96
override fun isConnected(): Boolean {
val s = socket
return s != null && s.isConnected && !s.isClosed
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Treat closed Android sockets as disconnected

When the terminal drops the TCP connection, the reader loop exits and sets running = false, but the socket remains non-null and not explicitly closed. On Android/Java, Socket.isConnected only means the socket has connected before, so after an unexpected peer close this can still return true; runTransaction then computes dropped=false, skips auto-reconnect/retry, and later ensureConnected() also refuses to open a fresh socket. Include running or close/null the socket in the reader's finally path so unexpected EOF is observable as disconnected.

Useful? React with 👍 / 👎.

Comment on lines +132 to +134
// Common fields (per spec offsets; note pos 48 overlaps the positive amount
// field in the spec table, so cardType is only meaningful for KO responses).
r.cardType = at(p, 48, 1);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid reporting pre-auth amount digits as card type

For successful pre-auth responses, the parser reads preAuthorizedAmount from positions 41-48 and then unconditionally reads cardType from position 48, so any approved amount ending in 1, 2, or 3 is exposed to JS as debit/credit/other even though that byte is just the last amount digit. This affects successful preAuth() results with those amount endings; leave cardType empty for the OK layout or parse it from a non-overlapping KO/common offset.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements the full ECR17 stack on top of the existing framing/LRC core: complete request builders, response parsers, an ACK/NAK + retransmit session orchestrator with timeouts, a native TCP transport (Kotlin + best-effort Swift), and an async Promise/event-based HybridEcr17Client that ties them together. Payment-safety is enforced via an isolated RetryPolicy that prevents financial commands from being blindly replayed on auto-reconnect, with recovery via sendLastResult ('G'). Adds 82 C++ unit tests, an opt-in real-terminal integration test, CI workflows (ts-checks, android-build), self-contained CI tsconfig, and substantial README/agent-context documentation.

Changes:

  • Complete ECR17 command builders, response parsers, session orchestration, and native transport (Kotlin/Swift) wired through a Nitro async client API with progress/receipt/connection events.
  • Money-safety policy (RetryPolicy.hpp) keeping financial commands from being replayed after reconnect, with extensive C++ test coverage including a FakeTransport and opt-in real-terminal test.
  • New CI (ts-checks, android-build) and refreshed README / AGENTS.md / CLAUDE.md / LESSON.md / PROGRESS.md documentation.

Reviewed changes

Copilot reviewed 38 out of 39 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
PROGRESS.md New session/phase progress log (contains some duplicated stale checklist entries).
package/tsconfig.ci.json Self-contained typecheck config avoiding the private @padosoft/config.
package/src/types/client.ts New request/result/event TypeScript types for the full command set.
package/src/specs/transport.nitro.ts Nitro spec for the native Ecr17Transport (Swift/Kotlin).
package/src/specs/client.nitro.ts Expanded client spec with async commands, events, and receiptDrainMs.
package/src/index.ts Re-exports the new transport spec.
package/README.md Major rewrite documenting the full feature set, async API, events, and AI-agent context.
package/nitro.json Registers the new Ecr17Transport HybridObject.
package/ios/HybridEcr17Transport.swift iOS Swift TCP transport via Network.framework (best-effort, no iOS CI).
package/cpp/Transport/NativeTransportAdapter.{hpp,cpp} Adapts the Nitro transport spec to the C++ Transport interface.
package/cpp/Transport/FakeTransport.hpp Deterministic in-memory transport for session tests.
package/cpp/Session/RetryPolicy.hpp Money-critical decision: never replay financial commands after reconnect.
package/cpp/Session/Ecr17Session.{hpp,cpp} ACK/NAK + retransmit orchestrator, progress/receipt forwarding, receipt drain.
package/cpp/PacketCodec/PacketCodec.cpp Rejects SOH frames not terminated with EOT.
package/cpp/Ecr17Response/Ecr17Response.{hpp,cpp} Defensive parsers for E/V/s/T/C/e/K responses (incl. DCC).
package/cpp/Ecr17Protocol/Ecr17Protocol.{hpp,cpp} Complete command builder set with fixed-width validation.
package/cpp/Ecr17Client/Ecr17Client.{hpp,cpp} Async client wiring transport + session + parsers, tokenization flow, retry policy.
package/cpp/tests/* New tests: protocol commands, responses, session, retry policy, integration, packet codec regression.
package/cpp/tests/CMakeLists.txt Builds new test files and links pthread.
package/cpp/tests/PosixTcpTransport.hpp Test-only POSIX TCP transport for the opt-in integration test.
package/android/src/main/java/com/margelo/nitro/ecr17/HybridEcr17Transport.kt Kotlin TCP transport with intentional-disconnect handling.
package/android/CMakeLists.txt Adds new C++ sources to the Android build.
docs/LESSON.md, CLAUDE.md, AGENTS.md Agent-facing project guidance / accumulated lessons / non-negotiables.
.gitignore Ignores the privately-kept full Nexi spec markdown.
.github/workflows/{ts-checks.yml,android-build.yml,cpp-tests.yml} New CI for TS+nitrogen and Android build; checkout bumped to v5.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +276 to +286
} catch (const std::exception&) {
const bool autoReconnect = config_.autoReconnect.value_or(false);
const bool dropped = !transport_ || !transport_->isConnected();
if (autoReconnect && dropped) {
ensureConnected(); // restore the socket for subsequent commands
}
if (shouldRetryAfterReconnect(autoReconnect, dropped, safeToRetry)) {
return doExchange(); // only read-only/idempotent ops may be replayed
}
throw; // financial op: surface the error (recover via sendLastResult / 'G')
}
Comment on lines +154 to +155
}
// Ignore any progress frames that may precede the ACK.
Comment thread package/cpp/Ecr17Client/Ecr17Client.cpp Outdated
Comment on lines +300 to +302
}
// else: surface the original error (already rethrown below for non-retry)
else {
Comment thread PROGRESS.md Outdated
Comment on lines +60 to +66
- [ ] Phase 3 — Ecr17Session orchestration + FakeTransport
- [ ] Phase 4 — HybridEcr17Client async + events
- [ ] Phase 5 — native transport (Swift/Kotlin + JNI; ref corasan/image-compressor#11)
- [ ] Phase 6 — C++ tests expanded
- [ ] Phase 7 — README (Roadmap/Feature status ✅) + example
- [ ] Phase 8 — CI: typecheck + nitrogen check + Android/iOS build jobs
- [ ] Phase 9 — distill LESSON.md into AGENTS.md / rules / skills
Comment on lines +206 to +212
void HybridEcr17Client::configure(const Ecr17Config& config) {
config_ = config;
// Force re-init so a new configuration rebuilds the session/timeouts.
session_.reset();
adapter_.reset();
transport_.reset();
}
Money-critical / correctness:
- session: don't drop an APPLICATION result that arrives before/without the
  physical ACK — stash it for waitForResult() so a completed transaction is
  never lost to a handshake timeout (+ regression test).
- client: serialize command exchanges with a transaction mutex; concurrent
  Promise workers shared one session_/RX buffer and could ACK each other's
  frames or clobber the buffer.
- response(preAuth): stop leaking the approved-amount last digit (pos 48,
  inside preAuthorizedAmount 41-48) as cardType; only read it for the KO
  layout (+ regression test).

Robustness / quality:
- client: on auto-reconnect failure, surface the original exchange error
  instead of the reconnect error (runTransaction + runAckOnly).
- client(configure): disconnect the existing transport before tearing it
  down so the native socket doesn't leak.
- client(runAckOnly): clarify retry control flow (no orphaned else).
- android transport: isConnected() now also checks the reader's `running`
  flag — Socket.isConnected stays true after a peer close, defeating
  auto-reconnect/retry.
- docs(PROGRESS): remove the stale duplicate Phase 3-9 checklist.

Verified locally: g++ 16 (C++20, -Wall -Wextra) RED->GREEN on the two core
regression tests + sanity; full gtest suite runs in CI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@lopadova lopadova merged commit 0e255b8 into main May 28, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants