@padosoft/react-native-ecr17 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/Ecr17.podspec +39 -0
  2. package/README.md +348 -0
  3. package/android/CMakeLists.txt +41 -0
  4. package/android/build.gradle +149 -0
  5. package/android/fix-prefab.gradle +51 -0
  6. package/android/gradle.properties +5 -0
  7. package/android/src/main/AndroidManifest.xml +2 -0
  8. package/android/src/main/cpp/cpp-adapter.cpp +9 -0
  9. package/android/src/main/java/com/margelo/nitro/ecr17/HybridEcr17Transport.kt +233 -0
  10. package/android/src/main/java/com/padosoft/ecr17/Ecr17Package.kt +30 -0
  11. package/cpp/Ecr17.cpp +1 -0
  12. package/cpp/Ecr17.hpp +2 -0
  13. package/cpp/Ecr17Client/HybridEcr17Client.cpp +598 -0
  14. package/cpp/Ecr17Client/HybridEcr17Client.hpp +85 -0
  15. package/cpp/Ecr17Protocol/Ecr17Protocol.cpp +277 -0
  16. package/cpp/Ecr17Protocol/Ecr17Protocol.hpp +103 -0
  17. package/cpp/Ecr17Response/Ecr17Response.cpp +155 -0
  18. package/cpp/Ecr17Response/Ecr17Response.hpp +113 -0
  19. package/cpp/Lcr/Lcr.cpp +42 -0
  20. package/cpp/Lcr/Lcr.hpp +22 -0
  21. package/cpp/PacketCodec/PacketCodec.cpp +146 -0
  22. package/cpp/PacketCodec/PacketCodec.hpp +48 -0
  23. package/cpp/Session/Ecr17Session.cpp +260 -0
  24. package/cpp/Session/Ecr17Session.hpp +97 -0
  25. package/cpp/Session/RetryPolicy.hpp +23 -0
  26. package/cpp/Transport/FakeTransport.hpp +95 -0
  27. package/cpp/Transport/NativeTransportAdapter.cpp +42 -0
  28. package/cpp/Transport/NativeTransportAdapter.hpp +32 -0
  29. package/cpp/Transport/Transport.hpp +31 -0
  30. package/cpp/tests/CMakeLists.txt +55 -0
  31. package/cpp/tests/PosixTcpTransport.hpp +105 -0
  32. package/cpp/tests/stubs/LrcMode.hpp +25 -0
  33. package/cpp/tests/test_flows.cpp +148 -0
  34. package/cpp/tests/test_integration_terminal.cpp +72 -0
  35. package/cpp/tests/test_lrc.cpp +66 -0
  36. package/cpp/tests/test_packet_codec.cpp +164 -0
  37. package/cpp/tests/test_protocol.cpp +102 -0
  38. package/cpp/tests/test_protocol_commands.cpp +190 -0
  39. package/cpp/tests/test_response.cpp +164 -0
  40. package/cpp/tests/test_retry_policy.cpp +28 -0
  41. package/cpp/tests/test_session.cpp +262 -0
  42. package/ios/Bridge.h +1 -0
  43. package/ios/HybridEcr17Transport.swift +103 -0
  44. package/nitro.json +30 -0
  45. package/nitrogen/generated/.gitattributes +1 -0
  46. package/nitrogen/generated/android/Ecr17+autolinking.cmake +82 -0
  47. package/nitrogen/generated/android/Ecr17+autolinking.gradle +27 -0
  48. package/nitrogen/generated/android/Ecr17OnLoad.cpp +68 -0
  49. package/nitrogen/generated/android/Ecr17OnLoad.hpp +34 -0
  50. package/nitrogen/generated/android/c++/JFunc_void.hpp +75 -0
  51. package/nitrogen/generated/android/c++/JFunc_void_std__shared_ptr_ArrayBuffer_.hpp +77 -0
  52. package/nitrogen/generated/android/c++/JHybridEcr17TransportSpec.cpp +93 -0
  53. package/nitrogen/generated/android/c++/JHybridEcr17TransportSpec.hpp +68 -0
  54. package/nitrogen/generated/android/kotlin/com/margelo/nitro/ecr17/Ecr17OnLoad.kt +35 -0
  55. package/nitrogen/generated/android/kotlin/com/margelo/nitro/ecr17/Func_void.kt +80 -0
  56. package/nitrogen/generated/android/kotlin/com/margelo/nitro/ecr17/Func_void_std__shared_ptr_ArrayBuffer_.kt +80 -0
  57. package/nitrogen/generated/android/kotlin/com/margelo/nitro/ecr17/HybridEcr17TransportSpec.kt +86 -0
  58. package/nitrogen/generated/ios/Ecr17+autolinking.rb +62 -0
  59. package/nitrogen/generated/ios/Ecr17-Swift-Cxx-Bridge.cpp +57 -0
  60. package/nitrogen/generated/ios/Ecr17-Swift-Cxx-Bridge.hpp +154 -0
  61. package/nitrogen/generated/ios/Ecr17-Swift-Cxx-Umbrella.hpp +47 -0
  62. package/nitrogen/generated/ios/Ecr17Autolinking.mm +43 -0
  63. package/nitrogen/generated/ios/Ecr17Autolinking.swift +26 -0
  64. package/nitrogen/generated/ios/c++/HybridEcr17TransportSpecSwift.cpp +11 -0
  65. package/nitrogen/generated/ios/c++/HybridEcr17TransportSpecSwift.hpp +119 -0
  66. package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
  67. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
  68. package/nitrogen/generated/ios/swift/Func_void_std__shared_ptr_ArrayBuffer_.swift +46 -0
  69. package/nitrogen/generated/ios/swift/HybridEcr17TransportSpec.swift +60 -0
  70. package/nitrogen/generated/ios/swift/HybridEcr17TransportSpec_cxx.swift +211 -0
  71. package/nitrogen/generated/shared/c++/CardType.hpp +84 -0
  72. package/nitrogen/generated/shared/c++/CardVerificationRequest.hpp +97 -0
  73. package/nitrogen/generated/shared/c++/CardVerificationResult.hpp +136 -0
  74. package/nitrogen/generated/shared/c++/CloseSessionResult.hpp +106 -0
  75. package/nitrogen/generated/shared/c++/ConnectionState.hpp +80 -0
  76. package/nitrogen/generated/shared/c++/CurrencyExchange.hpp +100 -0
  77. package/nitrogen/generated/shared/c++/Ecr17Config.hpp +138 -0
  78. package/nitrogen/generated/shared/c++/HybridEcr17ClientSpec.cpp +42 -0
  79. package/nitrogen/generated/shared/c++/HybridEcr17ClientSpec.hpp +138 -0
  80. package/nitrogen/generated/shared/c++/HybridEcr17TransportSpec.cpp +26 -0
  81. package/nitrogen/generated/shared/c++/HybridEcr17TransportSpec.hpp +70 -0
  82. package/nitrogen/generated/shared/c++/IncrementalAuthRequest.hpp +96 -0
  83. package/nitrogen/generated/shared/c++/LrcMode.hpp +84 -0
  84. package/nitrogen/generated/shared/c++/PaymentCardType.hpp +84 -0
  85. package/nitrogen/generated/shared/c++/PaymentRequest.hpp +109 -0
  86. package/nitrogen/generated/shared/c++/PaymentResult.hpp +139 -0
  87. package/nitrogen/generated/shared/c++/PosStatusResponse.hpp +96 -0
  88. package/nitrogen/generated/shared/c++/PreAuthClosureRequest.hpp +96 -0
  89. package/nitrogen/generated/shared/c++/PreAuthRequest.hpp +109 -0
  90. package/nitrogen/generated/shared/c++/PreAuthResult.hpp +144 -0
  91. package/nitrogen/generated/shared/c++/ProgressEvent.hpp +83 -0
  92. package/nitrogen/generated/shared/c++/ReceiptLine.hpp +83 -0
  93. package/nitrogen/generated/shared/c++/ReversalRequest.hpp +88 -0
  94. package/nitrogen/generated/shared/c++/ReversalResult.hpp +132 -0
  95. package/nitrogen/generated/shared/c++/TokenizationRequest.hpp +89 -0
  96. package/nitrogen/generated/shared/c++/TokenizationService.hpp +76 -0
  97. package/nitrogen/generated/shared/c++/TotalsResult.hpp +93 -0
  98. package/nitrogen/generated/shared/c++/TransactionEntryMode.hpp +92 -0
  99. package/nitrogen/generated/shared/c++/TransactionOutcome.hpp +88 -0
  100. package/nitrogen/generated/shared/c++/VasResult.hpp +96 -0
  101. package/package.json +102 -0
  102. package/react-native.config.js +18 -0
  103. package/src/index.ts +4 -0
  104. package/src/specs/client.nitro.ts +102 -0
  105. package/src/specs/transport.nitro.ts +25 -0
  106. package/src/types/client.ts +196 -0
  107. package/src/utils/client.ts +10 -0
