From 37ecb32165248cac3fdab97de0701511fdfb1e2e Mon Sep 17 00:00:00 2001 From: "lorenzo.padovani@padosoft.com" Date: Wed, 27 May 2026 23:40:33 +0200 Subject: [PATCH 1/6] fix: ECR17 spec-compliance and build bugs in C++ protocol stack Verified the implementation against the Nexi ECR17 protocol spec and fixed correctness/build defects: - Ecr17Client::status(): non-void function with no return (UB / garbage struct returned to JS). Now throws a clear "not implemented" error until the transport send/receive + status-response parsing flow exists. - Ecr17Protocol.hpp: removed illegal qualified name in the member declaration (Ecr17Protocol::buildStatusMessage), which fails to compile on Clang (Android NDK / iOS). - PacketCodec::decode(): guard the SOH (progress-update) branch against buffers shorter than 2 bytes (invalid iterator range -> UB/crash); read the LRC as the byte immediately after ETX instead of data.back() and require its presence; add missing / includes. - PacketCodec.hpp: fix cross-include "Lcr.hpp" -> "Lcr/Lcr.hpp" (did not resolve from the ../cpp include root); rename PacketType::STATUS -> PROGRESS to match the spec (SOH 0x01 frame is a progress update, not the terminal status response). - Ecr17Protocol: validate fixed-width fields (throw on overflow) and reject negative amounts, so a malformed frame is never sent to the terminal. - Remove Transport/Transport.cpp: it was a byte-for-byte copy of the header for a pure-abstract interface (no translation unit needed). - android/CMakeLists.txt: compile the new .cpp sources (Ecr17Client, Ecr17Protocol, Lcr, PacketCodec); previously only Ecr17.cpp was built, so HybridEcr17Client symbols would be undefined at link time on Android. Confirmed correct against the spec (no change needed): status message code is lowercase 's' (0x73) and the Payment ("P") message layout is exactly 167 bytes with the documented field order/padding. Also vendored the protocol reference under docs/. Co-Authored-By: Claude Opus 4.7 (1M context) --- ...R17 - supported by Nexi Group terminals.md | 1513 +++++++++++++++++ package/android/CMakeLists.txt | 7 +- package/cpp/Ecr17Client/Ecr17Client.cpp | 13 +- package/cpp/Ecr17Protocol/Ecr17Protocol.cpp | 17 +- package/cpp/Ecr17Protocol/Ecr17Protocol.hpp | 2 +- package/cpp/PacketCodec/PacketCodec.cpp | 28 +- package/cpp/PacketCodec/PacketCodec.hpp | 5 +- package/cpp/Transport/Transport.cpp | 31 - 8 files changed, 1576 insertions(+), 40 deletions(-) create mode 100644 docs/standard ECR17 - supported by Nexi Group terminals.md delete mode 100644 package/cpp/Transport/Transport.cpp diff --git a/docs/standard ECR17 - supported by Nexi Group terminals.md b/docs/standard ECR17 - supported by Nexi Group terminals.md new file mode 100644 index 0000000..e1d303b --- /dev/null +++ b/docs/standard ECR17 - supported by Nexi Group terminals.md @@ -0,0 +1,1513 @@ +--- +title: "Traditional POS Start Page | Traditional POS | Nexi group developer portal" +source: "https://developer.nexigroup.com/traditionalpos/en-EU/docs/" +author: +published: +created: 2026-05-27 +description: +tags: + - "clippings" +--- +## Documentation for Traditional POS + +Traditional POS documentation includes the description of the protocol, derived from the Italian standard **ECR17**, supported by Nexi Group terminals for local connection with Merchant systems in order to integrate electronic payment services. + +![undefined](https://developer.nexigroup.com/static/background-f6a3aae6cf2045cedb11ea0cce88701a.png) + +Traditional terminals are offered in the following configurations: + +- **Desktop with Pin Pad:** connected via Ethernet to the shop lan, can be used as a standalone terminal or integrated with the cash register. +- **Cordless+:** can be connected via Ethernet through a charge and connectivity cradle and directly via Wi-Fi, to the shop’s LAN, allowing the merchant to accept payments even outside the cradle's coverage range. Cordless terminals are suitable for integrated solutions at the checkout via LAN. +- **Portable:** equipped with a 4G SIM, allow the Merchant to accept payments in shop where there is no LAN option or outside. In this case, there is no integration option available and, if required, you must choose a SmartPOS solution. +- **Retail Pin Pad:** devices without printer that are connected via Ethernet to the shop LAN and always require an integration with the cash register. + + + +----------------------- + +--- +title: "Traditional POS | Nexi group developer portal" +source: "https://developer.nexigroup.com/traditionalpos/en-EU/docs/integration-options-this-section-provides-an-overview-of-lan-integration-and-other-basic-integration-options/" +author: +published: +created: 2026-05-27 +description: +tags: + - "clippings" +--- +## Integration options + +## LAN integration + +LAN integration allows the Merchant to activate the POS directly from the ECR, sending a message from the ECR to the IP address of the POS. For new EFTPOS based with Linux OS is required a port over 1024. + +Some terminals have also USB/RS232 ports, but the usage is deprecated, as it might be not available on new products in the future. + +The POS interacts with the Cardholder reading the payment card and send the authorization request to NEXI. + +After the completion of the payment, the POS sends a response message to the ECR in synchronous way. + +Is up to you to define if the POS prints the payment receipts at the end of the transaction or to integrate the payment receipts in the tax receipts printed by ECR. + +## Integration test kit + +The Merchant can request through his commercial contact (i.e. Bank, Nexi or ISV) the supply of a test kit composed by a terminal and a kit of cards, to test the integration made by the Developer. + +If you have no commercial contact, you can ask to be contacted by NEXI filling the form at the following page: [Partner Program](https://www.nexi.it/it/partner-program) + +## Go-live + +The Merchant subscribes the POS and acquiring contract with Nexi through his commercial contact. + +Nexi technical support team contacts the Merchant for checking the compatibility between ECR and POS, and for gathering all POS configuration need for the activation (IP, PORT, etc.). + +You may require the following POS configuration: + +- **ECR Integration and standalone usage** – Payments can be requested by the cash register o manually through the user interface of the POS terminal +- **ECR Integration only** – Payment can be requested only by the ECR When all the information is available, NEXI performs the configuration activity onsite or remotely. + +## Basic integration + +## Terminal Status + +ECR verifies Eft-POS status by [Terminal Status](https://developer.nexigroup.com/traditionalpos/en-EU/docs/terminal-status-request-message-from-ecr/). + +Terminal Status command guarantee the connection between ECR and EFT-POS. + +## Payment + +### Basic payment + +ECR requests a Payment EFT-POS by command Payment, the payment amount is filled. + +EFT-POS will process transaction and response with the transaction Result. For detail see [Payment](https://developer.nexigroup.com/traditionalpos/en-EU/docs/payment/) (command “P” or “X”). + +Below is reported a few information useful to understand the protocol. + +#### Date and time + +**EFT-POS:** + +- is synchronized with Host System +- Displays local data and time +- Via Lan Integration protocol, ECR receives the date and time of the Host System + +**Payment Type:** The response field reports the type of card processed. For historical Reason: + +- Debit Card is used to refer *PagoBANCOMAT* Card +- Credit Card is used to refer International Card + +**Aquirer ID:** The response field reports Acquirer code that process transaction. Warning This Acquirer Id code may be different for each Scheme/Acquirer. + +Below is shown the basic payment flow: + +![Basic Payment Flow](https://images.ctfassets.net/di680may5c0k/6LrqNa2mtkkE69ZuO6A2bV/374e0f1652e0db69d527c8678f0ca22b/Basic_Payment_Flow.jpg?w=2500&h=1303&q=50&fm=webp) + +### Payment with Receipt manage by ECR and Additional TAG + +In this use case, it is required that the receipt is printed on the ECR and not on the POS, and with the execution of a Payment and simultaneous sending of additional TAGs to GT, no Addition TAGs received from GT. + +LAN Integration requires managing of multiple commands. + +The request consists of: + +- instruct the POS on the receipt management mode (command E, management mode 1) +- the Payment function must be started by specifying the amount and advice of presence Additional TAGS (command “P”). +- data relating to Additional Tags (U command). + +The response consists of: + +- Transaction result via 'E' message (protocol document link) +- Receipt via Message/Messages 'S' (protocol document link) + +Below is reported a few information useful to understand the protocol. + +**Receipt** + +- Receipt is formatted by the POS following the requirements of the Payment Schemes. +- The receipt, if requested by ECR, is sent pre-formatted. +- The ECR will print the receipt “as is”. + +**Warning** + +- NEXI can change the format of the receipt data without communication. +- Receipt could be transmitted by more than one ‘S’ message. It is up to ECR to concatenates all the ‘S’ messages. + +In the below example, the receipt is sent through 2 'S' messages: + +![Basic Payment Flow 2 -S- messages](https://images.ctfassets.net/di680may5c0k/5j8yo2Q04mJph8MXluxxrw/e8ca85561839bfccf334ecf96a2ca8d8/Basic_Payment_Flow_In_the_following_example__the_receipt_is_sent_through_2_-S-_messages.jpg?w=2500&h=2424&q=50&fm=webp) + +### Exception Management + +**Lost Payment Response** + +- The exception can rise for different reason, i.e. connection drop, EFT-POS issues, ECR timeout, etc. +- The protocol provides the command “G” that can be used to retrieve last transaction result. + +**Warning** + +- It is not available unique identifier between the pending request with last transaction result. + +**NAK usage** + +- NAK is used to control the reception and retransmission of messages, a retransmission attempt is expected for a maximum of 3 times ([Communication Protocol](https://developer.nexigroup.com/traditionalpos/en-EU/docs/communication-protocol/)) +- NAK is also returned in the case of commands not managed by POS. + +**Reprint Receipt** + +- ECR requests Reprint via command “R”. + +## Reversal Last Transaction + +Eft-Pos allows the cancellation of the last transaction performed. The purpose of the transaction is the management of any operational errors (i.e. incorrect amount entry). + +The functionality requires reading the card data in the same way as the previous transaction. + +ECR requests Reversal via command “S”. + +**Warning** STAN management is recommended. + +## Total and Close Session + +EFT-POS records the totals of the transactions managed; also the Host records the totals. The Local and Remote Total are reset by Close Session operation. + +ECR retrieved Local and Remote Total via command “T” putting in evidence any difference between locale and remote totals. + +ECR requests a Close Session via command “C”. + +## Functions for Hotel and Car rentals + +For hotel and car rental, Nexi has a set of specific functions to allow optimal management of payment processes in compliance with the requirements of the payment schemes and the needs of the Merchant. + +These functionalities are partially available using LAN Integration. + +- **Preauthorization**: requested for the estimated value of the service in order to block the plafond of the Cardholder +- **Incremental Authorization**: requested to block an additional amount during the execution of the contracted service +- **Pre-authorization capture**: requested to debit the Cardholder for the final amount service. +- **Card verification**: requested to validate the card presented by the Cardholder. +- **No-Show (not available via LAN Integration)**: requested to debit the Cardholder for guaranteed reservation. +- **Delayed Charges (not available via LAN Integration)**: requested to debit the Cardholder after the payment of the original service (i.e. for fine o damages in car rental sector). +- **Pre-authorization void (not available via LAN Integration)**: requested to unblock the card plafond. +- **Reversal of last transaction**: requested for preauthorization request and capture. + +Preauthorization requests deliver a unique code that can be used for the following requests (integration, capture, etc.) that can be requested by any POS of the Merchant. + +## Preauthorization + +Using LAN Integration, ECR initiates the preauthorization request with the estimated amount of the service. + +The terminal processes the transaction and gives back the result with some identification data of the transaction. + +In case of positive result, ECR receives the unique code of the preauthorization, instead, in case of negative result, a response code with the description of the error, if available. + +Options and exceptions are the same of the payment. + +For the details, please see command “p” specification in API section. + +## Incremental Authorization + +Using LAN Integration, ECR initiates the Incremental Authorization request using the unique preauthorization code received in the original preauthorization request and the additional estimated amount of the service. + +For the details, please see command “i” specification in API section. + +Options and exceptions are the same of the payment. + +## Preauthorization Capture + +Using LAN Integration, ECR initiates the Incremental Authorization request using the unique preauthorization code received in the original preauthorization request and the final amount of the service. For the details, please see command “c” specification in API section. + +Options and exceptions are the same of the payment. + +## Tokenization + +Tokenization allows to save Cardholder’s payment data, after signing a debit mandate contract proposed by the Merchant, to manage the following use cases: + +- Recurring Payments +- Unscheduled Recurring Payments +- OneClick Payments + +Merchant can activate Tokenization only using LAN Integration with the following transactions: + +- Payment +- Card Verification +- Preauthorization + +Additional Tags must be valued, as described in API specification, by the ECR specifying a unique code that identify the contract signed by the Cardholder. + +For the details, please see command “U” specification in API section. The subsequence transaction to debit the Cardholder are requested via API through XPAY Gateway. + + +------------------------- + +--- +title: "Traditional POS | Nexi group developer portal" +source: "https://developer.nexigroup.com/traditionalpos/en-EU/docs/lan-integration/" +author: +published: +created: 2026-05-27 +description: +tags: + - "clippings" +--- +## LAN Integration + +## Overview + +Traditional POS supports LAN integration. + +In this section, you will find the information for: + +- [Communication Protocol](https://developer.nexigroup.com/traditionalpos/en-EU/docs/communication-protocol/) +- [Application Messages](https://developer.nexigroup.com/traditionalpos/en-EU/docs/terminal-status-request-message-from-ecr/) + + +------------------ + +--- +title: "Traditional POS | Nexi group developer portal" +source: "https://developer.nexigroup.com/traditionalpos/en-EU/docs/communication-protocol/" +author: +published: +created: 2026-05-27 +description: +tags: + - "clippings" +--- +## Communication Protocol + +## Protocol rules and flow + +Communication is always started by the ECR, which sends the transaction request message to the terminal. For payments and refunds, the message includes the transaction amount. + +All messages are exchanged in plain text, and the characters composing the messages are not altered in any way. + +All messages include a LRC control byte. The LRC character is computed by executing an exclusive or any message byte, using as base value 0x7F. + +Both terminal and cash registers validate incoming messages. Validation may fail for one of these reasons: + +- Protocol error (invalid message code, invalid terminal ID) +- Parity error +- LRC error + +If validation fails, a NAK is sent to the ECR. + +Both the terminal and the cash registers repeat the message transmission. It is repeated up to three, on these events: + +- The wait message time-out expiration +- NAK received + +If the function is enabled, the terminal will verify that the terminal ID specified in cash register messages is equal to the one sent to cash register, or 00000000. + +## Application packet format + +The format used for application packets is defined in the table below. These application packets refer to: + +- Payment +- Reversal +- Refund +- Open session +- Close session +- Terminal status +- Send ticket +- Enable/disable ECR print +- Receipt reprint + +This is possible due to all data being composed of application messages ASCII (0 to 127). + +| Pos | Field name | Value | Type | Length (bytes) | +| --- | --- | --- | --- | --- | +| 1 | Start of text | 0x02 | Binary | 1 | +| 2…N | Application message | | Binary | N | +| N+1 | End of Text | 0x03 | Binary | 1 | +| N+2 | LRC | | Binary | 1 | + +All the application-level packets have a physical confirmation message in response from the peer. + +## Procedure progress update packet format + +During the procedures that imply a connection to the host, the terminal can transmit a message containing information about the progress of the current procedure. The messages are 20 characters long so that they can be easily displayed on the ECR display. The progress update messages do not require the physical confirmation message from the cash register. + +| Pos | Field name | Value | Type | Length (bytes) | +| --- | --- | --- | --- | --- | +| 1 | Start of heading | 0x01 | Binary | 1 | +| 2 | Message | | Binary | 20 | +| 22 | End of transmission | 0x04 | Binary | 1 | + +## Confirmation/refusal packet format + +The confirmation/denial messages sent to the peer always have the following formats: + +- **Confirmation message (ACK)** + +| Pos | Field name | Value | Type | Length (bytes) | +| --- | --- | --- | --- | --- | +| 1 | ACK | 0x06 | Binary | 1 | +| 2 | ETX | 0x03 | Binary | 1 | +| 3 | LRC | | Binary | 1 | + +- **Refusal message (NAK)** + +| Pos | Field name | Value | Type | Length (bytes) | +| --- | --- | --- | --- | --- | +| 1 | NAK | 0x15 | Binary | 1 | +| 2 | ETX | 0x03 | Binary | 1 | +| 3 | LRC | | Binary | 1 | + + +--------------- + +--- +title: "Terminal status request message (from ECR) | Traditional POS | Nexi group developer portal" +source: "https://developer.nexigroup.com/traditionalpos/en-EU/docs/terminal-status-request-message-from-ecr/" +author: +published: +created: 2026-05-27 +description: +tags: + - "clippings" +--- +## Terminal status request message (from ECR) + +## Terminal status request message (from ECR) + +| Pos | Length | Type | Content | +| --- | --- | --- | --- | +| 1 | 8 | N | Terminal ID (00000000-99999999) | +| 9 | 1 | A | Reserved – fixed to ‘0’ (0x30) | +| 10 | 1 | A | Message code: ‘s’ (0x73) | + +## POS status response message (from Terminal) + +| Pos | Length | Type | Content | +| --- | --- | --- | --- | +| 1 | 8 | N | Terminal ID (00000000-99999999) | +| 9 | 1 | A | Reserved – fixed to ‘0’ (0x30) | +| 10 | 1 | N | Message code: ‘s’ (0x73) | +| 11 | 10 | N | Reserved – fixed to ‘0’ (0x30) | +| 21 | 10 | N | Terminal date and time in format “DDMMYYhhmm | +| 31 | 1 | N | Terminal status - It depends on the terminals manufacturer. Es: - '0' = Terminal not configured. - '1' = Terminal configured, no DLL. - '2' = Terminal operative (after a DLL). - '3' = Terminal not aligned (first DLL requested). - '4' = KMPB/KPOS key corrupted (first DLL requested). - '5' = DLL solicited by GT pending. - '6' = Remote SW updated request pending. If the terminal ECR connection parameters are not configured, the command will not have response. | +| 32 | N\*8 | AN | Terminal SW release. It depends on the terminal’s manufacturer | + + +-------------------------- +--- +title: "Traditional POS | Nexi group developer portal" +source: "https://developer.nexigroup.com/traditionalpos/en-EU/docs/payment/" +author: +published: +created: 2026-05-27 +description: +tags: + - "clippings" +--- +## Payment + +## Payment request message (from ECR) + +| Pos | Length | Type | Content | +| --- | --- | --- | --- | +| 1 | 8 | N | Terminal ID (00000000-99999999) | +| 9 | 1 | A | Reserved – fixed to ‘0’ (0x30) | +| 10 | 1 | A | Message code: ‘P’ (0x50) | +| 11 | 8 | N | Cash register ID | +| 19 | 1 | N | Presence of message with additional data for the GT | +| 20 | 2 | N | Reserved – fixed to ‘0’ (0x30) | +| 22 | 1 | N | Start transaction when card already present: - ‘0’ = start with card not yet inserted in the terminal - ‘1’ = start with card already inserted in the terminal | +| 23 | 1 | N | Payment type: - ‘0’ = automatic card recognition on terminal with customer selection in case of cobranded cards - ‘1’ = only debit cards - ‘2’ = only credit cards - ‘3’ = other cards | +| 24 | 8 | N | Transaction amount, right aligned, filled with ‘0’ (0x30) on the left. Amount is always considered to be expressed in cents (e.g. 650 = 6,50) | +| 32 | 128 | AN | Text to be print (Code contract) at the end of the payment receipt. The field is always 128 chars long, it’s right aligned, filled with ‘ ‘ (0x20) on the left. The terminal shall only print valid chars, skipping the blanks. | +| 160 | 8 | N | Reserved – fixed to ‘0’ (0x30) | + +## Payment response message without currency exchange data (from Terminal) + +| Pos | Length | Type | Content | +| --- | --- | --- | --- | +| 1 | 8 | N | Terminal ID (00000000-99999999) | +| 9 | 1 | A | Reserved – fixed to ‘0’ (0x30) | +| 10 | 1 | A | Message code: ‘E’ (0x45) | +| 11 | 2 | N | Transaction result: - “00” = OK - “01” = KO - “05” = card not present (valid only if field 22 in request message is ‘1’ = start with card already present - “09” = received unknown Tag from GT | + +### If transaction result is positive (00) + +| Pos | Length | Type | Content | +| --- | --- | --- | --- | +| 13 | 19 | N | Card PAN, right aligned, filled with ‘0’ (0x30) on the left. The PAN is | +| 32 | 3 | A | Transaction type: - “ICC” = ICC card - “MAG” = magnetic stripe card - “MAN” = manual PAN entry - “CLM” = c-less magstripe - “CLI” = c-less ICC | +| 35 | 6 | A | Authorization code received from host | +| 41 | 7 | A | Transaction date and time received from host in DDDHHMM. The day is expressed as number of days since 01/01 of current year. For example, July 29, 15:20 is sent as 2111520. | + +### If transaction result is negative (01) + +| Pos | Length | Type | Content | +| --- | --- | --- | --- | +| 13 | 24 | A | Result description. This field shall report, using only ASCII characters, the reason of the denial. The field is left aligned, filled with blank on the right. | +| 37 | 11 | N | Reserved – fixed to ‘0’ (0x30) | + +### Common to any response + +| Pos | Length | Type | Content | +| --- | --- | --- | --- | +| 48 | 1 | N | Card type: - '1' = Bancomat card - '2' = Credit card - '3' = Other card | +| 49 | 11 | N | Acquirer ID. The field is left aligned, filled with blanks on the right. | +| 60 | 6 | N | STAN. Transaction sequence number. | +| 66 | 6 | N | ID online. Online operation progressive number. | + +## Payment response message with currency exchange data (from Terminal) + +| Pos | Length | Type | Content | +| --- | --- | --- | --- | +| 1 | 8 | N | Terminal ID (00000000-99999999) | +| 9 | 1 | A | Reserved – fixed to ‘0’ (0x30) | +| 10 | 1 | A | Message code: ‘V’ (0x56) | +| 11 | 2 | N | Transaction result: - “00” = OK - “01” = KO - “05” = card not present (valid only if field 22 in request message is ‘1’ = start with card already present - “09” = received unknown Tag from GT | + +### If transaction result is positive (00) + +| Pos | Length | Type | Content | +| --- | --- | --- | --- | +| 13 | 19 | N | Card PAN, right aligned, filled with ‘0’ (0x30) on the left. The PAN is | +| 32 | 3 | A | Transaction type: - “ICC” = ICC card - “MAG” = magnetic stripe card - “MAN” = manual PAN entry - “CLM” = c-less magstripe - “CLI” = c-less ICC | +| 35 | 6 | A | Authorization code received from host | +| 41 | 7 | A | Transaction date and time received from host in DDDHHMM. The day is expressed as number of days since 01/01 of current year. For example, July 29, 15:20 is sent as 2111520. | + +### If transaction result is negative (01) + +| Pos | Length | Type | Content | +| --- | --- | --- | --- | +| 13 | 24 | A | Result description. This field shall report, using only ASCII characters, the reason of the denial. The field is left aligned, filled with blank on the right. | +| 37 | 11 | N | Reserved – fixed to ‘0’ (0x30) | + +### Common to any response + +| Pos | Length | Type | Content | +| --- | --- | --- | --- | +| 48 | 1 | N | Card type: - ‘1’ = Bancomat card - ‘2’ = Credit card - '3' = Other card | +| 49 | 11 | N | Acquirer ID. The field is left aligned, filled with blanks on the right. | +| 60 | 6 | N | STAN. Transaction sequence number. | +| 66 | 6 | N | ID online. Online operation progressive number. | +| 72 | 3 | N | Action code related to current payment operation. | +| 75 | 8 | N | Transaction amount original, right aligned, with ‘0’ filling on the left. Amount is always considered to be expressed in cents (e.g. 650 = 6,50) | +| 83 | 1 | N | Currency exchange transaction flag. The value is set to ‘1’ only when the transaction has been performed with a currency different from terminal currency. **Important**: if this value is ‘0’, following fields are not meaningful. | +| 84 | 8 | N | Exchange rate, right aligned with 4 decimal digits. – Received by the host "DCC". | +| 92 | 3 | A | Alphanumeric currency code of transaction currency, as received by the host "DCC". | +| 95 | 12 | N | Transaction amount in the transaction currency, right aligned with ‘0’ (0x30) filler on the left - Received by the host “DCC”. | +| 107 | 1 | N | Precision (number of decimals) of transaction currency. | +| 108 | 10 | N | Reserved – fixed to ‘0’ (0x30) | +-------------------------- +--- +title: "Payment with extended result | Traditional POS | Nexi group developer portal" +source: "https://developer.nexigroup.com/traditionalpos/en-EU/docs/payment-with-extended-result/" +author: +published: +created: 2026-05-27 +description: +tags: + - "clippings" +--- +## Payment with extended result + +## Extended payment request message (from ECR) + +| Pos | Length | Type | Content | +| --- | --- | --- | --- | +| 1 | 8 | N | Terminal ID (00000000-99999999) | +| 9 | 1 | A | Reserved – fixed to ‘0’ (0x30) | +| 10 | 1 | A | Message code: ‘X’ (0x58) | +| 11 | 8 | N | Cash register ID | +| 19 | 1 | N | Presence of message with additional data for the GT: | +| 20 | 2 | N | Reserved – fixed to ‘0’ (0x30) | +| 22 | 1 | N | Start transaction when card already present: - ‘0’ = start with card not yet inserted in the terminal - ‘1’ = start with card already inserted in the terminal | +| 23 | 1 | N | Payment type: - ‘0’ = automatic card recognition on terminal with customer selection in case of cobranded cards - ‘1’ = only debit cards - ‘2’ = only credit cards - ‘3’ = other cards | +| 24 | 8 | N | Transaction amount, right aligned, filled with ‘0’ (0x30) on the left. Amount is always considered to be expressed in cents (e.g. 650 = 6,50) | +| 32 | 128 | AN | Text to be print (Code contract) at the end of the payment receipt. The field is always 128 chars long, it’s right aligned, filled with ‘ ‘ (0x20) on the left. The terminal shall only print valid chars, skipping the blanks. | +| 160 | 8 | N | Reserved – fixed to ‘0’ (0x30) | + +## Extended payment response message (from Terminal) + +| Pos | Length | Type | Content | +| --- | --- | --- | --- | +| 1 | 8 | N | Terminal ID (00000000-99999999) | +| 9 | 1 | A | Reserved – fixed to ‘0’ (0x30) | +| 10 | 1 | A | Message code: ‘E’ (0x45) | +| 11 | 2 | N | Transaction result: - “00” = OK - “01” = KO - “05” = card not present (valid only if field 22 in request message is ‘1’ = start with card already present - “09” = received unknown Tag from GT | + +### If transaction result is positive (00) + +| Pos | Length | Type | Content | +| --- | --- | --- | --- | +| 13 | 19 | N | Card PAN, right aligned, filled with ‘0’ (0x30) on the left. The PAN is | +| 32 | 3 | A | Transaction type: - “ICC” = ICC card - “MAG” = magnetic stripe card - “MAN” = manual PAN entry - “CLM” = c-less magstripe - “CLI” = c-less ICC | +| 35 | 6 | A | Authorization code received from host | +| 41 | 7 | A | Transaction date and time received from host in DDDHHMM. The day is expressed as number of days since 01/01 of current year. For example, July 29, 15:20 is sent as 2111520. | + +### If transaction result is negative (01) + +| Pos | Length | Type | Content | +| --- | --- | --- | --- | +| 13 | 24 | A | Result description. This field shall report, using only ASCII characters, the reason of the denial. The field is left aligned, filled with blank on the right. | +| 37 | 11 | N | Reserved – fixed to ‘0’ (0x30) | + +### Common to any response + +| Pos | Length | Type | Content | +| --- | --- | --- | --- | +| 48 | 1 | N | Card type: | +| 49 | 11 | N | Acquirer ID. The field is left aligned, filled with blanks on the right. | +| 60 | 6 | N | STAN. Transaction sequence number. | +| 66 | 6 | N | ID online. Online operation progressive number. | +| 72 | 3 | N | Action code related to current payment operation. | +| 75 | 8 | N | Transaction amount received by host, right aligned, with ‘0’ filling on the left. Amount is always considered to be expressed in cents (e.g. 650 = 6,50) | +| 83 | 10 | N | Reserved – fixed to ‘0’ (0x30) | +-------------------------- +--- +title: "Traditional POS | Nexi group developer portal" +source: "https://developer.nexigroup.com/traditionalpos/en-EU/docs/reversal/" +author: +published: +created: 2026-05-27 +description: +tags: + - "clippings" +--- +## Reversal + +## Reversal request message (from ECR) + +| Pos | Length | Type | Content | +| --- | --- | --- | --- | +| 1 | 8 | N | Terminal ID (00000000-99999999) | +| 9 | 1 | A | Reserved – fixed to ‘0’ (0x30) | +| 10 | 1 | A | Message code: ‘S’ (0x53) | +| 11 | 8 | N | Cash register ID | +| 19 | 6 | N | STAN of the transaction to be reversed. If the field is filled with ‘0’ (0x30) no check is performed. If the value is different from “000000”, the terminal performs the reversal only if the value is equal to the last payment transaction STAN. | +| 25 | 1 | N | Presence of message with additional data for the GT: | +| 26 | 1 | N | Reserved – fixed to ‘0’ (0x30) (Terminal requires the same card to perform the reversal) | + +## Reversal response message (from Terminal) + +| Pos | Length | Type | Content | +| --- | --- | --- | --- | +| 1 | 8 | N | Terminal ID (00000000-99999999) | +| 9 | 1 | A | Reserved – fixed to ‘0’ (0x30) | +| 10 | 1 | A | Message code: ‘E’ (0x45) | +| 11 | 2 | N | Transaction result: - “00” = OK - “01” = KO - “09” = received unknown Tag from GT | + +### If transaction result is positive (00) + +| Pos | Length | Type | Content | +| --- | --- | --- | --- | +| 13 | 19 | N | Card PAN, right aligned, filled with ‘0’ (0x30) on the left. The PAN is | +| 32 | 3 | A | Transaction type: - “ICC” = ICC card - “MAG” = magnetic stripe card - “MAN” = manual PAN entry - “CLM” = c-less magstripe - “CLI” = c-less ICC | +| 35 | 6 | A | Reserved – fixed to ‘0’ (0x30) | +| 41 | 7 | A | Transaction date and time received from host in DDDHHMM. The day is expressed as number of days since 01/01 of current year. For example, July 29, 15:20 is sent as 2111520. | + +### If transaction result is negative (01) + +| Pos | Length | Type | Content | +| --- | --- | --- | --- | +| 13 | 24 | A | Result description. This field shall report, using only ASCII characters, the reason of the denial. The field is left aligned, filled with blank on the right. | +| 37 | 11 | N | Reserved – fixed to ‘0’ (0x30) | + +### Common to any response + +| Pos | Length | Type | Content | +| --- | --- | --- | --- | +| 48 | 1 | N | Card type: | +| 49 | 11 | N | Acquirer ID. The field is left aligned, filled with blanks on the right. | +| 60 | 6 | N | STAN. Transaction sequence number. | +| 66 | 6 | N | ID online. Online operation progressive number. | +| 72 | 3 | N | Action code related to current payment operation. | +| 75 | 10 | N | Reserved – fixed to ‘0’ (0x30) | +-------------------------- +--- +title: "Traditional POS | Nexi group developer portal" +source: "https://developer.nexigroup.com/traditionalpos/en-EU/docs/pre-authorization-request/" +author: +published: +created: 2026-05-27 +description: +tags: + - "clippings" +--- +## Pre-Authorization request + +## Pre-authorization request message (from ECR) + +| Pos | Length | Type | Content | +| --- | --- | --- | --- | +| 1 | 8 | N | Terminal ID (00000000-99999999) | +| 9 | 1 | A | Reserved – fixed to ‘0’ (0x30) | +| 10 | 1 | A | Message code: ‘p’ (0x70) | +| 11 | 8 | N | Cash register ID | +| 19 | 1 | N | Presence of message with additional data for the GT: | +| 20 | 2 | N | Reserved – fixed to ‘0’ (0x30) | +| 22 | 1 | N | Start transaction when card already present: - ‘0’ = start with card not yet inserted in the terminal - ‘1’ = start with card already inserted in the terminal | +| 23 | 1 | N | Payment type: - ‘0’ = automatic card recognition on terminal with customer selection in case of cobranded cards - ‘1’ = only debit cards - ‘2’ = only credit cards - ‘3’ = other cards | +| 24 | 8 | N | Pre-authorization transaction amount, right aligned, filled with ‘0’ (0x30) on the left. Amount is always considered to be expressed in cents (e.g. 650 = 6,50) | +| 32 | 128 | AN | Text to be print (Code contract)at the end of the payment receipt. The field is always 128 chars long, it’s right aligned, filled with ‘ ‘ (0x20) on the left. The terminal shall only print valid chars, skipping the blanks. | +| 160 | 8 | N | Reserved – fixed to ‘0’ (0x30) | + +## Pre-authorization response message (from Terminal) + +| Pos | Length | Type | Content | +| --- | --- | --- | --- | +| 1 | 8 | N | Terminal ID (00000000-99999999) | +| 9 | 1 | A | Reserved – fixed to ‘0’ (0x30) | +| 10 | 1 | A | Message code: ‘e’ (0x65) | +| 11 | 2 | N | Transaction result: - “00” = OK - “01” = KO - “05” = card not present (valid only if field 22 in request message is ‘1’ = start with card already present - “09” = received unknown Tag from GT | + +### If transaction result is positive (00) + +| Pos | Length | Type | Content | +| --- | --- | --- | --- | +| 13 | 19 | N | Card PAN, right aligned, filled with ‘0’ (0x30) on the left. The PAN is | +| 32 | 3 | A | Transaction type: - “ICC” = ICC card - “MAG” = magnetic stripe card - “MAN” = manual PAN entry - “CLM” = c-less magstripe - “CLI” = c-less ICC | +| 35 | 6 | A | Authorization code received from host | +| 41 | 8 | N | Amount pre-authorized by the Host, right-aligned, with filler 0 (30 hex) on the left. The amount is always understood in euro cents (e.g. 6,50€ = 650). | +| 49 | 9 | N | Unique identifier of the pre-authorization received from Host and printed on the pre-authorization receipt issued by the terminal (Preauthorization Code). | +| 58 | 3 | N | Action Code of transaction received from Host. | +| 61 | 7 | N | The temporal parameters of transaction are received from Host in DDDHHMM format. The day is expressed as a sequential number from Jan 1st = 1 to Dec 31st = 365/366 of the current year. | +| 68 | 3 | N | Reserved – fixed to ‘0’ (0x30) | + +### If transaction result is negative (01) + +| Pos | Length | Type | Content | +| --- | --- | --- | --- | +| 13 | 24 | A | Result description. This field shall report, using only ASCII characters, the reason of the denial. The field is left aligned, filled with blank on the right. | +| 37 | 3 | N | Action Code of transaction received from Host. | +| 40 | 31 | N | Reserved – fixed to ‘0’ (0x30) | + +### Common to any response + +| Pos | Length | Type | Content | +| --- | --- | --- | --- | +| 48 | 1 | N | Card type: | +| 72 | 11 | N | Acquirer ID. The field is left aligned, filled with blanks on the right. | +| 83 | 6 | N | STAN. Transaction sequence number. | +| 89 | 6 | N | ID online. Online operation progressive number. | +| 95 | 12 | N | Reserved – fixed to ‘0’ (0x30) | +-------------------------- +--- +title: "Traditional POS | Nexi group developer portal" +source: "https://developer.nexigroup.com/traditionalpos/en-EU/docs/incremental-authorization-transaction/" +author: +published: +created: 2026-05-27 +description: +tags: + - "clippings" +--- +## Incremental authorization transaction + +## Incremental authorization request message (from ECR) + +| Pos | Length | Type | Content | +| --- | --- | --- | --- | +| 1 | 8 | N | Terminal ID (00000000-99999999) | +| 9 | 1 | A | Reserved – fixed to ‘0’ (0x30) | +| 10 | 1 | A | Message code: ‘i’ (0x69) | +| 11 | 8 | N | Cash register ID | +| 19 | 1 | N | Presence of message with additional data for the GT: | +| 20 | 4 | N | Reserved – fixed to ‘0’ (0x30) | +| 24 | 8 | N | Authorization transaction amount, right aligned, filled with ‘0’ (0x30) on the left. Amount is always considered to be expressed in cents (e.g. 650 = 6,50) | +| 32 | 128 | AN | Text to be print (Code contract) at the end of the payment receipt. The field is always 128 chars long, it’s right aligned, filled with ‘ ‘ (0x20) on the left. The terminal shall only print valid chars, skipping the blanks. | +| 160 | 9 | N | Unique identifier of the pre-authorization received from Host and printed on the pre-authorization receipt issued by the terminal (Original Preathorization Code) | +| 169 | 8 | N | Reserved – fixed to ‘0’ (0x30) | + +## Incremental authorization response message (from Terminal) + +| Pos | Length | Type | Content | +| --- | --- | --- | --- | +| 1 | 8 | N | Terminal ID (00000000-99999999) | +| 9 | 1 | A | Reserved – fixed to ‘0’ (0x30) | +| 10 | 1 | A | Message code: ‘i’ (0x69) | +| 11 | 2 | N | Transaction result: - “00” = OK - “01” = KO - “09” = received unknown tag from GT | +| 13 | 19 | N | Card PAN, right aligned, filled with ‘0’ (0x30) on the left. The PAN is sent following PAN truncation rules. | +| 32 | 3 | A | Transaction type: | +| 35 | 11 | N | Acquirer ID. The field is left-aligned with filler Blank on the right. | +| 46 | 6 | A | Authorization code received from host | +| 52 | 6 | N | STAN. Transaction sequence number. | +| 58 | 6 | N | ID online. Online operation progressive number. | +| 64 | 7 | A | Transaction date and time received from host in DDDHHMM. The day is expressed as number of days since 01/01 of current year. For example, July 29, 15:20 is sent as 2111520. | +| 71 | 3 | N | Action Code of transaction received from Host. | +| 74 | 2 | N | Reserved – fixed to ‘0’ (0x30) | +-------------------------- +--- +title: "Traditional POS | Nexi group developer portal" +source: "https://developer.nexigroup.com/traditionalpos/en-EU/docs/pre-authorization-closure/" +author: +published: +created: 2026-05-27 +description: +tags: + - "clippings" +--- +## Pre-authorization closure + +## Pre-authorization closure request message (from ECR) + +| Pos | Length | Type | Content | +| --- | --- | --- | --- | +| 1 | 8 | N | Terminal ID (00000000-99999999) | +| 9 | 1 | A | Reserved – fixed to ‘0’ (0x30) | +| 10 | 1 | A | Message code: ‘i’ (0x63) | +| 11 | 8 | N | Cash register ID | +| 19 | 1 | N | Presence of message with additional data for the GT: | +| 20 | 4 | N | Reserved – fixed to ‘0’ (0x30) | +| 24 | 8 | N | Authorization transaction amount, right aligned, filled with ‘0’ (0x30) on the left. Amount is always considered to be expressed in cents (e.g. 650 = 6,50) | +| 32 | 128 | AN | Text to be print (Code contract) at the end of the payment receipt. The field is always 128 chars long, it’s right aligned, filled with ‘ ‘ (0x20) on the left. The terminal shall only print valid chars, skipping the blanks. | +| 160 | 9 | N | Unique identifier of the pre-authorization received from Host and printed on the pre-authorization receipt issued by the terminal (Original Preathorization Code) | +| 169 | 8 | N | Reserved – fixed to ‘0’ (0x30) | + +## Pre-authorization closure response message without currency exchange data (from Terminal) + +| Pos | Length | Type | Content | +| --- | --- | --- | --- | +| 1 | 8 | N | Terminal ID (00000000-99999999) | +| 9 | 1 | A | Reserved – fixed to ‘0’ (0x30) | +| 10 | 1 | A | Message code: ‘c’ (0x63) | +| 11 | 2 | N | Transaction result: - “00” = OK - “01” = KO - “09” = received unknown Tag from GT | + +### If transaction result is positive (00) + +| Pos | Length | Type | Content | +| --- | --- | --- | --- | +| 13 | 19 | N | Card PAN, right aligned, filled with ‘0’ (0x30) on the left. The PAN is | +| 32 | 3 | A | Transaction type: - “ICC” = ICC card - “MAG” = magnetic stripe card - “MAN” = manual PAN entry - “CLM” = c-less magstripe - “CLI” = c-less ICC | +| 35 | 6 | A | Authorization code received from host | +| 41 | 7 | A | Transaction date and time received from host in DDDHHMM. The day is expressed as number of days since 01/01 of current year. For example, July 29, 15:20 is sent as 2111520. | + +### If transaction result is negative (01) + +| Pos | Length | Type | Content | +| --- | --- | --- | --- | +| 13 | 24 | A | Result description. This field shall report, using only ASCII characters, the reason of the denial. The field is left aligned, filled with blank on the right. | +| 37 | 11 | N | Reserved – fixed to ‘0’ (0x30) | + +### Common to any response + +| Pos | Length | Type | Content | +| --- | --- | --- | --- | +| 48 | 1 | N | Card type: - '1' = Bancomat card - '2' = Credit card - '3' = Other card | +| 49 | 11 | N | Acquirer ID. The field is left aligned, filled with blanks on the right. | +| 60 | 6 | N | STAN. Transaction sequence number. | +| 66 | 6 | N | ID online. Online operation progressive number. | +| 72 | 3 | N | Action Code relating to the current payment operation. The field, if present, applies either to the positive and negative transactions. | + +## Pre-authorization closure response message with currency exchange data (from Terminal) + +| Pos | Length | Type | Content | +| --- | --- | --- | --- | +| 1 | 8 | N | Terminal ID (00000000-99999999) | +| 9 | 1 | A | Reserved – fixed to ‘0’ (0x30) | +| 10 | 1 | A | Message code: ‘v’ (0x76) | +| 11 | 2 | N | Transaction result: - “00” = OK - “01” = KO - “09” = received unknown Tag from GT | + +### If transaction result is positive (00) + +| Pos | Length | Type | Content | +| --- | --- | --- | --- | +| 13 | 19 | N | Card PAN, right aligned, filled with ‘0’ (0x30) on the left. The PAN is | +| 32 | 3 | A | Transaction type: - “ICC” = ICC card - “MAG” = magnetic stripe card - “MAN” = manual PAN entry - “CLM” = c-less magstripe - “CLI” = c-less ICC | +| 35 | 6 | A | Authorization code received from host | +| 41 | 7 | A | Transaction date and time received from host in DDDHHMM. The day is expressed as number of days since 01/01 of current year. For example, July 29, 15:20 is sent as 2111520. | + +### If transaction result is negative (01) + +| Pos | Length | Type | Content | +| --- | --- | --- | --- | +| 13 | 24 | A | Result description. This field shall report, using only ASCII characters, the reason of the denial. The field is left aligned, filled with blank on the right. | +| 37 | 11 | N | Reserved – fixed to ‘0’ (0x30) | + +### Common to any response + +| Pos | Length | Type | Content | +| --- | --- | --- | --- | +| 48 | 1 | N | Card type: - '1' = Bancomat card - '2' = Credit card - '3' = Other card | +| 49 | 11 | N | Acquirer ID. The field is left aligned, filled with blanks on the right. | +| 60 | 6 | N | STAN. Transaction sequence number. | +| 66 | 6 | N | ID online. Online operation progressive number. | +| 72 | 3 | N | Action Code relating to the current payment operation. The field, if present, applies either to the positive and negative transactions. | +| 75 | 1 | N | Currency exchange transaction flag. The value is set to ‘1’ only when the transaction has been performed with a currency different from terminal currency. **Important**: if this value is ‘0’, following fields are not meaningful. | +| 76 | 8 | N | Exchange rate, right aligned with 4 decimal digits. – Received by the host “DCC”. | +| 84 | 3 | A | Alphanumeric currency code of transaction currency, as received by the host. | +| 87 | 12 | N | Transaction amount in the transaction currency, right aligned with ‘0’ (0x30) filler on the left - Received by the host “DCC”. | +| 99 | 1 | N | Precision (number of decimals) of transaction currency. | +| 100 | 10 | N | Reserved – fixed to ‘0’ (0x30) | +-------------------------- +--- +title: "Traditional POS | Nexi group developer portal" +source: "https://developer.nexigroup.com/traditionalpos/en-EU/docs/card-verification/" +author: +published: +created: 2026-05-27 +description: +tags: + - "clippings" +--- +## Card verification + +## Card Verification request message (from ECR) + +| Pos | Length | Type | Content | +| --- | --- | --- | --- | +| 1 | 8 | N | Terminal ID (00000000-99999999) | +| 9 | 1 | A | Reserved – fixed to ‘0’ (0x30) | +| 10 | 1 | A | Message code: ‘H’ (0x48) | +| 11 | 8 | N | Cash register ID | +| 19 | 1 | N | Presence of message with additional data for the GT: | +| 20 | 2 | N | Reserved – fixed to ‘0’ (0x30) | +| 22 | 1 | N | ‘0’ = standard card verification | +| 23 | 1 | N | Payment type: - ‘0’ = automatic card recognition on terminal with customer selection in case of cobranded cards - ‘1’ = only debit cards - ‘2’ = only credit cards - ‘3’ = other cards | +| 24 | 16 | N | Reserved – fixed to ‘0’ (0x30) | + +## Card Verification response message (from Terminal) + +| Pos | Length | Type | Content | +| --- | --- | --- | --- | +| 1 | 8 | N | Terminal ID (00000000-99999999) | +| 9 | 1 | A | Reserved – fixed to ‘0’ (0x30) | +| 10 | 1 | A | Message code: ‘E’ (0x45) | +| 11 | 2 | N | Transaction result: - “00” = OK - “01” = KO - “09” = received unknown Tag from GT | + +### If transaction result is positive (00) + +| Pos | Length | Type | Content | +| --- | --- | --- | --- | +| 13 | 19 | N | Card PAN, right aligned, filled with ‘0’ (0x30) on the left. The PAN is | +| 32 | 3 | A | Transaction type: - “ICC” = ICC card - “MAG” = magnetic stripe card - “MAN” = manual PAN entry - “CLM” = c-less magstripe - “CLI” = c-less ICC | +| 35 | 6 | A | Authorization code received from host | +| 41 | 7 | A | Transaction date and time received from host in DDDHHMM. The day is expressed as number of days since 01/01 of current year. For example, July 29, 15:20 is sent as 2111520. | + +### If transaction result is negative (01) + +| Pos | Length | Type | Content | +| --- | --- | --- | --- | +| 13 | 24 | A | Result description. This field shall report, using only ASCII characters, the reason of the denial. The field is left aligned, filled with blank on the right. | +| 37 | 3 | N | Action code related to current payment operation. | +| 40 | 8 | N | Reserved – fixed to ‘0’ (0x30) | + +### Common to any response + +| Pos | Length | Type | Content | +| --- | --- | --- | --- | +| 48 | 1 | N | Card type: | +| 49 | 11 | N | Acquirer ID. The field is left aligned, filled with blanks on the right. | +| 60 | 6 | N | STAN. Transaction sequence number. | +| 66 | 6 | N | ID online. Online operation progressive number. | +-------------------------- +--- +title: "Additional data to or from host | Traditional POS | Nexi group developer portal" +source: "https://developer.nexigroup.com/traditionalpos/en-EU/docs/additional-data-to-or-from-host/" +author: +published: +created: 2026-05-27 +description: +tags: + - "clippings" +--- +## Additional data to or from host + +## Overview + +Additional tags are used to send data in the authorization request that are returned in the settlement logs to the Merchant or are used by NEXI to activate specific services, like tokenization. + +The formatting of these tags is different per Bank: + +**Intesa San Paolo** + +- Data can be inserted in the tag with index 5. +- The maximum length of the tag is 100 bytes. + +**Other Banks** + +- Data can be inserted in a maximum of 4 tags with index from 1 to 5. +- The maximum length of concatenated data is 100 bytes (not considering control characters and separators). +- The maximum length of tags from 1 to 4 is 30 bytes +- The maximum length of tag 5 is 100 bytes + +The ADDITIONAL DATA FOR GT application message is only expected from the POS terminal when, in the single procedure activation commands, the relevant flag is found (“Presence of message with additional data for the GT”). + +There are two fields in this message that inform the terminal whether it should send the “additional data from GT message” after the procedure result message. The message with additional data is the only message that contains fields with variable length, indicated with the letter **V** in the **LENGTH** column in the table below. + +## Additional data for GT delivery message (from ECR) + +| Pos | Length | Type | Content | +| --- | --- | --- | --- | +| 1 | 8 | N | Terminal ID (00000000-99999999) | +| 9 | 1 | A | Reserved – fixed to ‘0’ (0x30) | +| 10 | 1 | A | Message code: ‘U’ (0x55) | +| 11 | 6 | N | Payment Type: - “0” => Standard payment | +| 17 | 2 | N | ISO field number. Indicates the ISO-8583 field where data from GT that must be sent to ECR is found. This field is “00” by default and indicates that there is not data from GT to return to ECR. A value other than “00” alerts the terminal of: - data from GT retrieval coordinates - obligation to send additional data from GT message **CURRENTLY THE FIELD IS SET AT “62”** | +| 19 | 8 | A | TAG number. Indicates the TAG where data from GT that must be sent to ECR is found. This field is only significant if the previous field is not “00”. The field is flush left, blank justified. TAG from GT length is fixed at 255. **CURRENTLY THE FIELD IS FIXED AT ‘DF8D01’** | +| 27 | 1 | N | Reserved (fixed at “0” 30 hex) | +| 28 | 4 | N | Exclusive TAG index with additional data to be sent to GT. The field is considered as a byte map for which each byte corresponds to a single index. “0” corresponds to no TAG to be sent to GT. | +| 32 | 5 | N | Reserved (fixed at “0” 30 hex) | +| 37 | 100-V | A | Privative TAG content. Max 100 characters - Min 1 character. The field is always closed by an end-of-field character 01B hex | + +The “Exclusive TAG content” field can be repeated for a maximum of 4 times. + +## Additional data from GT result message (from Terminal) + +The additional data from GT message is only sent by the POS terminal if the ISO number field in the "Message with additional data for GT" is not "00". + +| Pos | Length | Type | Content | +| --- | --- | --- | --- | +| 1 | 8 | N | Terminal ID (00000000-99999999) | +| 9 | 1 | A | Reserved – fixed to ‘0’ (0x30) | +| 10 | 1 | A | Message code: ‘U’ (0x55) | +| 11 | 6 | N | Reserved (fixed at “0” 30 hex) | +| 17 | 3 | N | Data from GT field length. Indicates the length of the next field | +| 20 | 255-V | A | Additional data from GT that must be sent to ECR | +| 20+n | 10 | N | Reserved (fixed at “0” 30 hex) | + +## Tokenization Request + +The transaction requested to activate tokenization via LAN Integration must include Additional Data, in the format requested for the service, with the following information: + +- Type of Tokenization service requested (Recurring, Unscheduled, One Click) +- Contract code: the code has to be unique at Merchant level and is alphanumeric 18 characters long. + +### Additional TAGs - Intesa San Paolo + +| Length | Type | Content | +| --- | --- | --- | +| 4 | An | Identification code of the service: - “0COF” – Unscheduled Recurring and One Click Payments - “0REC” – Recurring Payments | +| 4 | An | Fixed value “0TRK” | +| Ans..18 | An | Unique contract code | +| 1 | Ans 1 | “\|” (pipe) | +| 4 | An | Service code “Labelling Omnichannel” Fixed code “0FNZ03” | + +Example TAG 5 mapping for Unscheduled Recurring and One Click Payments with contract code 1666354841608 Es: 0COF0TRK1666354841608|0FNZ03 + +### TAGs – Other Banks + +| Index (TAG) | Position (Byte) | Length | Type | Content | +| --- | --- | --- | --- | --- | +| 1 | 1 | 3 | A/N | Fixed value “BTD” | +| | 4 | Variabile (max 18 bytes) | A/N | Unique contract code | +| 5 | 1 | 4 | A/N | Identification code of the service: - “0COF” – Unscheduled Recurring and One Click Payments - “0REC” – Recurring Payments | +| | 5 | 4 | A/N | Fisso a “0FNZ” | +| | 9 | 2 | A/N | Action: “03” – new contract creation | + +Example TAGs mapping for Unscheduled Recurring and One Click Payments with contract code *9297022f-75de-48*. + +| Index (TAG) | Position (Byte) | Length | Type | Content | +| --- | --- | --- | --- | --- | +| 1 | 1 | 3 | A/N | BTD | +| | 4 | Variable | A/N | 9297022f-75de-48 | +| 5 | 1 | 4 | A/N | 0COF | +| 5 | 5 | 4 | A/N | 0FNZ | +| 5 | 9 | 2 | A/N | 03 | +-------------------------- +--- +title: "Traditional POS | Nexi group developer portal" +source: "https://developer.nexigroup.com/traditionalpos/en-EU/docs/close-session/" +author: +published: +created: 2026-05-27 +description: +tags: + - "clippings" +--- +## Close session + +## Close Session request message (from ECR) + +| Pos | Length | Type | Content | +| --- | --- | --- | --- | +| 1 | 8 | N | Terminal ID (00000000-99999999) | +| 9 | 1 | A | Reserved – fixed to ‘0’ (0x30) | +| 10 | 1 | A | Message code: ‘C’ (0x43) | +| 11 | 8 | N | Cash register ID | +| 19 | 1 | N | Presence of message with additional data for the GT: | +| 20 | 7 | N | Reserved – fixed to ‘0’ (0x30) | + +## Close Session response message (from Terminal) + +| Pos | Length | Type | Content | +| --- | --- | --- | --- | +| 1 | 8 | N | Terminal ID (00000000-99999999) | +| 9 | 1 | A | Reserved – fixed to ‘0’ (0x30) | +| 10 | 1 | A | Message code: ‘C’ (0x43) | +| 11 | 2 | N | Transaction result: - “00” = OK - “01” = KO - “09” = received unknown tag from GT | + +### If transaction result is positive (00) + +| Pos | Length | Type | Content | +| --- | --- | --- | --- | +| 13 | 16 | N | EFT-POS Total amount, right aligned, filled with ‘0’ (0x30) on the left. Amount is always considered to be expressed in cents (e.g. 650 = 6,50) | +| 29 | 16 | N | Host Total amount, right aligned, filled with ‘0’ (0x30) on the left. Amount is always considered to be expressed in cents (e.g. 650 = 6,50) | + +### If transaction result is negative (01) + +| Pos | Length | Type | Content | +| --- | --- | --- | --- | +| 13 | 19 | A | Result description. This field shall report, using only ASCII characters, the reason of the denial. The field is left aligned, filled with blank on the right. | +| 32 | 3 | N | Action code related to current payment operation | +| 35 | 10 | N | Reserved – fixed to ‘0’ (0x30) | +-------------------------- +--- +title: "Traditional POS | Nexi group developer portal" +source: "https://developer.nexigroup.com/traditionalpos/en-EU/docs/terminal-totals/" +author: +published: +created: 2026-05-27 +description: +tags: + - "clippings" +--- +## Terminal totals + +## Terminal Totals request message (from ECR) + +| Pos | Length | Type | Content | +| --- | --- | --- | --- | +| 1 | 8 | N | Terminal ID (00000000-99999999) | +| 9 | 1 | A | Reserved – fixed to ‘0’ (0x30) | +| 10 | 1 | A | Message code: ‘T’ (0x54) | +| 11 | 8 | N | Cash register ID | +| 19 | 1 | N | Presence of message with additional data for the GT: | +| 20 | 7 | N | Reserved – fixed to ‘0’ (0x30) | + +## Terminal Totals response message (from Terminal) + +| Pos | Length | Type | Content | +| --- | --- | --- | --- | +| 1 | 8 | N | Terminal ID (00000000-99999999) | +| 9 | 1 | A | Reserved – fixed to ‘0’ (0x30) | +| 10 | 1 | A | Message code: ‘T’ (0x54) | +| 11 | 2 | N | Transaction result: | +| 13 | 16 | N | EFT-POS Total amount, right aligned, filled with ‘0’ (0x30) on the left. Amount is always considered to be expressed in cents (e.g. 650 = 6,50) | +| 29 | 6 | A | Reserved – fixed to ‘0’ (0x30) | +-------------------------- +--- +title: "Traditional POS | Nexi group developer portal" +source: "https://developer.nexigroup.com/traditionalpos/en-EU/docs/send-last-result/" +author: +published: +created: 2026-05-27 +description: +tags: + - "clippings" +--- +## Send last result + +## Overview + +Whenever the POS terminal runs one of the following transactions, commanded by the remote device: Payment (all types), Offset, Credit, Pre-authorization (request, integration, and closure) it saves the transaction result message sent by the remote device in its static memory. + +If saved, it includes the “additional data from GT result” message. When the remote device wants to retrieve these results, it sends the terminal this command. + +The terminal does not run any sequence check and always keeps this message even if any other transaction was run that does not require the result message to be saved (example, DLL, totals, etc.). + +## Receipt reprint request message (from ECR) + +| Pos | Length | Type | Content | +| --- | --- | --- | --- | +| 1 | 8 | N | Terminal ID (00000000-99999999) | +| 9 | 1 | A | Reserved – fixed to ‘0’ (0x30) | +| 10 | 1 | A | Message code: ‘G’ (0x47) | +| 11 | 8 | N | Cash register ID | +| 19 | 1 | N | Presence of message with additional data for the GT: | +| 20 | 3 | N | Reserved (fixed at “0” 30 hex) | + +## Last result request feedback message (from ECR) + +The feedback message is exactly the same as the last RESULT message saved during the foreseen procedures. If required by the command message, the additional data from GT message is also sent. + +The sequence is the same as a normal command result. +-------------------------- +--- +title: "Enable and disable printing receipt on ECR | Traditional POS | Nexi group developer portal" +source: "https://developer.nexigroup.com/traditionalpos/en-EU/docs/enable-and-disable-printing-receipt-on-ecr/" +author: +published: +created: 2026-05-27 +description: +tags: + - "clippings" +--- +## Enable and disable printing receipt on ECR + +## Overview + +The application message **"Enable/disable printing receipt on ECR"** can only have the confirmation message (ACK-ETX-LRC) as its reply. + +When the POS terminal goes back to idle, it automatically enables receipts printing on its printer. At the beginning of each procedure triggered by the ECR, the **“Enable printing receipt on ECR”** command is be sent if that is required. + +- If the terminal does not receive any **“Enable/disable printing receipt on ECR”** message, or if it receives the command of **"Disable printing receipt on ECR"**, the receipt of the requested procedure is printed by the terminal. +- If the terminal receives the **"Enable printing receipt on ECR"** command, the receipt of the requested procedure is always sent to the ECR. The POS terminal sends the receipt lines by using the **"S"** command. + +At the end of any procedure requested by the cash register, the terminal automatically returns the **"Print on terminal enabled"** status. + +## Enable and disable printing receipt on ECR message (from ECR) + +| Pos | Length | Type | Content | +| --- | --- | --- | --- | +| 1 | 8 | N | Terminal ID (00000000-99999999) | +| 9 | 1 | A | Reserved – fixed to ‘0’ (0x30) | +| 10 | 1 | A | Message code: ‘E’ (0x45) | +| 11 | 1 | N | Enable disable printing receipt on ECR flag: - **‘0’** - Disable printing receipt on ECR (any receipt will be printed by the terminal). - **‘1’** - Enable printing receipt on ECR (any receipt will be sent to the ECR by using the "S" command). | +-------------------------- +--- +title: "Traditional POS | Nexi group developer portal" +source: "https://developer.nexigroup.com/traditionalpos/en-EU/docs/send-ticket/" +author: +published: +created: 2026-05-27 +description: +tags: + - "clippings" +--- +## Send ticket + +## Overview + +Through this command, the Terminal sends the lines of the receipt to ECR for printing. Every message can contain up to a maximum of 200 characters. Multiple lines can be sent for printing, with different formats indicated by specific control characters. + +- **Standard format or double height format**: both normal and bold are available, lines are composed of 24 characters. +- **Double width format:** both normal and bold are available, lines are composed of 12 characters. +- **Compressed format:** lines are composed of 42 characters. + +- The new line character 0x7D resets the format to standard - 24 characters. If it’s the first character of a new line, it should be interpreted as a request to print an “empty line” (24 black). +- If the line is 24 characters long, the 0x7D is not required because the printer automatically considers the line as “completed”, so the next character will be printed in a new line. +- In case the line of printing is preceded by the characters 0x7B, 0x7C or 0x5E, the number of characters after which a line wrap is inserted is always 12 or 42. Therefore, it is not necessary to insert an end-of-line character 0x7D. +- If a row should be printed in “double height bold” (as an example: a row with the amount when the transaction is successful), the first character of this row must be 0x7F. If the line is 24 characters long, the next one will be printed automatically in the normal format, without the presence of any special character. + +## Send ticket message (from Terminal) + +| Pos | Length | Type | Content | +| --- | --- | --- | --- | +| 1 | 8 | N | Terminal ID (00000000-99999999) | +| 9 | 1 | N | Reserved – fixed to ‘0’ (0x30) | +| 10 | 1 | A | Message code: ‘S’ (0x53) | +| 11 | 1-200 | A | Lines to be printed; formatted to 12, 14 or 42 chars per line. | + +**Field 11** can contain the following special characters, with the specified meaning: + +- 0x7D = new line (CR+LF) and normal character format reset - USED +- 0x7E = start of double height normal characters - NOT USED +- 0x7F = start of double height bold characters - USED +- 0x7B = start of double width normal characters - NOT USED +- 0x7C = start of double width bold characters - NOT USED +- 0x5E = start of compressed characters - NOT USED + +The last **Send** ticket message will at the end of **Field 11** contain the following sequence: + +- 0x7D = new line (CR+LF) +- 0x7D = new line (CR+LF) +- 0x7D = new line (CR+LF) +- 0x7D = new line (CR+LF) +- 0x7D = new line (CR+LF) +- 0x7D = new line (CR+LF) +- 0x1B = ticket end +-------------------------- +--- +title: "Traditional POS | Nexi group developer portal" +source: "https://developer.nexigroup.com/traditionalpos/en-EU/docs/reprint-ticket/" +author: +published: +created: 2026-05-27 +description: +tags: + - "clippings" +--- +## Reprint ticket + +## Overview + +This command can be used to ask the "POS" terminal to print a copy of the last financial receipt created. The command contains the indication of whether the copy of the receipt must be printed directly from the terminal on its own printer or if the copy must be sent to the ECR. + +In case the copy must be sent to the ECR, after having confirmed the "reprint ticket” command, the POS terminal sends the copy of the receipt to the ECR by using the "S" type commands. + +If the "POS" terminal is without a printer, the “reprint ticket” command with direct printing on the terminal does not produce any results. + +## Send ticket message (from Terminal) + +| Pos | Length | Type | Content | +| --- | --- | --- | --- | +| 1 | 8 | N | Terminal ID (00000000-99999999) | +| 9 | 1 | N | Reserved – fixed to ‘0’ (0x30) | +| 10 | 1 | A | Message code: ‘R’ (0x52) | +| 11 | 1 | N | Enable disable printing receipt on ECR flag: - **‘0’** = disable printing receipt on ECR (any receipt will be print by terminal) - **‘1’** = enable printing receipt on ECR (any receipt will be sent to the ECR using ‘S’ command) | +| 12 | 1 | N | Ticket type flag: | +| 13 | 10 | A | Reserved – fixed to ‘0’ (0x30) | +-------------------------- +--- +title: "VAS request - K command | Traditional POS | Nexi group developer portal" +source: "https://developer.nexigroup.com/traditionalpos/en-EU/docs/vas-request-k-command/" +author: +published: +created: 2026-05-27 +description: +tags: + - "clippings" +--- +## VAS request - K command + +## Overview + +This command can be used to manage the VAS Services (APPs developed by Nexi) for alternative payment methods (APM) and other services. The protocol is a carrier for requests and responses from ECR to the corresponding APPs. + +This feature is only available for Traditional POS. + +## VAS Request (from ECR) + +| Pos | Length | Type | Content | +| --- | --- | --- | --- | +| 1 | 8 | N | Terminal ID (00000000-99999999) | +| 9 | 1 | N | Reserved (fixed at “0” 30 hex) | +| 10 | 1 | A | Message code: “K” (75 hex) | +| 11 | 8 | N | ECR Identifier | +| 19 | 3 | A | Reserved (fixed at “0” 30 hex) | +| 22 | 1 | N | Reserved (fixed at “0”) | +| 23 | 4 | N | Vas Request Length | +| 27 | max 1024 | A | Vas Request (XML Format). The length of message is specified in “VAS Request Length". | + +## VAS Response (from Terminal) + +| Pos | Length | Type | Content | +| --- | --- | --- | --- | +| 1 | 8 | N | Terminal ID (00000000-99999999) | +| 9 | 1 | N | Reserved (fixed at “0” 30 hex) | +| 10 | 1 | A | Message code: “K” (75 hex) | +| 11 | 4 | A | Reserved (fixed at “0” 30 hex) | +| 15 | 1 | N | Concatenation Flag: - **“0”** - last message - **“1”** - more messages to be sent | +| 16 | 3 | N | ID message: identify the message in sequence. The first message is set to “001”. | +| 23 | 4 | N | Vas Response Length | +| 27 | max 1024 | A | Vas Response (XML Format) | + +If the XML response is greater than 1024 bytes (in the form of a long Receipt Data), the response will be formulated in more concatenated messages so that it would be max 1024 bytes. + +If the “concatenation Flag” is set to “1”, the ECR will queue more messages (after sending ACK). + +The messages are characterized by the “ID Message” field, which is incremental from 001 to the number of messages that compose the response. + +ECR will concatenate the messages and compose the final XML “VAS Response”. + +The response XML can contain the receipt produced in response to the execution of the requested VAS. The receipt, also in XML format, can contain the following TAGs for print formatting: + +
TAGsMeaning
The text to be printed out
Line feed
Start printing in bold
Start printing in double height
Start printing in double weight
Start printing in normal
Switch from bold to normal
Switch to normal height
Switch to normal weight
+ +## APM (BancomatPAY, ALIPAY, WECHAT) + +APM management is based on the use of the VAS\_CLIENT application. Services can be engaged by ECR using the “K” command. + +The management of APMs involves a single delivery method. These are the supported functions: + +| Function | Description | +| --- | --- | +| Payment via QrCode | Triggers the flow for managing payment via QR-CODE on the terminal. The transaction amount is passed as an optional parameter. | +| Last operation status | Triggers the flow on the terminal to verify the outcome of the last operation performed. | +| Total or Partial Refund | Triggers the flow for managing the reversal of a payment on the terminal. The cancellation can be total or partial. | +| Daily Totals | Triggers the request for totals (number of transactions and amount) from the last accounting close. | +| Accounting Closure | Triggers the request for accounting closure with zeroing of the totals. | + +### Payment via QrCode - Request + +### Payment via QrCode - Request + +```json + +

BPAY_QR

+

+

value

+``` + +| Name | Description | Type | Values | +| --- | --- | --- | --- | +| ECRVASID | Request Service ID | Alphanumeric | BPAY\_QR WECHAT\_QR ALIPAY\_QR | +| AMOUNT | Amount to authorize | Numeric | | +| PRINTER | Defines who prints the receipt. If omitted, print the POS. | Alphanumeric | POS ECR | + +### Payment via QrCode - Request + +```json +"

BPAY_QR

10

ECR

" +``` + +### Payment via QrCode - Response + +### Payment via QrCode - Response + +```json + +

BPAY_QR

+

0

+

OK-APPROVED

+``` + +| Name | Description | Type | Values | +| --- | --- | --- | --- | +| ECRVASID | Request Service ID | Alphanumeric | BPAY\_QR WECHAT\_QR ALIPAY\_QR | +| RESPID | Outcome of the Request | Numeric | 0 = OK 0 <> KO | +| RESPMSG | Description Outcome | Alphanumeric | If OK –-> APPROVED If not OK –-> Description of the anomaly found | +| ORDER\_ID | Unique identifier attributed to the transaction | Alphanumeric, max 27 | Value returned by the circuit. Necessary for the management of any Refund request. | + +### Last operation status - Request + +### Last operation status - Request + +```json + +