@@ -0,0 +1,277 @@
1
+ #include "Ecr17Protocol.hpp"
2
+
3
+ #include <stdexcept>
4
+
5
+ namespace margelo::nitro::ecr17 {
6
+
7
+ namespace {
8
+
9
+ constexpr char kReserved = '0'; // '0' (0x30) filler for reserved numeric fields
10
+ constexpr char kFieldSep = 0x1B; // end-of-field for the privative TAG content
11
+
12
+ // Right-aligns `value` into a fixed-width field of `size` bytes, padding on the
13
+ // left with `ch`. ECR17 fields have a fixed length, so a value longer than the
14
+ // field would shift every following field and corrupt the frame: reject it.
15
+ std::string leftPad(const std::string& value, size_t size, char ch = kReserved) {
16
+ if (value.size() > size) {
17
+ throw std::invalid_argument("ECR17: value '" + value + "' exceeds fixed field width of " +
18
+ std::to_string(size) + " bytes");
19
+ }
20
+ if (value.size() == size) {
21
+ return value;
22
+ }
23
+ return std::string(size - value.size(), ch) + value;
24
+ }
25
+
26
+ // Left-aligns `value` into a fixed-width field, padding on the right with `ch`.
27
+ std::string rightPad(const std::string& value, size_t size, char ch = ' ') {
28
+ if (value.size() > size) {
29
+ throw std::invalid_argument("ECR17: value '" + value + "' exceeds fixed field width of " +
30
+ std::to_string(size) + " bytes");
31
+ }
32
+ return value + std::string(size - value.size(), ch);
33
+ }
34
+
35
+ std::string amountField(int amountCents) {
36
+ if (amountCents < 0) {
37
+ throw std::invalid_argument("ECR17: amount must be non-negative");
38
+ }
39
+ return leftPad(std::to_string(amountCents), 8);
40
+ }
41
+
42
+ char flag(bool on) { return on ? '1' : '0'; }
43
+
44
+ // Shared 167-byte payment-family layout (codes 'P', 'X', 'p').
45
+ std::string buildPaymentLike(char code, const std::string& terminalId,
46
+ const std::string& cashRegisterId, int amountCents, char paymentType,
47
+ bool cardAlreadyPresent, bool withAdditionalData,
48
+ const std::string& receiptText) {
49
+ std::string m;
50
+ m += leftPad(terminalId, 8); // 1 : terminal id
51
+ m += kReserved; // 9 : reserved
52
+ m += code; // 10 : message code
53
+ m += leftPad(cashRegisterId, 8); // 11 : cash register id
54
+ m += flag(withAdditionalData); // 19 : presence of additional GT data
55
+ m += "00"; // 20 : reserved
56
+ m += flag(cardAlreadyPresent); // 22 : start-with-card-present
57
+ m += paymentType; // 23 : payment type
58
+ m += amountField(amountCents); // 24 : amount (8)
59
+ m += leftPad(receiptText, 128, ' '); // 32 : receipt text (128)
60
+ m += "00000000"; // 160: reserved (8)
61
+ return m; // 167
62
+ }
63
+
64
+ // Shared 176-byte pre-auth integration/closure layout (codes 'i', 'c').
65
+ std::string buildPreAuthFollowUp(char code, const std::string& terminalId,
66
+ const std::string& cashRegisterId, int amountCents,
67
+ const std::string& originalPreAuthCode, bool withAdditionalData,
68
+ const std::string& receiptText) {
69
+ std::string m;
70
+ m += leftPad(terminalId, 8); // 1 : terminal id
71
+ m += kReserved; // 9 : reserved
72
+ m += code; // 10 : message code
73
+ m += leftPad(cashRegisterId, 8); // 11 : cash register id
74
+ m += flag(withAdditionalData); // 19 : presence of additional GT data
75
+ m += "0000"; // 20 : reserved (4)
76
+ m += amountField(amountCents); // 24 : amount (8)
77
+ m += leftPad(receiptText, 128, ' '); // 32 : receipt text (128)
78
+ m += leftPad(originalPreAuthCode, 9); // 160: original pre-auth code (9)
79
+ m += "00000000"; // 169: reserved (8)
80
+ return m; // 176
81
+ }
82
+
83
+ // Shared 26-byte session command layout (codes 'C', 'T').
84
+ std::string buildSessionCommand(char code, const std::string& terminalId,
85
+ const std::string& cashRegisterId, bool withAdditionalData) {
86
+ std::string m;
87
+ m += leftPad(terminalId, 8); // 1 : terminal id
88
+ m += kReserved; // 9 : reserved
89
+ m += code; // 10 : message code
90
+ m += leftPad(cashRegisterId, 8); // 11 : cash register id
91
+ m += flag(withAdditionalData); // 19 : presence of additional GT data
92
+ m += "0000000"; // 20 : reserved (7)
93
+ return m; // 26
94
+ }
95
+
96
+ } // namespace
97
+
98
+ std::string Ecr17Protocol::buildPaymentMessage(const std::string& terminalId,
99
+ const std::string& cashRegisterId, int amountCents,
100
+ char paymentType, bool cardAlreadyPresent,
101
+ bool withAdditionalData,
102
+ const std::string& receiptText) {
103
+ return buildPaymentLike('P', terminalId, cashRegisterId, amountCents, paymentType,
104
+ cardAlreadyPresent, withAdditionalData, receiptText);
105
+ }
106
+
107
+ std::string Ecr17Protocol::buildExtendedPaymentMessage(const std::string& terminalId,
108
+ const std::string& cashRegisterId,
109
+ int amountCents, char paymentType,
110
+ bool cardAlreadyPresent,
111
+ bool withAdditionalData,
112
+ const std::string& receiptText) {
113
+ return buildPaymentLike('X', terminalId, cashRegisterId, amountCents, paymentType,
114
+ cardAlreadyPresent, withAdditionalData, receiptText);
115
+ }
116
+
117
+ std::string Ecr17Protocol::buildPreAuthMessage(const std::string& terminalId,
118
+ const std::string& cashRegisterId, int amountCents,
119
+ char paymentType, bool cardAlreadyPresent,
120
+ bool withAdditionalData,
121
+ const std::string& receiptText) {
122
+ return buildPaymentLike('p', terminalId, cashRegisterId, amountCents, paymentType,
123
+ cardAlreadyPresent, withAdditionalData, receiptText);
124
+ }
125
+
126
+ std::string Ecr17Protocol::buildIncrementalMessage(const std::string& terminalId,
127
+ const std::string& cashRegisterId,
128
+ int amountCents,
129
+ const std::string& originalPreAuthCode,
130
+ bool withAdditionalData,
131
+ const std::string& receiptText) {
132
+ return buildPreAuthFollowUp('i', terminalId, cashRegisterId, amountCents, originalPreAuthCode,
133
+ withAdditionalData, receiptText);
134
+ }
135
+
136
+ std::string Ecr17Protocol::buildPreAuthClosureMessage(const std::string& terminalId,
137
+ const std::string& cashRegisterId,
138
+ int amountCents,
139
+ const std::string& originalPreAuthCode,
140
+ bool withAdditionalData,
141
+ const std::string& receiptText) {
142
+ return buildPreAuthFollowUp('c', terminalId, cashRegisterId, amountCents, originalPreAuthCode,
143
+ withAdditionalData, receiptText);
144
+ }
145
+
146
+ std::string Ecr17Protocol::buildCardVerificationMessage(const std::string& terminalId,
147
+ const std::string& cashRegisterId,
148
+ char paymentType, bool withAdditionalData) {
149
+ std::string m;
150
+ m += leftPad(terminalId, 8); // 1 : terminal id
151
+ m += kReserved; // 9 : reserved
152
+ m += 'H'; // 10 : message code
153
+ m += leftPad(cashRegisterId, 8); // 11 : cash register id
154
+ m += flag(withAdditionalData); // 19 : presence of additional GT data
155
+ m += "00"; // 20 : reserved (2)
156
+ m += '0'; // 22 : standard card verification
157
+ m += paymentType; // 23 : payment type
158
+ m += "0000000000000000"; // 24 : reserved (16)
159
+ return m; // 39
160
+ }
161
+
162
+ std::string Ecr17Protocol::buildCloseSessionMessage(const std::string& terminalId,
163
+ const std::string& cashRegisterId,
164
+ bool withAdditionalData) {
165
+ return buildSessionCommand('C', terminalId, cashRegisterId, withAdditionalData);
166
+ }
167
+
168
+ std::string Ecr17Protocol::buildTotalsMessage(const std::string& terminalId,
169
+ const std::string& cashRegisterId,
170
+ bool withAdditionalData) {
171
+ return buildSessionCommand('T', terminalId, cashRegisterId, withAdditionalData);
172
+ }
173
+
174
+ std::string Ecr17Protocol::buildSendLastResultMessage(const std::string& terminalId,
175
+ const std::string& cashRegisterId,
176
+ bool withAdditionalData) {
177
+ std::string m;
178
+ m += leftPad(terminalId, 8); // 1 : terminal id
179
+ m += kReserved; // 9 : reserved
180
+ m += 'G'; // 10 : message code
181
+ m += leftPad(cashRegisterId, 8); // 11 : cash register id
182
+ m += flag(withAdditionalData); // 19 : presence of additional GT data
183
+ m += "000"; // 20 : reserved (3)
184
+ return m; // 22
185
+ }
186
+
187
+ std::string Ecr17Protocol::buildEnableEcrPrintMessage(const std::string& terminalId, bool enabled) {
188
+ std::string m;
189
+ m += leftPad(terminalId, 8); // 1 : terminal id
190
+ m += kReserved; // 9 : reserved
191
+ m += 'E'; // 10 : message code
192
+ m += flag(enabled); // 11 : enable(1)/disable(0) printing on ECR
193
+ return m; // 11
194
+ }
195
+
196
+ std::string Ecr17Protocol::buildReprintMessage(const std::string& terminalId, bool toEcr,
197
+ char ticketType) {
198
+ std::string m;
199
+ m += leftPad(terminalId, 8); // 1 : terminal id
200
+ m += kReserved; // 9 : reserved
201
+ m += 'R'; // 10 : message code
202
+ m += flag(toEcr); // 11 : 1 = send receipt to ECR, 0 = print on terminal
203
+ m += ticketType; // 12 : ticket type flag
204
+ m += "0000000000"; // 13 : reserved (10)
205
+ return m; // 22
206
+ }
207
+
208
+ std::string Ecr17Protocol::buildStatusMessage(const std::string& terminalId) {
209
+ std::string m;
210
+ m += leftPad(terminalId, 8); // 1 : terminal id
211
+ m += kReserved; // 9 : reserved
212
+ m += 's'; // 10 : message code (lowercase per spec)
213
+ return m; // 10
214
+ }
215
+
216
+ std::string Ecr17Protocol::buildReversalMessage(const std::string& terminalId,
217
+ const std::string& cashRegisterId,
218
+ const std::string& stan) {
219
+ std::string m;
220
+ m += leftPad(terminalId, 8); // 1 : terminal id
221
+ m += kReserved; // 9 : reserved
222
+ m += 'S'; // 10 : message code
223
+ m += leftPad(cashRegisterId, 8); // 11 : cash register id
224
+ m += leftPad(stan, 6); // 19 : STAN ("000000" = no check)
225
+ m += kReserved; // 25 : presence of additional GT data
226
+ m += kReserved; // 26 : reserved
227
+ return m; // 26
228
+ }
229
+
230
+ std::string Ecr17Protocol::buildVasMessage(const std::string& terminalId, const std::string& ecrId,
231
+ const std::string& xmlRequest) {
232
+ if (xmlRequest.size() > 1024) {
233
+ throw std::invalid_argument("ECR17: VAS request exceeds 1024 bytes");
234
+ }
235
+ std::string m;
236
+ m += leftPad(terminalId, 8); // 1 : terminal id
237
+ m += kReserved; // 9 : reserved
238
+ m += 'K'; // 10 : message code
239
+ m += leftPad(ecrId, 8); // 11 : ECR identifier
240
+ m += "000"; // 19 : reserved (3)
241
+ m += kReserved; // 22 : reserved (1)
242
+ m += leftPad(std::to_string(xmlRequest.size()), 4);// 23 : VAS request length (4)
243
+ m += xmlRequest; // 27 : VAS request (XML)
244
+ return m;
245
+ }
246
+
247
+ std::string Ecr17Protocol::buildAdditionalTagsMessage(const std::string& terminalId,
248
+ const std::string& tagContent,
249
+ const std::string& isoField,
250
+ const std::string& tagNumber) {
251
+ if (tagContent.empty() || tagContent.size() > 100) {
252
+ throw std::invalid_argument("ECR17: additional TAG content must be 1..100 chars");
253
+ }
254
+ std::string m;
255
+ m += leftPad(terminalId, 8); // 1 : terminal id
256
+ m += kReserved; // 9 : reserved
257
+ m += 'U'; // 10 : message code
258
+ m += "000000"; // 11 : payment type (6) -> standard payment
259
+ m += leftPad(isoField, 2); // 17 : ISO field number (e.g. "62")
260
+ m += rightPad(tagNumber, 8); // 19 : TAG number, left-justified, blank-filled
261
+ m += kReserved; // 27 : reserved (1)
262
+ m += "0000"; // 28 : exclusive TAG index bytemap (none to send to GT)
263
+ m += "00000"; // 32 : reserved (5)
264
+ m += tagContent; // 37 : privative TAG content (1..100)
265
+ m += kFieldSep; // end-of-field (0x1B)
266
+ return m;
267
+ }
268
+
269
+ std::string Ecr17Protocol::formatTokenizationTag(bool recurring, const std::string& contractCode) {
270
+ if (contractCode.empty() || contractCode.size() > 18) {
271
+ throw std::invalid_argument("ECR17: tokenization contract code must be 1..18 chars");
272
+ }
273
+ const std::string service = recurring ? "0REC" : "0COF";
274
+ return service + "0TRK" + contractCode + "|0FNZ03";
275
+ }
276
+
277
+ } // namespace margelo::nitro::ecr17
@@ -0,0 +1,103 @@
1
+ #pragma once
2
+
3
+ #include <string>
4
+
5
+ namespace margelo::nitro::ecr17 {
6
+
7
+ // Builds ECR17 application-message payloads (the bytes that go between STX and
8
+ // ETX). All fields are fixed-width and validated; values that overflow their
9
+ // field throw std::invalid_argument so a malformed frame is never produced.
10
+ //
11
+ // `paymentType` is the single request digit: '0' auto, '1' debit, '2' credit,
12
+ // '3' other. `cardAlreadyPresent` maps to field "start with card already
13
+ // inserted". `receiptText` is the contract/print text (max 128 chars).
14
+ class Ecr17Protocol {
15
+ public:
16
+ // --- Payment family (167 bytes): 'P' payment, 'X' extended, 'p' pre-auth ---
17
+ static std::string buildPaymentMessage(const std::string& terminalId,
18
+ const std::string& cashRegisterId, int amountCents,
19
+ char paymentType = '0', bool cardAlreadyPresent = false,
20
+ bool withAdditionalData = false,
21
+ const std::string& receiptText = "");
22
+
23
+ static std::string buildExtendedPaymentMessage(const std::string& terminalId,
24
+ const std::string& cashRegisterId, int amountCents,
25
+ char paymentType = '0',
26
+ bool cardAlreadyPresent = false,
27
+ bool withAdditionalData = false,
28
+ const std::string& receiptText = "");
29
+
30
+ static std::string buildPreAuthMessage(const std::string& terminalId,
31
+ const std::string& cashRegisterId, int amountCents,
32
+ char paymentType = '0', bool cardAlreadyPresent = false,
33
+ bool withAdditionalData = false,
34
+ const std::string& receiptText = "");
35
+
36
+ // --- Pre-auth integration/closure (176 bytes): 'i' incremental, 'c' closure ---
37
+ static std::string buildIncrementalMessage(const std::string& terminalId,
38
+ const std::string& cashRegisterId, int amountCents,
39
+ const std::string& originalPreAuthCode,
40
+ bool withAdditionalData = false,
41
+ const std::string& receiptText = "");
42
+
43
+ static std::string buildPreAuthClosureMessage(const std::string& terminalId,
44
+ const std::string& cashRegisterId, int amountCents,
45
+ const std::string& originalPreAuthCode,
46
+ bool withAdditionalData = false,
47
+ const std::string& receiptText = "");
48
+
49
+ // --- Card verification 'H' (39 bytes) ---
50
+ static std::string buildCardVerificationMessage(const std::string& terminalId,
51
+ const std::string& cashRegisterId,
52
+ char paymentType = '0',
53
+ bool withAdditionalData = false);
54
+
55
+ // --- Session commands (26 bytes): 'C' close, 'T' totals ---
56
+ static std::string buildCloseSessionMessage(const std::string& terminalId,
57
+ const std::string& cashRegisterId,
58
+ bool withAdditionalData = false);
59
+
60
+ static std::string buildTotalsMessage(const std::string& terminalId,
61
+ const std::string& cashRegisterId,
62
+ bool withAdditionalData = false);
63
+
64
+ // --- Send last result 'G' (22 bytes) ---
65
+ static std::string buildSendLastResultMessage(const std::string& terminalId,
66
+ const std::string& cashRegisterId,
67
+ bool withAdditionalData = false);
68
+
69
+ // --- Enable/disable ECR printing 'E' (11 bytes) ---
70
+ static std::string buildEnableEcrPrintMessage(const std::string& terminalId, bool enabled);
71
+
72
+ // --- Reprint ticket 'R' (22 bytes) ---
73
+ static std::string buildReprintMessage(const std::string& terminalId, bool toEcr,
74
+ char ticketType = '0');
75
+
76
+ // --- Status 's' (10 bytes) ---
77
+ static std::string buildStatusMessage(const std::string& terminalId);
78
+
79
+ // --- Reversal 'S' (26 bytes); stan "000000" = reverse last, no STAN check ---
80
+ static std::string buildReversalMessage(const std::string& terminalId,
81
+ const std::string& cashRegisterId,
82
+ const std::string& stan = "000000");
83
+
84
+ // --- VAS 'K' (variable, length-prefixed XML, max 1024) ---
85
+ static std::string buildVasMessage(const std::string& terminalId, const std::string& ecrId,
86
+ const std::string& xmlRequest);
87
+
88
+ // --- Additional data for GT / tokenization 'U' (variable) ---
89
+ // `tagContent` is the privative TAG content (1..100 chars), terminated with
90
+ // 0x1B by this builder. Use formatTokenizationTag() to produce it.
91
+ static std::string buildAdditionalTagsMessage(const std::string& terminalId,
92
+ const std::string& tagContent,
93
+ const std::string& isoField = "62",
94
+ const std::string& tagNumber = "DF8D01");
95
+
96
+ // Formats the TAG 5 content for tokenization (Intesa-style mapping):
97
+ // "0COF0TRK<contract>|0FNZ03" (unscheduled/one-click)
98
+ // "0REC0TRK<contract>|0FNZ03" (recurring)
99
+ // `recurring` selects 0REC vs 0COF.
100
+ static std::string formatTokenizationTag(bool recurring, const std::string& contractCode);
101
+ };
102
+
103
+ } // namespace margelo::nitro::ecr17
@@ -0,0 +1,155 @@
1
+ #include "Ecr17Response.hpp"
2
+
3
+ #include <algorithm>
4
+ #include <cctype>
5
+
6
+ namespace margelo::nitro::ecr17 {
7
+
8
+ namespace {
9
+
10
+ // 1-based field extractor. Returns "" if the field starts beyond the payload;
11
+ // clamps the length to whatever bytes are actually present (defensive parsing).
12
+ std::string at(const std::string& p, size_t pos1, size_t len) {
13
+ if (pos1 == 0 || pos1 > p.size()) {
14
+ return "";
15
+ }
16
+ const size_t i = pos1 - 1;
17
+ return p.substr(i, std::min(len, p.size() - i));
18
+ }
19
+
20
+ std::string trimRight(const std::string& s) {
21
+ const size_t end = s.find_last_not_of(' ');
22
+ return end == std::string::npos ? std::string() : s.substr(0, end + 1);
23
+ }
24
+
25
+ // Extracts the value of a Nexi VAS XML param: <p k="KEY">value</p>.
26
+ std::string xmlValue(const std::string& xml, const std::string& key) {
27
+ const std::string needle = "\"" + key + "\">";
28
+ const size_t start = xml.find(needle);
29
+ if (start == std::string::npos) {
30
+ return "";
31
+ }
32
+ const size_t from = start + needle.size();
33
+ const size_t end = xml.find('<', from);
34
+ std::string value = xml.substr(from, end == std::string::npos ? std::string::npos : end - from);
35
+ // trim surrounding whitespace
36
+ const size_t a = value.find_first_not_of(" \t\r\n");
37
+ const size_t b = value.find_last_not_of(" \t\r\n");
38
+ return a == std::string::npos ? std::string() : value.substr(a, b - a + 1);
39
+ }
40
+
41
+ } // namespace
42
+
43
+ Outcome outcomeFromCode(const std::string& code) {
44
+ if (code == "00") return Outcome::Ok;
45
+ if (code == "01") return Outcome::Ko;
46
+ if (code == "05") return Outcome::CardNotPresent;
47
+ if (code == "09") return Outcome::UnknownTag;
48
+ return Outcome::Unknown;
49
+ }
50
+
51
+ PaymentResponse Ecr17Response::parsePayment(const std::string& p) {
52
+ PaymentResponse r;
53
+ const std::string code = at(p, 10, 1); // message code 'E' (plain) or 'V' (DCC)
54
+ const bool dcc = code == "V";
55
+
56
+ r.resultCode = at(p, 11, 2);
57
+ r.outcome = outcomeFromCode(r.resultCode);
58
+
59
+ if (r.outcome == Outcome::Ko) {
60
+ r.errorDescription = trimRight(at(p, 13, 24));
61
+ } else {
62
+ r.pan = at(p, 13, 19);
63
+ r.transactionType = trimRight(at(p, 32, 3));
64
+ r.authCode = trimRight(at(p, 35, 6));
65
+ r.hostDateTime = at(p, 41, 7);
66
+ }
67
+
68
+ // Common to any response.
69
+ r.cardType = at(p, 48, 1);
70
+ r.acquirerId = trimRight(at(p, 49, 11));
71
+ r.stan = at(p, 60, 6);
72
+ r.onlineId = at(p, 66, 6);
73
+
74
+ if (dcc) {
75
+ r.currency.applied = at(p, 83, 1) == "1";
76
+ r.currency.rate = at(p, 84, 8);
77
+ r.currency.currencyCode = trimRight(at(p, 92, 3));
78
+ r.currency.amount = at(p, 95, 12);
79
+ r.currency.precision = at(p, 107, 1);
80
+ }
81
+ return r;
82
+ }
83
+
84
+ StatusResponse Ecr17Response::parseStatus(const std::string& p) {
85
+ StatusResponse r;
86
+ r.terminalId = at(p, 1, 8);
87
+ r.dateTimeRaw = at(p, 21, 10);
88
+ const std::string s = at(p, 31, 1);
89
+ r.status = (!s.empty() && std::isdigit(static_cast<unsigned char>(s[0]))) ? s[0] - '0' : -1;
90
+ r.softwareRelease = trimRight(at(p, 32, p.size()));
91
+ return r;
92
+ }
93
+
94
+ TotalsResponse Ecr17Response::parseTotals(const std::string& p) {
95
+ TotalsResponse r;
96
+ r.resultCode = at(p, 11, 2);
97
+ r.outcome = outcomeFromCode(r.resultCode);
98
+ r.posTotal = at(p, 13, 16);
99
+ return r;
100
+ }
101
+
102
+ CloseResponse Ecr17Response::parseClose(const std::string& p) {
103
+ CloseResponse r;
104
+ r.resultCode = at(p, 11, 2);
105
+ r.outcome = outcomeFromCode(r.resultCode);
106
+ if (r.outcome == Outcome::Ok) {
107
+ r.posTotal = at(p, 13, 16);
108
+ r.hostTotal = at(p, 29, 16);
109
+ } else {
110
+ r.errorDescription = trimRight(at(p, 13, 19));
111
+ r.actionCode = at(p, 32, 3);
112
+ }
113
+ return r;
114
+ }
115
+
116
+ PreAuthResponse Ecr17Response::parsePreAuth(const std::string& p) {
117
+ PreAuthResponse r;
118
+ r.resultCode = at(p, 11, 2);
119
+ r.outcome = outcomeFromCode(r.resultCode);
120
+ if (r.outcome == Outcome::Ko) {
121
+ r.errorDescription = trimRight(at(p, 13, 24));
122
+ r.actionCode = at(p, 37, 3);
123
+ } else {
124
+ r.pan = at(p, 13, 19);
125
+ r.transactionType = trimRight(at(p, 32, 3));
126
+ r.authCode = trimRight(at(p, 35, 6));
127
+ r.preAuthorizedAmount = at(p, 41, 8);
128
+ r.preAuthCode = at(p, 49, 9);
129
+ r.actionCode = at(p, 58, 3);
130
+ r.hostDateTime = at(p, 61, 7);
131
+ }
132
+ // In the OK layout preAuthorizedAmount occupies positions 41-48, so position
133
+ // 48 is the amount's last digit, NOT a card type. Only read cardType for the
134
+ // KO layout, where that byte is not part of an amount field.
135
+ if (r.outcome == Outcome::Ko) {
136
+ r.cardType = at(p, 48, 1);
137
+ }
138
+ r.acquirerId = trimRight(at(p, 72, 11));
139
+ r.stan = at(p, 83, 6);
140
+ r.onlineId = at(p, 89, 6);
141
+ return r;
142
+ }
143
+
144
+ VasResponse Ecr17Response::parseVas(const std::string& p) {
145
+ VasResponse r;
146
+ r.moreMessages = at(p, 15, 1) == "1";
147
+ r.idMessage = at(p, 16, 3);
148
+ r.rawXml = at(p, 27, p.size());
149
+ r.responseId = xmlValue(r.rawXml, "RESPID");
150
+ r.responseMessage = xmlValue(r.rawXml, "RESPMSG");
151
+ r.orderId = xmlValue(r.rawXml, "ORDER_ID");
152
+ return r;
153
+ }
154
+
155
+ } // namespace margelo::nitro::ecr17
@@ -0,0 +1,113 @@
1
+ #pragma once
2
+
3
+ #include <cstdint>
4
+ #include <string>
5
+
6
+ // Parsers for ECR17 terminal *response* application messages. They take the
7
+ // application payload (the bytes between STX and ETX, i.e. DecodedPacket.payload)
8
+ // and return plain C++ structs — intentionally independent of Nitro so they can
9
+ // be unit-tested standalone. HybridEcr17Client maps these to the generated Nitro
10
+ // result types. Field offsets follow the spec response tables in docs/.
11
+ //
12
+ // Parsing is defensive: fields beyond the payload length come back empty rather
13
+ // than throwing, so a short/truncated response degrades gracefully.
14
+
15
+ namespace margelo::nitro::ecr17 {
16
+
17
+ enum class Outcome {
18
+ Ok, // "00"
19
+ Ko, // "01"
20
+ CardNotPresent, // "05"
21
+ UnknownTag, // "09"
22
+ Unknown, // anything else / missing
23
+ };
24
+
25
+ Outcome outcomeFromCode(const std::string& code);
26
+
27
+ // Optional DCC / currency-exchange block (parsed from a 'V'/'v' response).
28
+ // Named DccInfo (not CurrencyExchange) to avoid clashing with the Nitro-generated
29
+ // CurrencyExchange struct in the same namespace.
30
+ struct DccInfo {
31
+ bool applied = false;
32
+ std::string rate; // 8 digits, 4 decimals
33
+ std::string currencyCode; // alpha-3
34
+ std::string amount; // 12 digits, in transaction currency
35
+ std::string precision; // decimals
36
+ };
37
+
38
+ // Result of a payment-family response ('E' without DCC, 'V' with DCC). Reused
39
+ // for reversal / card verification / pre-auth closure which share the layout.
40
+ struct PaymentResponse {
41
+ Outcome outcome = Outcome::Unknown;
42
+ std::string resultCode; // raw "00"/"01"/"05"/"09"
43
+ std::string pan; // positive
44
+ std::string transactionType; // positive, raw "ICC"/"MAG"/...
45
+ std::string authCode; // positive
46
+ std::string hostDateTime; // positive, raw DDDHHMM
47
+ std::string errorDescription; // negative
48
+ std::string cardType; // common, raw "1"/"2"/"3"
49
+ std::string acquirerId; // common
50
+ std::string stan; // common
51
+ std::string onlineId; // common
52
+ DccInfo currency; // only when DCC present
53
+ };
54
+
55
+ struct StatusResponse {
56
+ std::string terminalId;
57
+ std::string dateTimeRaw; // "DDMMYYhhmm"
58
+ int status = -1; // 0..6, -1 = unknown/missing
59
+ std::string softwareRelease;
60
+ };
61
+
62
+ struct TotalsResponse {
63
+ Outcome outcome = Outcome::Unknown;
64
+ std::string resultCode;
65
+ std::string posTotal; // 16 digits, cents
66
+ };
67
+
68
+ struct CloseResponse {
69
+ Outcome outcome = Outcome::Unknown;
70
+ std::string resultCode;
71
+ std::string posTotal; // positive, 16 digits
72
+ std::string hostTotal; // positive, 16 digits
73
+ std::string errorDescription; // negative
74
+ std::string actionCode; // negative
75
+ };
76
+
77
+ struct PreAuthResponse {
78
+ Outcome outcome = Outcome::Unknown;
79
+ std::string resultCode;
80
+ std::string pan;
81
+ std::string transactionType;
82
+ std::string authCode;
83
+ std::string preAuthorizedAmount; // 8 digits, cents
84
+ std::string preAuthCode; // 9 digits, unique pre-auth id
85
+ std::string actionCode;
86
+ std::string hostDateTime;
87
+ std::string errorDescription;
88
+ std::string cardType;
89
+ std::string acquirerId;
90
+ std::string stan;
91
+ std::string onlineId;
92
+ };
93
+
94
+ struct VasResponse {
95
+ std::string responseId; // RESPID parsed from XML ("0" = OK), "" if absent
96
+ std::string responseMessage; // RESPMSG
97
+ std::string orderId; // ORDER_ID
98
+ bool moreMessages = false; // concatenation flag "1"
99
+ std::string idMessage; // 3-digit sequence
100
+ std::string rawXml; // the XML body of this message
101
+ };
102
+
103
+ class Ecr17Response {
104
+ public:
105
+ static PaymentResponse parsePayment(const std::string& payload);
106
+ static StatusResponse parseStatus(const std::string& payload);
107
+ static TotalsResponse parseTotals(const std::string& payload);
108
+ static CloseResponse parseClose(const std::string& payload);
109
+ static PreAuthResponse parsePreAuth(const std::string& payload);
110
+ static VasResponse parseVas(const std::string& payload);
111
+ };
112
+
113
+ } // namespace margelo::nitro::ecr17