BPAY_ INQUIRY

+

+

value

+``` + +| Name | Description | Type | Values | +| --- | --- | --- | --- | +| ECRVASID | Request Service ID | Alphanumeric | BPAY\_INQUIRY WECHAT\_INQUIRY ALIPAY\_INQUIRY | +| PRINTER | Defines who prints the receipt. If omitted, print the POS. | Alphanumeric | POS ECR | + +### Last operation status - Request + +```json +"

BPAY_QR

ECR

" +``` + +### Last operation status - Response + +### Last operation status - Response + +```json + +

BPAY_ INQUIRY

+

0

+

OK-APPROVED

+``` + +| Name | Description | Type | Values | +| --- | --- | --- | --- | +| ECRVASID | Request Service ID | Alphanumeric | BPAY\_INQUIRY WECHAT\_INQUIRY ALIPAY\_INQUIRY | +| RESPID | Outcome of the Request | Numeric | 0 = OK 0 <> KO | +| RESPMSG | Description Outcome | Alphanumeric | If OK –-> APPROVED If not OK –-> Description of the anomaly found | +| ORDER\_ID | Unique identifier attributed to the transaction | Alphanumeric, max 27 | Value returned by the circuit. Necessary for the management of any Refund request. | + +### Total or Partial Refund - Request + +### Total or Partial Refund - Request + +```json + +

BPAY_REFUND

+

+

value

+``` + +| Name | Description | Type | Values | +| --- | --- | --- | --- | +| ECRVASID | Request Service ID | Alphanumeric | BPAY\_REFUND WECHAT\_REFUND ALIPAY\_ REFUND | +| AMOUNT | Amount to authorize | Numeric | Amount to be reversed (less than or equal to the authorized amount) | +| ORDER\_ID | Unique identifier of the transaction to be refunded | Alphanumeric, max 27 | Assigned by the authorization system | +| PRINTER | Defines who prints the receipt. If omitted, print the POS. | Alphanumeric | POS ECR | + +### Total or Partial Refund - Request + +```json +"

BPAY_REFUND

10

ECR

" +``` + +### Total or Partial Refund - Response + +### Total or Partial Refund - Response + +```json + +

BPAY_REFUND

+

0

+

OK-APPROVED

+``` + +| Name | Description | Type | Values | +| --- | --- | --- | --- | +| ECRVASID | Request Service ID | Alphanumeric | BPAY\_REFUND WECHAT\_REFUND ALIPAY\_ REFUND | +| RESPID | Outcome of the Request | Numeric | 0 = OK 0 <> KO | +| RESPMSG | Description Outcome | Alphanumeric | If OK –-> APPROVED If not OK –-> Description of the anomaly found | +| ORDER\_ID | Unique identifier attributed to the transaction | Alphanumeric, max 27 | Value echo | + +### Daily Totals - Request + +Currently available only for BPAY. + +### Daily Totals - Request + +```json + +

BPAY_TOTAL

+

+

value

+``` + +| Name | Description | Type | Values | +| --- | --- | --- | --- | +| ECRVASID | Request Service ID | Alphanumeric | BPAY\_TOTAL | +| PRINTER | Defines who prints the receipt. If omitted, print the POS. | Alphanumeric | POS ECR | + +### Daily Totals - Request + +```json +"

BPAY_TOTAL

" +``` + +### Daily Totals - Response + +### Daily Totals - Response + +```json + +

BPAY_TOTAL

+

0

+

OK-APPROVED

+``` + +| Name | Description | Type | Value | +| --- | --- | --- | --- | +| ECRVASID | Request Service ID | Alphanumeric | BPAY\_REFUND | +| RESPID | Outcome of the Request | Numeric | 0 = OK 0 <> KO | +| RESPMSG | Description Outcome | Alphanumeric | If OK –-> APPROVED If not OK –-> Description of the anomaly found | +| NUM\_TR | Total number of transactions accounted for | Alphanumeric | Value calculated from the last closing | +| TOT\_TR | Total amount accounted for | Alphanumeric | Value calculated from the last closing | + +### Accounting Closure - Request + +Currently available only for BPAY. + +### Accounting Closure - Request + +```json + +

BPAY_CHIUSURA

+

+

value

+``` + +| Name | Description | Type | Value | +| --- | --- | --- | --- | +| ECRVASID | Request Service ID | Alphanumeric | BPAY\_CHIUSURA | +| PRINTER | Defines who prints the receipt. If omitted, print the POS. | Alphanumeric | POS ECR | + +### Accounting Closure - Request + +```json +"

BPAY_CHIUSURA

" +``` + +### Accounting Closure - Response + +### Accounting Closure - Response + +```json + +

BPAY_CHIUSURA

+

0

+

OK-APPROVED

+``` + +| Name | Description | Type | Value | +| --- | --- | --- | --- | +| ECRVASID | Request Service ID | Alphanumeric | BPAY\_CHIUSURA | +| RESPID | Outcome of the Request | Numeric | 0 = OK 0 <> KO | +| RESPMSG | Description Outcome | Alphanumeric | If OK –-> APPROVED If not OK –-> Description of the anomaly found | +-------------------------- diff --git a/package/android/CMakeLists.txt b/package/android/CMakeLists.txt index 37695fe..eb179e1 100644 --- a/package/android/CMakeLists.txt +++ b/package/android/CMakeLists.txt @@ -9,10 +9,13 @@ set (CMAKE_CXX_STANDARD 20) add_compile_options(-DRN_SERIALIZABLE_STATE=1) # Define C++ library and add all sources -add_library(${PACKAGE_NAME} SHARED +add_library(${PACKAGE_NAME} SHARED src/main/cpp/cpp-adapter.cpp ../cpp/Ecr17.cpp - + ../cpp/Ecr17Client/Ecr17Client.cpp + ../cpp/Ecr17Protocol/Ecr17Protocol.cpp + ../cpp/Lcr/Lcr.cpp + ../cpp/PacketCodec/PacketCodec.cpp ) # Add Nitrogen specs :) diff --git a/package/cpp/Ecr17Client/Ecr17Client.cpp b/package/cpp/Ecr17Client/Ecr17Client.cpp index ff6f460..a535109 100644 --- a/package/cpp/Ecr17Client/Ecr17Client.cpp +++ b/package/cpp/Ecr17Client/Ecr17Client.cpp @@ -1,10 +1,21 @@ #include "Ecr17Client.hpp" +#include + namespace margelo::nitro::ecr17 { void HybridEcr17Client::configure(const Ecr17Config& config) { config_ = config; } Ecr17Config HybridEcr17Client::configuration() { return config_; } -PosStatusResponse HybridEcr17Client::status() {}; +PosStatusResponse HybridEcr17Client::status() { + // NOTE: not implemented yet. Producing a real PosStatusResponse requires a + // configured Transport, sending the 's' status request and parsing the + // terminal's status response message. Until that flow exists we throw + // instead of falling off the end of a non-void function (undefined + // behaviour, which previously returned a garbage struct to JS). + throw std::runtime_error( + "Ecr17Client::status() is not implemented yet: missing transport send/receive and " + "status response parsing"); +} } // namespace margelo::nitro::ecr17 \ No newline at end of file diff --git a/package/cpp/Ecr17Protocol/Ecr17Protocol.cpp b/package/cpp/Ecr17Protocol/Ecr17Protocol.cpp index d63771e..7f94e22 100644 --- a/package/cpp/Ecr17Protocol/Ecr17Protocol.cpp +++ b/package/cpp/Ecr17Protocol/Ecr17Protocol.cpp @@ -1,9 +1,20 @@ #include "Ecr17Protocol.hpp" +#include + namespace margelo::nitro::ecr17 { +// Right-aligns `value` into a fixed-width field of `size` bytes, padding on the +// left with `ch`. ECR17 fields have a fixed length, so a value longer than the +// field would shift every following field and corrupt the frame: reject it +// instead of emitting a malformed message. static std::string leftPad(const std::string& value, size_t size, char ch = '0') { - if (value.size() >= size) { + if (value.size() > size) { + throw std::invalid_argument("ECR17: value '" + value + "' exceeds fixed field width of " + + std::to_string(size) + " bytes"); + } + + if (value.size() == size) { return value; } @@ -12,6 +23,10 @@ static std::string leftPad(const std::string& value, size_t size, char ch = '0') std::string Ecr17Protocol::buildPaymentMessage(const std::string& terminalId, const std::string& cashRegisterId, int amountCents) { + if (amountCents < 0) { + throw std::invalid_argument("ECR17: amount must be non-negative"); + } + std::string m; m += leftPad(terminalId, 8); diff --git a/package/cpp/Ecr17Protocol/Ecr17Protocol.hpp b/package/cpp/Ecr17Protocol/Ecr17Protocol.hpp index ba50b77..c242eff 100644 --- a/package/cpp/Ecr17Protocol/Ecr17Protocol.hpp +++ b/package/cpp/Ecr17Protocol/Ecr17Protocol.hpp @@ -9,7 +9,7 @@ class Ecr17Protocol { static std::string buildPaymentMessage(const std::string& terminalId, const std::string& cashRegisterId, int amountCents); - static std::string Ecr17Protocol::buildStatusMessage(const std::string& terminalId); + static std::string buildStatusMessage(const std::string& terminalId); }; } // namespace margelo::nitro::ecr17 \ No newline at end of file diff --git a/package/cpp/PacketCodec/PacketCodec.cpp b/package/cpp/PacketCodec/PacketCodec.cpp index 4b8e685..382c7ac 100644 --- a/package/cpp/PacketCodec/PacketCodec.cpp +++ b/package/cpp/PacketCodec/PacketCodec.cpp @@ -1,5 +1,8 @@ #include "PacketCodec.hpp" +#include // std::find +#include // std::distance + namespace margelo::nitro::ecr17 { PacketCodec::PacketCodec(LrcMode mode) : lrcMode_(mode) {} @@ -61,10 +64,21 @@ DecodedPacket PacketCodec::decode(const std::vector& data) { } if (first == SOH) { + // Progress update packet: SOH + message + EOT (no LRC). We need at least + // SOH and the trailing EOT before stripping the first/last byte, + // otherwise the iterator range below would be invalid (last < first). + if (data.size() < 2) { + return { + PacketType::UNKNOWN, + "", + false, + }; + } + std::string payload(data.begin() + 1, data.end() - 1); return { - PacketType::STATUS, + PacketType::PROGRESS, payload, true, }; @@ -83,9 +97,19 @@ DecodedPacket PacketCodec::decode(const std::vector& data) { size_t etxIndex = std::distance(data.begin(), etxIt); + // The LRC is the byte immediately after ETX (STX + payload + ETX + LRC). + // If nothing follows ETX the frame is truncated and cannot be validated. + if (etxIndex + 1 >= data.size()) { + return { + PacketType::UNKNOWN, + "", + false, + }; + } + std::string payload(data.begin() + 1, data.begin() + etxIndex); - uint8_t rxLrc = data.back(); + uint8_t rxLrc = data[etxIndex + 1]; uint8_t calcLrc = Lrc::compute(payload, lrcMode_); diff --git a/package/cpp/PacketCodec/PacketCodec.hpp b/package/cpp/PacketCodec/PacketCodec.hpp index 461309f..fe1bd2f 100644 --- a/package/cpp/PacketCodec/PacketCodec.hpp +++ b/package/cpp/PacketCodec/PacketCodec.hpp @@ -5,13 +5,14 @@ #include #include -#include "Lcr.hpp" +#include "Lcr/Lcr.hpp" namespace margelo::nitro::ecr17 { enum class PacketType { APPLICATION, - STATUS, + // SOH-framed procedure progress update (0x01 ... 0x04), see protocol spec. + PROGRESS, ACK, NAK, UNKNOWN, diff --git a/package/cpp/Transport/Transport.cpp b/package/cpp/Transport/Transport.cpp deleted file mode 100644 index 54c5a98..0000000 --- a/package/cpp/Transport/Transport.cpp +++ /dev/null @@ -1,31 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -namespace margelo::nitro::ecr17 { - -using DataCallback = std::function&)>; - -using DisconnectCallback = std::function; - -class Transport { - public: - virtual ~Transport() = default; - - virtual void connect() = 0; - - virtual void disconnect() = 0; - - virtual bool isConnected() const = 0; - - virtual void send(const std::vector& bytes) = 0; - - virtual void setDataCallback(DataCallback cb) = 0; - - virtual void setDisconnectCallback(DisconnectCallback cb) = 0; -}; - -} // namespace margelo::nitro::ecr17 \ No newline at end of file From 596b2341232fb60a461f376a88b9859f899bc2c6 Mon Sep 17 00:00:00 2001 From: "lorenzo.padovani@padosoft.com" Date: Wed, 27 May 2026 23:46:34 +0200 Subject: [PATCH 2/6] test: add C++ unit suite for LRC, packet codec and protocol builders + CI Adds a standalone GoogleTest suite (no Nitro/codegen required) that compiles the pure-logic units in isolation and runs on every PR via GitHub Actions. Coverage: - Lrc::compute: base value, all four LrcMode variants, known vectors, string/vector overload parity, and a cross-check against an independent reference implementation. - PacketCodec: application encode framing + round-trip for every mode, corrupted-LRC detection, control (ACK/NAK) framing/decoding, and the edge cases fixed in this branch -- empty buffer, lone SOH (no crash), progress-update frame, STX without ETX, and ETX with no trailing LRC. - Ecr17Protocol: exact 167-byte Payment layout with per-field assertions against the spec, 10-byte status layout with lowercase 's', amount boundary (8 digits), and the new validation (negative amount, oversized fields throw std::invalid_argument). A test-only stubs/LrcMode.hpp mirrors the enum Nitrogen generates so the suite builds without running codegen; it is on the test target's include path only and documents the contract the production code assumes. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/cpp-tests.yml | 33 +++++++ package/cpp/tests/CMakeLists.txt | 46 +++++++++ package/cpp/tests/stubs/LrcMode.hpp | 25 +++++ package/cpp/tests/test_lrc.cpp | 66 +++++++++++++ package/cpp/tests/test_packet_codec.cpp | 126 ++++++++++++++++++++++++ package/cpp/tests/test_protocol.cpp | 78 +++++++++++++++ 6 files changed, 374 insertions(+) create mode 100644 .github/workflows/cpp-tests.yml create mode 100644 package/cpp/tests/CMakeLists.txt create mode 100644 package/cpp/tests/stubs/LrcMode.hpp create mode 100644 package/cpp/tests/test_lrc.cpp create mode 100644 package/cpp/tests/test_packet_codec.cpp create mode 100644 package/cpp/tests/test_protocol.cpp diff --git a/.github/workflows/cpp-tests.yml b/.github/workflows/cpp-tests.yml new file mode 100644 index 0000000..db1c29e --- /dev/null +++ b/.github/workflows/cpp-tests.yml @@ -0,0 +1,33 @@ +name: C++ unit tests + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +concurrency: + group: cpp-tests-${{ github.ref }} + cancel-in-progress: true + +jobs: + cpp-tests: + name: Build & run C++ tests + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install toolchain + run: | + sudo apt-get update + sudo apt-get install -y cmake ninja-build g++ + + - name: Configure + run: cmake -S package/cpp/tests -B build -G Ninja -DCMAKE_BUILD_TYPE=Release + + - name: Build + run: cmake --build build --config Release + + - name: Run tests + run: ctest --test-dir build --output-on-failure diff --git a/package/cpp/tests/CMakeLists.txt b/package/cpp/tests/CMakeLists.txt new file mode 100644 index 0000000..8262198 --- /dev/null +++ b/package/cpp/tests/CMakeLists.txt @@ -0,0 +1,46 @@ +cmake_minimum_required(VERSION 3.16) +project(ecr17_cpp_tests CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +# GoogleTest via FetchContent (CI has network access). +include(FetchContent) +FetchContent_Declare( + googletest + URL https://github.com/google/googletest/archive/refs/tags/v1.15.2.tar.gz +) +# Keep CRT linkage consistent on Windows runners. +set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) +FetchContent_MakeAvailable(googletest) + +enable_testing() + +# Production C++ sources under test (../ is package/cpp). +set(ECR17_CPP_DIR ${CMAKE_CURRENT_SOURCE_DIR}/..) + +add_executable(ecr17_cpp_tests + test_lrc.cpp + test_packet_codec.cpp + test_protocol.cpp + ${ECR17_CPP_DIR}/Lcr/Lcr.cpp + ${ECR17_CPP_DIR}/PacketCodec/PacketCodec.cpp + ${ECR17_CPP_DIR}/Ecr17Protocol/Ecr17Protocol.cpp +) + +target_include_directories(ecr17_cpp_tests PRIVATE + ${ECR17_CPP_DIR} # resolves "Lcr/Lcr.hpp", "PacketCodec/..." + ${CMAKE_CURRENT_SOURCE_DIR}/stubs # test-only LrcMode.hpp (Nitrogen-generated in real builds) +) + +if(MSVC) + target_compile_options(ecr17_cpp_tests PRIVATE /W4) +else() + target_compile_options(ecr17_cpp_tests PRIVATE -Wall -Wextra) +endif() + +target_link_libraries(ecr17_cpp_tests PRIVATE GTest::gtest_main) + +include(GoogleTest) +gtest_discover_tests(ecr17_cpp_tests) diff --git a/package/cpp/tests/stubs/LrcMode.hpp b/package/cpp/tests/stubs/LrcMode.hpp new file mode 100644 index 0000000..6bece7c --- /dev/null +++ b/package/cpp/tests/stubs/LrcMode.hpp @@ -0,0 +1,25 @@ +#pragma once + +// TEST-ONLY STUB. +// +// In a real build this header is generated by Nitrogen from the TypeScript +// union `type LrcMode = "stx" | "std" | "noext" | "stx_noext"` (see +// package/src/types/client.ts). The unit tests compile the pure-logic C++ +// units (Lrc, PacketCodec, Ecr17Protocol) in isolation, without running +// Nitrogen, so we provide a minimal enum that mirrors the generated one. +// +// The member names MUST match what the production code references +// (Lrc.cpp uses LrcMode::STX, LrcMode::STX_NOEXT, LrcMode::NOEXT). If Nitrogen +// turns out to emit different casing, that is a real bug to reconcile when the +// codegen is run — this stub documents the contract the code assumes today. + +namespace margelo::nitro::ecr17 { + +enum class LrcMode { + STX, + STD, + NOEXT, + STX_NOEXT, +}; + +} // namespace margelo::nitro::ecr17 diff --git a/package/cpp/tests/test_lrc.cpp b/package/cpp/tests/test_lrc.cpp new file mode 100644 index 0000000..8095d5a --- /dev/null +++ b/package/cpp/tests/test_lrc.cpp @@ -0,0 +1,66 @@ +#include + +#include +#include +#include + +#include "Lcr/Lcr.hpp" + +using margelo::nitro::ecr17::Lrc; +using margelo::nitro::ecr17::LrcMode; + +namespace { + +constexpr uint8_t kBase = 0x7F; +constexpr uint8_t kStx = 0x02; +constexpr uint8_t kEtx = 0x03; + +// Reference implementation kept intentionally independent from the production +// code, so the tests assert against first principles rather than a copy. +uint8_t reference(const std::vector& payload, LrcMode mode) { + uint8_t lrc = kBase; + if (mode == LrcMode::STX || mode == LrcMode::STX_NOEXT) { + lrc ^= kStx; + } + for (uint8_t b : payload) { + lrc ^= b; + } + if (mode == LrcMode::STX || mode == LrcMode::NOEXT) { + lrc ^= kEtx; + } + return lrc; +} + +} // namespace + +TEST(Lrc, EmptyPayloadStdIsBase) { + EXPECT_EQ(Lrc::compute(std::vector{}, LrcMode::STD), kBase); +} + +TEST(Lrc, EmptyPayloadStxFoldsStxAndEtx) { + // 0x7F ^ 0x02 ^ 0x03 == 0x7E + EXPECT_EQ(Lrc::compute(std::vector{}, LrcMode::STX), 0x7E); +} + +TEST(Lrc, KnownVectorAllModes) { + const std::vector payload{'A'}; // 0x41 + EXPECT_EQ(Lrc::compute(payload, LrcMode::STD), 0x3E); + EXPECT_EQ(Lrc::compute(payload, LrcMode::STX), 0x3F); + EXPECT_EQ(Lrc::compute(payload, LrcMode::NOEXT), 0x3D); + EXPECT_EQ(Lrc::compute(payload, LrcMode::STX_NOEXT), 0x3C); +} + +TEST(Lrc, MatchesReferenceForEveryMode) { + const std::vector payload{0x00, 0x7F, 0x55, 0xAA, 'Z', 0x10}; + for (LrcMode mode : {LrcMode::STX, LrcMode::STD, LrcMode::NOEXT, LrcMode::STX_NOEXT}) { + EXPECT_EQ(Lrc::compute(payload, mode), reference(payload, mode)); + } +} + +TEST(Lrc, StringAndVectorOverloadsAgree) { + const std::string payload = "12345678P0"; + const std::vector bytes(payload.begin(), payload.end()); + for (LrcMode mode : {LrcMode::STX, LrcMode::STD, LrcMode::NOEXT, LrcMode::STX_NOEXT}) { + EXPECT_EQ(Lrc::compute(payload, mode), Lrc::compute(bytes, mode)); + } +} diff --git a/package/cpp/tests/test_packet_codec.cpp b/package/cpp/tests/test_packet_codec.cpp new file mode 100644 index 0000000..8172aa9 --- /dev/null +++ b/package/cpp/tests/test_packet_codec.cpp @@ -0,0 +1,126 @@ +#include + +#include +#include +#include + +#include "PacketCodec/PacketCodec.hpp" + +using margelo::nitro::ecr17::DecodedPacket; +using margelo::nitro::ecr17::LrcMode; +using margelo::nitro::ecr17::PacketCodec; +using margelo::nitro::ecr17::PacketType; + +namespace { +constexpr uint8_t kSoh = 0x01; +constexpr uint8_t kStx = 0x02; +constexpr uint8_t kEtx = 0x03; +constexpr uint8_t kEot = 0x04; +constexpr uint8_t kAck = 0x06; +constexpr uint8_t kNak = 0x15; +} // namespace + +TEST(PacketCodec, EncodeApplicationFramesStxPayloadEtxLrc) { + PacketCodec codec(LrcMode::STD); + auto frame = codec.encodeApplication("AB"); + ASSERT_EQ(frame.size(), 5u); + EXPECT_EQ(frame[0], kStx); + EXPECT_EQ(frame[1], 'A'); + EXPECT_EQ(frame[2], 'B'); + EXPECT_EQ(frame[3], kEtx); + EXPECT_EQ(frame[4], 0x7C); // 0x7F ^ 'A' ^ 'B' +} + +TEST(PacketCodec, ApplicationRoundTrip) { + for (LrcMode mode : {LrcMode::STX, LrcMode::STD, LrcMode::NOEXT, LrcMode::STX_NOEXT}) { + PacketCodec codec(mode); + const std::string payload = "123456780P0000065000"; + DecodedPacket decoded = codec.decode(codec.encodeApplication(payload)); + EXPECT_EQ(decoded.type, PacketType::APPLICATION); + EXPECT_EQ(decoded.payload, payload); + EXPECT_TRUE(decoded.validLrc); + } +} + +TEST(PacketCodec, ApplicationDetectsCorruptedLrc) { + PacketCodec codec(LrcMode::STD); + auto frame = codec.encodeApplication("HELLO"); + frame.back() ^= 0xFF; // corrupt the LRC byte + DecodedPacket decoded = codec.decode(frame); + EXPECT_EQ(decoded.type, PacketType::APPLICATION); + EXPECT_EQ(decoded.payload, "HELLO"); + EXPECT_FALSE(decoded.validLrc); +} + +TEST(PacketCodec, EncodeControlFramesCtrlEtxLrc) { + PacketCodec codec(LrcMode::STD); + auto frame = codec.encodeControl(kAck); + ASSERT_EQ(frame.size(), 3u); + EXPECT_EQ(frame[0], kAck); + EXPECT_EQ(frame[1], kEtx); +} + +TEST(PacketCodec, DecodeAck) { + PacketCodec codec(LrcMode::STD); + DecodedPacket decoded = codec.decode({kAck}); + EXPECT_EQ(decoded.type, PacketType::ACK); + EXPECT_TRUE(decoded.validLrc); +} + +TEST(PacketCodec, DecodeNak) { + PacketCodec codec(LrcMode::STD); + DecodedPacket decoded = codec.decode({kNak}); + EXPECT_EQ(decoded.type, PacketType::NAK); + EXPECT_TRUE(decoded.validLrc); +} + +TEST(PacketCodec, DecodeEmptyIsUnknown) { + PacketCodec codec(LrcMode::STD); + DecodedPacket decoded = codec.decode({}); + EXPECT_EQ(decoded.type, PacketType::UNKNOWN); + EXPECT_FALSE(decoded.validLrc); +} + +// Regression: a lone SOH byte previously built a string from an inverted +// iterator range [begin()+1, end()-1) == [end(), begin()) -> UB/crash. +TEST(PacketCodec, DecodeLoneSohIsUnknownNotCrash) { + PacketCodec codec(LrcMode::STD); + DecodedPacket decoded = codec.decode({kSoh}); + EXPECT_EQ(decoded.type, PacketType::UNKNOWN); + EXPECT_FALSE(decoded.validLrc); +} + +TEST(PacketCodec, DecodeProgressUpdate) { + PacketCodec codec(LrcMode::STD); + std::vector frame{kSoh}; + const std::string msg = "ELABORAZIONE... "; // 20 chars per spec + frame.insert(frame.end(), msg.begin(), msg.end()); + frame.push_back(kEot); + + DecodedPacket decoded = codec.decode(frame); + EXPECT_EQ(decoded.type, PacketType::PROGRESS); + EXPECT_EQ(decoded.payload, msg); +} + +TEST(PacketCodec, DecodeStxWithoutEtxIsUnknown) { + PacketCodec codec(LrcMode::STD); + DecodedPacket decoded = codec.decode({kStx, 'A', 'B'}); + EXPECT_EQ(decoded.type, PacketType::UNKNOWN); + EXPECT_FALSE(decoded.validLrc); +} + +// Regression: ETX present but no trailing LRC byte must not read past the end +// nor mistake ETX for the LRC. +TEST(PacketCodec, DecodeStxWithEtxButNoLrcIsUnknown) { + PacketCodec codec(LrcMode::STD); + DecodedPacket decoded = codec.decode({kStx, 'A', kEtx}); + EXPECT_EQ(decoded.type, PacketType::UNKNOWN); + EXPECT_FALSE(decoded.validLrc); +} + +TEST(PacketCodec, DecodeUnknownLeadByte) { + PacketCodec codec(LrcMode::STD); + DecodedPacket decoded = codec.decode({0x99, 0x00}); + EXPECT_EQ(decoded.type, PacketType::UNKNOWN); + EXPECT_FALSE(decoded.validLrc); +} diff --git a/package/cpp/tests/test_protocol.cpp b/package/cpp/tests/test_protocol.cpp new file mode 100644 index 0000000..d06fbdc --- /dev/null +++ b/package/cpp/tests/test_protocol.cpp @@ -0,0 +1,78 @@ +#include + +#include +#include + +#include "Ecr17Protocol/Ecr17Protocol.hpp" + +using margelo::nitro::ecr17::Ecr17Protocol; + +// Field layout reference (1-based positions from the Nexi ECR17 spec, Payment +// request "P"): +// 1 8 Terminal ID +// 9 1 Reserved '0' +// 10 1 Message code 'P' +// 11 8 Cash register ID +// 19 1 Presence of additional data for GT +// 20 2 Reserved "00" +// 22 1 Start transaction when card already present +// 23 1 Payment type +// 24 8 Transaction amount (right aligned, '0' filled) +// 32 128 Text to print (' ' filled) +// 160 8 Reserved '0' => total 167 bytes + +TEST(Protocol, StatusMessageLayout) { + std::string m = Ecr17Protocol::buildStatusMessage("42"); + ASSERT_EQ(m.size(), 10u); + EXPECT_EQ(m.substr(0, 8), "00000042"); // terminal id, left-padded + EXPECT_EQ(m[8], '0'); // reserved + EXPECT_EQ(m[9], 's'); // lowercase message code per spec +} + +TEST(Protocol, StatusMessageKeepsFullWidthId) { + std::string m = Ecr17Protocol::buildStatusMessage("12345678"); + EXPECT_EQ(m, "123456780s"); +} + +TEST(Protocol, PaymentMessageIs167Bytes) { + std::string m = Ecr17Protocol::buildPaymentMessage("1", "2", 650); + EXPECT_EQ(m.size(), 167u); +} + +TEST(Protocol, PaymentMessageFieldLayout) { + std::string m = Ecr17Protocol::buildPaymentMessage("12345678", "87654321", 650); + ASSERT_EQ(m.size(), 167u); + EXPECT_EQ(m.substr(0, 8), "12345678"); // terminal id + EXPECT_EQ(m[8], '0'); // reserved + EXPECT_EQ(m[9], 'P'); // message code + EXPECT_EQ(m.substr(10, 8), "87654321"); // cash register id + EXPECT_EQ(m[18], '0'); // presence of additional data + EXPECT_EQ(m.substr(19, 2), "00"); // reserved + EXPECT_EQ(m[21], '0'); // start-with-card + EXPECT_EQ(m[22], '0'); // payment type + EXPECT_EQ(m.substr(23, 8), "00000650"); // amount, right aligned + EXPECT_EQ(m.substr(31, 128), std::string(128, ' ')); // text field + EXPECT_EQ(m.substr(159, 8), "00000000"); // trailing reserved +} + +TEST(Protocol, PaymentMessageAmountMaxFits) { + std::string m = Ecr17Protocol::buildPaymentMessage("1", "2", 99999999); + EXPECT_EQ(m.substr(23, 8), "99999999"); +} + +TEST(Protocol, PaymentRejectsNegativeAmount) { + EXPECT_THROW(Ecr17Protocol::buildPaymentMessage("1", "2", -1), std::invalid_argument); +} + +TEST(Protocol, PaymentRejectsAmountOverflowingField) { + // 9 digits does not fit the 8-byte amount field. + EXPECT_THROW(Ecr17Protocol::buildPaymentMessage("1", "2", 100000000), std::invalid_argument); +} + +TEST(Protocol, PaymentRejectsOversizedTerminalId) { + EXPECT_THROW(Ecr17Protocol::buildPaymentMessage("123456789", "2", 650), std::invalid_argument); +} + +TEST(Protocol, StatusRejectsOversizedTerminalId) { + EXPECT_THROW(Ecr17Protocol::buildStatusMessage("123456789"), std::invalid_argument); +} From faceb595349627a405673d9fdb483399ef39856e Mon Sep 17 00:00:00 2001 From: "lorenzo.padovani@padosoft.com" Date: Wed, 27 May 2026 23:51:36 +0200 Subject: [PATCH 3/6] feat: add reversal builder + documented end-to-end flow tests - Implement Ecr17Protocol::buildReversalMessage (command 'S', 26-byte layout per spec; STAN defaults to "000000" = reverse last, no STAN check). - Add test_flows.cpp modelling the documented sequences: basic payment (request -> ACK -> progress -> result -> ACK), NAK retransmission, reversal/"annullamento", pay -> reverse -> repay, and status round-trip. Response field parsing is not implemented yet, so terminal responses are spec-shaped payloads asserted at the framing/classification level. - Add direct unit tests for the reversal layout and STAN validation. Co-Authored-By: Claude Opus 4.7 (1M context) --- package/cpp/Ecr17Protocol/Ecr17Protocol.cpp | 16 +++ package/cpp/Ecr17Protocol/Ecr17Protocol.hpp | 8 ++ package/cpp/tests/CMakeLists.txt | 1 + package/cpp/tests/test_flows.cpp | 148 ++++++++++++++++++++ package/cpp/tests/test_protocol.cpp | 24 ++++ 5 files changed, 197 insertions(+) create mode 100644 package/cpp/tests/test_flows.cpp diff --git a/package/cpp/Ecr17Protocol/Ecr17Protocol.cpp b/package/cpp/Ecr17Protocol/Ecr17Protocol.cpp index 7f94e22..13e28ed 100644 --- a/package/cpp/Ecr17Protocol/Ecr17Protocol.cpp +++ b/package/cpp/Ecr17Protocol/Ecr17Protocol.cpp @@ -57,4 +57,20 @@ std::string Ecr17Protocol::buildStatusMessage(const std::string& terminalId) { return m; } +std::string Ecr17Protocol::buildReversalMessage(const std::string& terminalId, + const std::string& cashRegisterId, + const std::string& stan) { + std::string m; + + m += leftPad(terminalId, 8); // pos 1 : terminal id + m += "0"; // pos 9 : reserved + m += "S"; // pos 10 : message code + m += leftPad(cashRegisterId, 8); // pos 11 : cash register id + m += leftPad(stan, 6); // pos 19 : STAN ("000000" = no check) + m += "0"; // pos 25 : presence of additional GT data + m += "0"; // pos 26 : reserved + + return m; // 26 bytes +} + } // namespace margelo::nitro::ecr17 \ No newline at end of file diff --git a/package/cpp/Ecr17Protocol/Ecr17Protocol.hpp b/package/cpp/Ecr17Protocol/Ecr17Protocol.hpp index c242eff..e75fa2e 100644 --- a/package/cpp/Ecr17Protocol/Ecr17Protocol.hpp +++ b/package/cpp/Ecr17Protocol/Ecr17Protocol.hpp @@ -10,6 +10,14 @@ class Ecr17Protocol { const std::string& cashRegisterId, int amountCents); static std::string buildStatusMessage(const std::string& terminalId); + + // Reversal ("annullamento") of the last transaction (message code 'S'). + // `stan` is the 6-digit STAN of the transaction to reverse; "000000" + // (the default) tells the terminal to skip the STAN check and reverse the + // last payment. + static std::string buildReversalMessage(const std::string& terminalId, + const std::string& cashRegisterId, + const std::string& stan = "000000"); }; } // namespace margelo::nitro::ecr17 \ No newline at end of file diff --git a/package/cpp/tests/CMakeLists.txt b/package/cpp/tests/CMakeLists.txt index 8262198..bc9ff3e 100644 --- a/package/cpp/tests/CMakeLists.txt +++ b/package/cpp/tests/CMakeLists.txt @@ -24,6 +24,7 @@ add_executable(ecr17_cpp_tests test_lrc.cpp test_packet_codec.cpp test_protocol.cpp + test_flows.cpp ${ECR17_CPP_DIR}/Lcr/Lcr.cpp ${ECR17_CPP_DIR}/PacketCodec/PacketCodec.cpp ${ECR17_CPP_DIR}/Ecr17Protocol/Ecr17Protocol.cpp diff --git a/package/cpp/tests/test_flows.cpp b/package/cpp/tests/test_flows.cpp new file mode 100644 index 0000000..cf97c94 --- /dev/null +++ b/package/cpp/tests/test_flows.cpp @@ -0,0 +1,148 @@ +// End-to-end protocol *flow* tests, modelled on the documented Nexi ECR17 +// sequences (basic payment, reversal/"annullamento", re-payment, status, +// NAK retransmission). +// +// These exercise the layers that exist today: request building +// (Ecr17Protocol) + physical framing (PacketCodec). Response *field* parsing +// is not implemented yet, so terminal responses are synthesized as spec-shaped +// payloads and asserted at the framing/classification level (packet type, LRC +// validity, message code position), not by parsed fields. + +#include + +#include +#include +#include + +#include "Ecr17Protocol/Ecr17Protocol.hpp" +#include "PacketCodec/PacketCodec.hpp" + +using namespace margelo::nitro::ecr17; + +namespace { + +constexpr uint8_t kStx = 0x02; +constexpr uint8_t kEtx = 0x03; +constexpr uint8_t kSoh = 0x01; +constexpr uint8_t kEot = 0x04; +constexpr uint8_t kAck = 0x06; +constexpr uint8_t kNak = 0x15; + +const std::string kTerminal = "12345678"; +const std::string kCashReg = "00000001"; + +// Terminal-side framing of a progress-update packet: SOH + 20-char message + EOT. +std::vector progressFrame(const std::string& msg20) { + std::vector f{kSoh}; + f.insert(f.end(), msg20.begin(), msg20.end()); + f.push_back(kEot); + return f; +} + +// Terminal-side framing of a spec-shaped result message ('E', result "00" = OK). +std::vector okResultFrame(PacketCodec& codec) { + std::string e = kTerminal + "0" + "E" + "00" + std::string(60, '0'); + return codec.encodeApplication(e); +} + +} // namespace + +// Documented "Basic Payment Flow": request -> ACK -> progress -> result -> ACK. +TEST(Flow, BasicPayment) { + PacketCodec codec(LrcMode::STD); + + // 1. ECR -> POS: payment request on the wire (STX .. ETX .. LRC). + std::string req = Ecr17Protocol::buildPaymentMessage(kTerminal, kCashReg, 650); + auto reqFrame = codec.encodeApplication(req); + EXPECT_EQ(reqFrame.front(), kStx); + EXPECT_EQ(reqFrame[reqFrame.size() - 2], kEtx); + + // POS validates and recovers the exact request payload. + DecodedPacket atPos = codec.decode(reqFrame); + EXPECT_EQ(atPos.type, PacketType::APPLICATION); + EXPECT_TRUE(atPos.validLrc); + EXPECT_EQ(atPos.payload, req); + EXPECT_EQ(atPos.payload[9], 'P'); + + // 2. POS -> ECR: physical ACK. + EXPECT_EQ(codec.decode(codec.encodeControl(kAck)).type, PacketType::ACK); + + // 3. POS -> ECR: progress update while contacting the host (no LRC). + DecodedPacket prog = codec.decode(progressFrame("ATTENDERE PREGO ")); + EXPECT_EQ(prog.type, PacketType::PROGRESS); + EXPECT_EQ(prog.payload, "ATTENDERE PREGO "); + + // 4. POS -> ECR: positive result. + DecodedPacket result = codec.decode(okResultFrame(codec)); + EXPECT_EQ(result.type, PacketType::APPLICATION); + EXPECT_TRUE(result.validLrc); + EXPECT_EQ(result.payload[9], 'E'); + EXPECT_EQ(result.payload.substr(10, 2), "00"); + + // 5. ECR -> POS: ACK confirming receipt of the result. + EXPECT_EQ(codec.decode(codec.encodeControl(kAck)).type, PacketType::ACK); +} + +// Documented NAK handling: a NAK triggers retransmission of the same message +// (up to 3 times). Framing is deterministic, so the retransmitted bytes match. +TEST(Flow, NakTriggersIdenticalRetransmit) { + PacketCodec codec(LrcMode::STD); + std::string req = Ecr17Protocol::buildPaymentMessage(kTerminal, kCashReg, 1999); + auto first = codec.encodeApplication(req); + + EXPECT_EQ(codec.decode(codec.encodeControl(kNak)).type, PacketType::NAK); + + auto retransmit = codec.encodeApplication(req); + EXPECT_EQ(first, retransmit); +} + +// Documented "Reversal Last Transaction" (annullamento), command 'S'. +TEST(Flow, ReversalAnnullamento) { + PacketCodec codec(LrcMode::STD); + + std::string req = Ecr17Protocol::buildReversalMessage(kTerminal, kCashReg, "000123"); + ASSERT_EQ(req.size(), 26u); + EXPECT_EQ(req[9], 'S'); + EXPECT_EQ(req.substr(18, 6), "000123"); + + DecodedPacket atPos = codec.decode(codec.encodeApplication(req)); + EXPECT_EQ(atPos.type, PacketType::APPLICATION); + EXPECT_TRUE(atPos.validLrc); + EXPECT_EQ(atPos.payload, req); + + EXPECT_EQ(codec.decode(codec.encodeControl(kAck)).type, PacketType::ACK); + DecodedPacket result = codec.decode(okResultFrame(codec)); + EXPECT_TRUE(result.validLrc); + EXPECT_EQ(result.payload[9], 'E'); +} + +// Pay -> annullamento -> ripaga (re-payment with the corrected amount). +TEST(Flow, PayReverseRepay) { + PacketCodec codec(LrcMode::STD); + + auto pay1 = codec.encodeApplication(Ecr17Protocol::buildPaymentMessage(kTerminal, kCashReg, 650)); + auto rev = codec.encodeApplication(Ecr17Protocol::buildReversalMessage(kTerminal, kCashReg)); + auto pay2 = codec.encodeApplication(Ecr17Protocol::buildPaymentMessage(kTerminal, kCashReg, 720)); + + for (const auto* frame : {&pay1, &rev, &pay2}) { + DecodedPacket d = codec.decode(*frame); + EXPECT_EQ(d.type, PacketType::APPLICATION); + EXPECT_TRUE(d.validLrc); + } + + EXPECT_EQ(codec.decode(pay1).payload.substr(23, 8), "00000650"); + EXPECT_EQ(codec.decode(pay2).payload.substr(23, 8), "00000720"); + EXPECT_NE(pay1, pay2); // the corrected re-payment differs on the wire +} + +// Terminal status request/round-trip ('s'). +TEST(Flow, StatusRequest) { + PacketCodec codec(LrcMode::STD); + std::string req = Ecr17Protocol::buildStatusMessage(kTerminal); + EXPECT_EQ(req, "123456780s"); + + DecodedPacket atPos = codec.decode(codec.encodeApplication(req)); + EXPECT_EQ(atPos.type, PacketType::APPLICATION); + EXPECT_TRUE(atPos.validLrc); + EXPECT_EQ(atPos.payload, req); +} diff --git a/package/cpp/tests/test_protocol.cpp b/package/cpp/tests/test_protocol.cpp index d06fbdc..8546220 100644 --- a/package/cpp/tests/test_protocol.cpp +++ b/package/cpp/tests/test_protocol.cpp @@ -76,3 +76,27 @@ TEST(Protocol, PaymentRejectsOversizedTerminalId) { TEST(Protocol, StatusRejectsOversizedTerminalId) { EXPECT_THROW(Ecr17Protocol::buildStatusMessage("123456789"), std::invalid_argument); } + +// Reversal request "S" layout (1-based spec positions): +// 1 8 Terminal ID · 9 1 Reserved · 10 1 'S' · 11 8 Cash register ID +// 19 6 STAN · 25 1 Presence of GT data · 26 1 Reserved => 26 bytes +TEST(Protocol, ReversalMessageLayout) { + std::string m = Ecr17Protocol::buildReversalMessage("12345678", "87654321", "000123"); + ASSERT_EQ(m.size(), 26u); + EXPECT_EQ(m.substr(0, 8), "12345678"); + EXPECT_EQ(m[8], '0'); + EXPECT_EQ(m[9], 'S'); + EXPECT_EQ(m.substr(10, 8), "87654321"); + EXPECT_EQ(m.substr(18, 6), "000123"); + EXPECT_EQ(m[24], '0'); + EXPECT_EQ(m[25], '0'); +} + +TEST(Protocol, ReversalDefaultStanIsNoCheck) { + std::string m = Ecr17Protocol::buildReversalMessage("12345678", "87654321"); + EXPECT_EQ(m.substr(18, 6), "000000"); +} + +TEST(Protocol, ReversalRejectsOversizedStan) { + EXPECT_THROW(Ecr17Protocol::buildReversalMessage("1", "2", "1234567"), std::invalid_argument); +} From bca027f855dacfd226e1fe1dd5127281e89fae80 Mon Sep 17 00:00:00 2001 From: "lorenzo.padovani@padosoft.com" Date: Thu, 28 May 2026 00:04:12 +0200 Subject: [PATCH 4/6] docs: comprehensive package README Replace the bare Nitro template README with a full guide: badges (incl. CI), TOC, an honest feature-status matrix (what works today vs roadmap), junior-proof quick start, Ecr17Config reference, LRC-mode and command-code cheat-sheets, architecture overview, and test instructions. Also fixes the license badge link (pointed at the wrong org). Co-Authored-By: Claude Opus 4.7 (1M context) --- package/README.md | 281 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 272 insertions(+), 9 deletions(-) diff --git a/package/README.md b/package/README.md index da6fad2..2a3af9b 100644 --- a/package/README.md +++ b/package/README.md @@ -1,18 +1,281 @@ -# react-native-ecr17 +
-react-native-ecr17 is a react native package built with Nitro +# 💳 react-native-ecr17 -[![Version](https://img.shields.io/npm/v/react-native-ecr17.svg)](https://www.npmjs.com/package/react-native-ecr17) -[![Downloads](https://img.shields.io/npm/dm/react-native-ecr17.svg)](https://www.npmjs.com/package/react-native-ecr17) -[![License](https://img.shields.io/npm/l/react-native-ecr17.svg)](https://github.com/patrickkabwe/react-native-ecr17/LICENSE) +**A React Native / Nitro module for the Italian ECR17 payment protocol — talk to Nexi Group POS terminals straight from your cash-register app.** + +[![npm version](https://img.shields.io/npm/v/react-native-ecr17.svg?style=flat-square)](https://www.npmjs.com/package/react-native-ecr17) +[![npm downloads](https://img.shields.io/npm/dm/react-native-ecr17.svg?style=flat-square)](https://www.npmjs.com/package/react-native-ecr17) +[![License: MIT](https://img.shields.io/npm/l/react-native-ecr17.svg?style=flat-square)](https://github.com/padosoft/react-native-ecr17-protocol/blob/main/LICENSE) +[![C++ tests](https://github.com/padosoft/react-native-ecr17-protocol/actions/workflows/cpp-tests.yml/badge.svg)](https://github.com/padosoft/react-native-ecr17-protocol/actions/workflows/cpp-tests.yml) +[![Built with Nitro](https://img.shields.io/badge/built%20with-Nitro-8B5CF6?style=flat-square)](https://nitro.margelo.com) +[![Platforms](https://img.shields.io/badge/platforms-iOS%20%7C%20Android-555?style=flat-square)](#requirements) + +
+ +--- + +> [!IMPORTANT] +> **Early foundation release.** The protocol core (packet framing, LRC, message +> builders) is implemented and unit-tested, but the JavaScript surface is still +> minimal: you can `configure()` a client and read the configuration back. +> Sending payments end-to-end requires the transport layer, which is on the +> [roadmap](#-roadmap). See [Feature status](#-feature-status) for the exact +> state of every piece — nothing here is oversold. + +## 📚 Table of contents + +- [What is ECR17?](#-what-is-ecr17) +- [Why this library](#-why-this-library) +- [Feature status](#-feature-status) +- [Requirements](#requirements) +- [Installation](#-installation) +- [Quick start](#-quick-start) +- [Configuration](#%EF%B8%8F-configuration) +- [API reference](#-api-reference) +- [Protocol cheat-sheet](#-protocol-cheat-sheet) +- [Architecture](#%EF%B8%8F-architecture) +- [Running the tests](#-running-the-tests) +- [Roadmap](#-roadmap) +- [Contributing](#-contributing) +- [License](#-license) + +## 🧭 What is ECR17? + +**ECR17** is the Italian standard protocol — supported by **Nexi Group** +terminals — used to integrate an *Electronic Cash Register* (ECR) with an +*EFT-POS* payment terminal over a local LAN connection. The cash register sends +a request (a payment, a reversal, a status check…), the terminal talks to the +acquiring host, and replies synchronously. + +This library speaks that protocol from a React Native app, with the +performance-sensitive parts written in C++ and bridged via +[Nitro Modules](https://nitro.margelo.com). + +The protocol reference used to build this library is vendored in +[`docs/`](https://github.com/padosoft/react-native-ecr17-protocol/tree/main/docs). + +## ✨ Why this library + +- ⚡️ **C++ core, Nitro-bridged** — framing/LRC run natively on both iOS & + Android, no JS-thread overhead. +- 🧱 **Spec-faithful** — message layouts validated byte-for-byte against the + Nexi ECR17 documentation. +- 🛡️ **Fails loudly, not silently** — oversized fixed-width fields and negative + amounts throw instead of emitting a malformed frame to a payment terminal. +- ✅ **Tested** — 34 unit + flow tests (LRC, packet codec, builders, and + documented payment/reversal flows) run in CI on every PR. +- 🔌 **Configurable LRC modes** — the standard leaves the LRC scope to the + integrator; all four variants are first-class. + +## 📊 Feature status + +| Area | Status | Notes | +|------|:------:|-------| +| Packet framing (`STX`/`ETX`/`SOH`/`EOT`, ACK/NAK) | ✅ | `PacketCodec` encode/decode | +| LRC computation (4 modes, base `0x7F`) | ✅ | `Lrc` | +| Payment request builder (`P`) | 🟡 | C++ (`Ecr17Protocol`), not yet exposed to JS | +| Reversal / *annullamento* builder (`S`) | 🟡 | C++, not yet exposed to JS | +| Terminal status builder (`s`) | 🟡 | C++, not yet exposed to JS | +| `Ecr17Client.configure()` / `configuration()` | ✅ | usable from JS today | +| `Ecr17Client.status()` | 🚧 | throws "not implemented" until transport lands | +| Transport (TCP/LAN, ACK/NAK retry) | ❌ | [roadmap](#-roadmap) | +| Response field parsing (`E`/`V`/`s`/…) | ❌ | [roadmap](#-roadmap) | + +Legend: ✅ done · 🟡 implemented natively, not yet on the JS API · 🚧 stub · ❌ not started. ## Requirements -- React Native v0.76.0 or higher -- Node 18.0.0 or higher +- **React Native** 0.76.0+ (new architecture) +- **react-native-nitro-modules** (peer dependency) +- **Node** 18+ +- A Nexi Group ECR17-compatible terminal configured for **LAN integration** -## Installation +## 📦 Installation ```bash +# with bun bun add react-native-ecr17 react-native-nitro-modules -``` \ No newline at end of file + +# or npm / yarn +npm install react-native-ecr17 react-native-nitro-modules +``` + +Then install native deps: + +```bash +cd ios && pod install # iOS +# Android autolinks — just rebuild +``` + +> This is a Nitro module: it requires the React Native **new architecture** +> (`newArchEnabled=true`), which is the default on RN 0.76+. + +## 🚀 Quick start + +A junior-proof, copy-paste example of what works **today** — create a client, +configure it, and read the configuration back: + +```ts +import { createEcr17Client } from 'react-native-ecr17'; + +// 1. Create + configure a client pointed at your terminal on the LAN. +const client = createEcr17Client({ + host: '192.168.1.50', // terminal IP + port: 1024, // configured ECR port (Linux EFT-POS: > 1024) + terminalId: '12345678', // 8-digit terminal id (or '00000000') + cashRegisterId: '00000001', + lrcMode: 'std', // see "Protocol cheat-sheet" +}); + +// 2. Read back the effective configuration (handy for debugging). +const cfg = client.configuration(); +console.log('Configured for terminal', cfg.terminalId, 'at', cfg.host); +``` + +> [!NOTE] +> `client.status()` currently **throws** `"... not implemented yet ..."` on +> purpose — querying the terminal needs the transport layer, which is on the +> roadmap. This is intentional so you never get a fake/garbage response. + +Here's the shape the payment API will take once the transport lands (🚧 **not +functional yet** — shown so you can plan your integration): + +```ts +// ⚠️ ROADMAP — does not work yet. +// const result = await client.pay({ amountCents: 650 }); +// if (result.outcome === 'OK') { /* print receipt, etc. */ } +``` + +## ⚙️ Configuration + +`createEcr17Client(config)` / `client.configure(config)` accept an `Ecr17Config`: + +| Field | Type | Required | Description | +|-------|------|:--------:|-------------| +| `host` | `string` | ✅ | Terminal IP address on the LAN | +| `port` | `number` | | TCP port (Linux EFT-POS requires > 1024) | +| `terminalId` | `string` | ✅ | 8-digit terminal id (`00000000`–`99999999`) | +| `cashRegisterId` | `string` | ✅ | Cash register / ECR identifier | +| `lrcMode` | `LrcMode` | | LRC scope — see below (default per terminal) | +| `keepAlive` | `boolean` | | Keep the socket open between requests | +| `autoReconnect` | `boolean` | | Reconnect automatically on drop | +| `connectionTimeoutMs` | `number` | | Connect timeout | +| `responseTimeoutMs` | `number` | | Per-request response timeout | +| `ackTimeoutMs` | `number` | | Physical ACK/NAK timeout | +| `retryCount` | `number` | | Retransmission attempts (spec: up to 3) | +| `retryDelayMs` | `number` | | Delay between retransmissions | +| `debug` | `boolean` | | Verbose protocol logging | + +## 📖 API reference + +### `createEcr17Client(config: Ecr17Config): Ecr17Client` + +Creates a Nitro `Ecr17Client` HybridObject and configures it in one call. + +### `client.configure(config: Ecr17Config): void` + +(Re)applies configuration to an existing client. + +### `client.configuration(): Ecr17Config` + +Returns the currently applied configuration. + +### `client.status(): PosStatusResponse` 🚧 + +Will return the terminal status. **Currently throws** until transport + +response parsing are implemented. + +```ts +interface PosStatusResponse { + terminalId: string; + terminalDateTime: Date; + status: PosTerminalStatus; // -1 (Unknown) … 6 + softwareRelease: string; +} +``` + +`PosTerminalStatus` maps to human-readable strings via `PosTerminalStatusMessage` +(e.g. `2` → `"Terminal operative (after a DLL)"`, `-1` → `"Unknown"`). + +## 🔐 Protocol cheat-sheet + +**Application packet:** `STX(0x02)` · payload · `ETX(0x03)` · `LRC` +**Progress update:** `SOH(0x01)` · 20-char message · `EOT(0x04)` (no LRC) +**Confirmation:** `ACK(0x06)` / `NAK(0x15)` · `ETX` · `LRC` + +**LRC** = `0x7F` XOR-folded over the message bytes. Which framing bytes are +included is configurable via `lrcMode`: + +| `lrcMode` | Bytes folded into the LRC | +|-----------|---------------------------| +| `'stx'` | `STX` + payload + `ETX` | +| `'std'` | payload only | +| `'noext'` | payload + `ETX` | +| `'stx_noext'` | `STX` + payload | + +**Command codes** (from the Nexi ECR17 spec): + +| Code | Command | Builder | +|:----:|---------|:-------:| +| `s` | Terminal status | ✅ (native) | +| `P` | Payment | ✅ (native) | +| `S` | Reversal (*annullamento*) | ✅ (native) | +| `X` | Extended payment | ❌ | +| `p` `i` `c` | Pre-auth / incremental / closure | ❌ | +| `H` | Card verification | ❌ | +| `U` | Additional GT data / tokenization | ❌ | +| `C` `T` | Close session / totals | ❌ | +| `G` `E` `R` | Last result / ECR print / reprint | ❌ | +| `K` | VAS / APM (BancomatPay, Alipay, …) | ❌ | + +## 🏗️ Architecture + +``` +package/cpp/ +├── Lcr/ # Lrc — LRC computation (4 modes, base 0x7F) +├── PacketCodec/ # framing: encode/decode STX·ETX·SOH·EOT·ACK·NAK + LRC check +├── Ecr17Protocol/ # message builders (P / S / s) — fixed-width, validated +├── Transport/ # abstract send/receive interface (impl on the roadmap) +└── Ecr17Client/ # HybridEcr17Client — the Nitro-exposed entry point +``` + +The JS/TS spec lives in `package/src/specs/client.nitro.ts`; the C++/Swift/Kotlin +glue is generated by Nitrogen into `package/nitrogen/generated/`. + +## 🧪 Running the tests + +The C++ core is covered by a standalone GoogleTest suite that builds without +Nitro (it stubs the generated `LrcMode` enum): + +```bash +cmake -S package/cpp/tests -B build -DCMAKE_BUILD_TYPE=Release +cmake --build build +ctest --test-dir build --output-on-failure +``` + +The same suite runs in CI on every pull request. It covers LRC (all modes), +packet (de)framing and its edge cases, the message builders' exact byte layout +and validation, and the documented **payment / reversal / re-payment** flows. + +## 🛣️ Roadmap + +- [ ] `Transport` TCP/LAN implementation (iOS + Android) +- [ ] Send/receive orchestration with ACK/NAK + retransmit-up-to-3 (per spec) +- [ ] Response field parsing (`E`/`V` results, `s` status, `S` receipt stream, `U` GT data) +- [ ] Expose `pay()`, `reverse()`, `status()` on the JS API +- [ ] Remaining command builders (`X`, `p`, `i`, `c`, `H`, `U`, `C`, `T`, `G`, `E`, `R`, `K`) +- [ ] Tokenization helpers + +## 🤝 Contributing + +Issues and PRs welcome. Please keep the C++ core spec-faithful and add tests for +any new builder or codec path — CI must stay green. + +## 📄 License + +[MIT](https://github.com/padosoft/react-native-ecr17-protocol/blob/main/LICENSE) © [padosoft](https://github.com/padosoft) + +> **Disclaimer:** this is an independent integration library. "ECR17", "Nexi" +> and related marks belong to their respective owners and are referenced for +> interoperability only. From 4436cfa2d3684a2068b548a2d5506f8a728f9c12 Mon Sep 17 00:00:00 2001 From: "lorenzo.padovani@padosoft.com" Date: Thu, 28 May 2026 00:07:29 +0200 Subject: [PATCH 5/6] fix: correct Biome lint glob (packages/** -> package/**) The repository package folder is `package/` (singular), but biome.json included `packages/**` (plural), so the main package's TypeScript was never linted. Found via the Copilot review report in PR #2. Co-Authored-By: Claude Opus 4.7 (1M context) --- biome.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/biome.json b/biome.json index 3a22d96..2556137 100644 --- a/biome.json +++ b/biome.json @@ -6,8 +6,8 @@ "includes": [ "example/**/*.ts", "example/**/*.tsx", - "packages/**/*.ts", - "packages/**/*.tsx" + "package/**/*.ts", + "package/**/*.tsx" ] } } From 2d8174c7ff14cae271351879841312c2be9748f2 Mon Sep 17 00:00:00 2001 From: "lorenzo.padovani@padosoft.com" Date: Thu, 28 May 2026 00:20:42 +0200 Subject: [PATCH 6/6] fix: reject trailing bytes after the application-frame LRC (Codex review) decode() now requires the LRC to be the final byte of an STX application frame (etxIndex + 2 == data.size()), rejecting both truncated frames and buffers with trailing bytes. Previously a coalesced socket read holding a second frame, or garbage after the LRC, was silently accepted as a single valid APPLICATION packet and the remainder dropped. Splitting a byte stream into frames is the transport layer's responsibility; DecodedPacket has no remainder field. Adds regression tests for trailing-bytes and two coalesced frames. Co-Authored-By: Claude Opus 4.7 (1M context) --- package/cpp/PacketCodec/PacketCodec.cpp | 11 ++++++++--- package/cpp/tests/test_packet_codec.cpp | 24 ++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/package/cpp/PacketCodec/PacketCodec.cpp b/package/cpp/PacketCodec/PacketCodec.cpp index 382c7ac..406a097 100644 --- a/package/cpp/PacketCodec/PacketCodec.cpp +++ b/package/cpp/PacketCodec/PacketCodec.cpp @@ -97,9 +97,14 @@ DecodedPacket PacketCodec::decode(const std::vector& data) { size_t etxIndex = std::distance(data.begin(), etxIt); - // The LRC is the byte immediately after ETX (STX + payload + ETX + LRC). - // If nothing follows ETX the frame is truncated and cannot be validated. - if (etxIndex + 1 >= data.size()) { + // A well-formed application frame is exactly STX + payload + ETX + LRC, + // so the LRC must be the final byte. Reject both a truncated frame (no + // LRC after ETX) and a buffer with trailing bytes -- e.g. a coalesced + // socket read holding a second frame ("STX..ETX LRC STX..") or garbage + // after the LRC. Splitting a byte stream into individual frames is the + // transport layer's responsibility, and DecodedPacket cannot carry the + // unconsumed remainder. + if (etxIndex + 2 != data.size()) { return { PacketType::UNKNOWN, "", diff --git a/package/cpp/tests/test_packet_codec.cpp b/package/cpp/tests/test_packet_codec.cpp index 8172aa9..0474491 100644 --- a/package/cpp/tests/test_packet_codec.cpp +++ b/package/cpp/tests/test_packet_codec.cpp @@ -124,3 +124,27 @@ TEST(PacketCodec, DecodeUnknownLeadByte) { EXPECT_EQ(decoded.type, PacketType::UNKNOWN); EXPECT_FALSE(decoded.validLrc); } + +// Regression: trailing bytes after a complete frame's LRC must not be silently +// accepted (the LRC must be the final byte). +TEST(PacketCodec, DecodeStxWithTrailingBytesAfterLrcIsUnknown) { + PacketCodec codec(LrcMode::STD); + auto frame = codec.encodeApplication("AB"); // STX A B ETX LRC + frame.push_back(0x00); // stray trailing byte + DecodedPacket decoded = codec.decode(frame); + EXPECT_EQ(decoded.type, PacketType::UNKNOWN); + EXPECT_FALSE(decoded.validLrc); +} + +// Regression: a coalesced read holding two frames must not be reported as one +// valid APPLICATION packet (which would silently drop the second frame). +// Framing/splitting a byte stream is the transport layer's job. +TEST(PacketCodec, DecodeCoalescedFramesIsUnknown) { + PacketCodec codec(LrcMode::STD); + auto first = codec.encodeApplication("AB"); + auto second = codec.encodeApplication("CD"); + first.insert(first.end(), second.begin(), second.end()); + DecodedPacket decoded = codec.decode(first); + EXPECT_EQ(decoded.type, PacketType::UNKNOWN); + EXPECT_FALSE(decoded.validLrc); +}