@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,164 @@
1
+ #include <gtest/gtest.h>
2
+
3
+ #include <cstdint>
4
+ #include <string>
5
+ #include <vector>
6
+
7
+ #include "PacketCodec/PacketCodec.hpp"
8
+
9
+ using margelo::nitro::ecr17::DecodedPacket;
10
+ using margelo::nitro::ecr17::LrcMode;
11
+ using margelo::nitro::ecr17::PacketCodec;
12
+ using margelo::nitro::ecr17::PacketType;
13
+
14
+ namespace {
15
+ constexpr uint8_t kSoh = 0x01;
16
+ constexpr uint8_t kStx = 0x02;
17
+ constexpr uint8_t kEtx = 0x03;
18
+ constexpr uint8_t kEot = 0x04;
19
+ constexpr uint8_t kAck = 0x06;
20
+ constexpr uint8_t kNak = 0x15;
21
+ } // namespace
22
+
23
+ TEST(PacketCodec, EncodeApplicationFramesStxPayloadEtxLrc) {
24
+ PacketCodec codec(LrcMode::STD);
25
+ auto frame = codec.encodeApplication("AB");
26
+ ASSERT_EQ(frame.size(), 5u);
27
+ EXPECT_EQ(frame[0], kStx);
28
+ EXPECT_EQ(frame[1], 'A');
29
+ EXPECT_EQ(frame[2], 'B');
30
+ EXPECT_EQ(frame[3], kEtx);
31
+ EXPECT_EQ(frame[4], 0x7C); // 0x7F ^ 'A' ^ 'B'
32
+ }
33
+
34
+ TEST(PacketCodec, ApplicationRoundTrip) {
35
+ for (LrcMode mode : {LrcMode::STX, LrcMode::STD, LrcMode::NOEXT, LrcMode::STX_NOEXT}) {
36
+ PacketCodec codec(mode);
37
+ const std::string payload = "123456780P0000065000";
38
+ DecodedPacket decoded = codec.decode(codec.encodeApplication(payload));
39
+ EXPECT_EQ(decoded.type, PacketType::APPLICATION);
40
+ EXPECT_EQ(decoded.payload, payload);
41
+ EXPECT_TRUE(decoded.validLrc);
42
+ }
43
+ }
44
+
45
+ TEST(PacketCodec, ApplicationDetectsCorruptedLrc) {
46
+ PacketCodec codec(LrcMode::STD);
47
+ auto frame = codec.encodeApplication("HELLO");
48
+ frame.back() ^= 0xFF; // corrupt the LRC byte
49
+ DecodedPacket decoded = codec.decode(frame);
50
+ EXPECT_EQ(decoded.type, PacketType::APPLICATION);
51
+ EXPECT_EQ(decoded.payload, "HELLO");
52
+ EXPECT_FALSE(decoded.validLrc);
53
+ }
54
+
55
+ TEST(PacketCodec, EncodeControlFramesCtrlEtxLrc) {
56
+ PacketCodec codec(LrcMode::STD);
57
+ auto frame = codec.encodeControl(kAck);
58
+ ASSERT_EQ(frame.size(), 3u);
59
+ EXPECT_EQ(frame[0], kAck);
60
+ EXPECT_EQ(frame[1], kEtx);
61
+ }
62
+
63
+ TEST(PacketCodec, DecodeAck) {
64
+ PacketCodec codec(LrcMode::STD);
65
+ DecodedPacket decoded = codec.decode({kAck});
66
+ EXPECT_EQ(decoded.type, PacketType::ACK);
67
+ EXPECT_TRUE(decoded.validLrc);
68
+ }
69
+
70
+ TEST(PacketCodec, DecodeNak) {
71
+ PacketCodec codec(LrcMode::STD);
72
+ DecodedPacket decoded = codec.decode({kNak});
73
+ EXPECT_EQ(decoded.type, PacketType::NAK);
74
+ EXPECT_TRUE(decoded.validLrc);
75
+ }
76
+
77
+ TEST(PacketCodec, DecodeEmptyIsUnknown) {
78
+ PacketCodec codec(LrcMode::STD);
79
+ DecodedPacket decoded = codec.decode({});
80
+ EXPECT_EQ(decoded.type, PacketType::UNKNOWN);
81
+ EXPECT_FALSE(decoded.validLrc);
82
+ }
83
+
84
+ // Regression: a lone SOH byte previously built a string from an inverted
85
+ // iterator range [begin()+1, end()-1) == [end(), begin()) -> UB/crash.
86
+ TEST(PacketCodec, DecodeLoneSohIsUnknownNotCrash) {
87
+ PacketCodec codec(LrcMode::STD);
88
+ DecodedPacket decoded = codec.decode({kSoh});
89
+ EXPECT_EQ(decoded.type, PacketType::UNKNOWN);
90
+ EXPECT_FALSE(decoded.validLrc);
91
+ }
92
+
93
+ TEST(PacketCodec, DecodeProgressUpdate) {
94
+ PacketCodec codec(LrcMode::STD);
95
+ std::vector<uint8_t> frame{kSoh};
96
+ const std::string msg = "ELABORAZIONE... "; // 20 chars per spec
97
+ frame.insert(frame.end(), msg.begin(), msg.end());
98
+ frame.push_back(kEot);
99
+
100
+ DecodedPacket decoded = codec.decode(frame);
101
+ EXPECT_EQ(decoded.type, PacketType::PROGRESS);
102
+ EXPECT_EQ(decoded.payload, msg);
103
+ }
104
+
105
+ TEST(PacketCodec, DecodeStxWithoutEtxIsUnknown) {
106
+ PacketCodec codec(LrcMode::STD);
107
+ DecodedPacket decoded = codec.decode({kStx, 'A', 'B'});
108
+ EXPECT_EQ(decoded.type, PacketType::UNKNOWN);
109
+ EXPECT_FALSE(decoded.validLrc);
110
+ }
111
+
112
+ // Regression: ETX present but no trailing LRC byte must not read past the end
113
+ // nor mistake ETX for the LRC.
114
+ TEST(PacketCodec, DecodeStxWithEtxButNoLrcIsUnknown) {
115
+ PacketCodec codec(LrcMode::STD);
116
+ DecodedPacket decoded = codec.decode({kStx, 'A', kEtx});
117
+ EXPECT_EQ(decoded.type, PacketType::UNKNOWN);
118
+ EXPECT_FALSE(decoded.validLrc);
119
+ }
120
+
121
+ TEST(PacketCodec, DecodeUnknownLeadByte) {
122
+ PacketCodec codec(LrcMode::STD);
123
+ DecodedPacket decoded = codec.decode({0x99, 0x00});
124
+ EXPECT_EQ(decoded.type, PacketType::UNKNOWN);
125
+ EXPECT_FALSE(decoded.validLrc);
126
+ }
127
+
128
+ // Regression: trailing bytes after a complete frame's LRC must not be silently
129
+ // accepted (the LRC must be the final byte).
130
+ TEST(PacketCodec, DecodeStxWithTrailingBytesAfterLrcIsUnknown) {
131
+ PacketCodec codec(LrcMode::STD);
132
+ auto frame = codec.encodeApplication("AB"); // STX A B ETX LRC
133
+ frame.push_back(0x00); // stray trailing byte
134
+ DecodedPacket decoded = codec.decode(frame);
135
+ EXPECT_EQ(decoded.type, PacketType::UNKNOWN);
136
+ EXPECT_FALSE(decoded.validLrc);
137
+ }
138
+
139
+ // Regression: a coalesced read holding two frames must not be reported as one
140
+ // valid APPLICATION packet (which would silently drop the second frame).
141
+ // Framing/splitting a byte stream is the transport layer's job.
142
+ TEST(PacketCodec, DecodeCoalescedFramesIsUnknown) {
143
+ PacketCodec codec(LrcMode::STD);
144
+ auto first = codec.encodeApplication("AB");
145
+ auto second = codec.encodeApplication("CD");
146
+ first.insert(first.end(), second.begin(), second.end());
147
+ DecodedPacket decoded = codec.decode(first);
148
+ EXPECT_EQ(decoded.type, PacketType::UNKNOWN);
149
+ EXPECT_FALSE(decoded.validLrc);
150
+ }
151
+
152
+ // Regression: SOH frame whose last byte is not EOT must not be accepted as a
153
+ // valid PROGRESS packet. Only SOH + payload + EOT is well-formed per spec.
154
+ TEST(PacketCodec, DecodeSohWithoutEotIsUnknown) {
155
+ PacketCodec codec(LrcMode::STD);
156
+ // SOH + 20-char message, but terminated with 0xFF instead of EOT.
157
+ std::vector<uint8_t> frame{kSoh};
158
+ const std::string msg = "ELABORAZIONE... ";
159
+ frame.insert(frame.end(), msg.begin(), msg.end());
160
+ frame.push_back(0xFF); // wrong terminator
161
+ DecodedPacket decoded = codec.decode(frame);
162
+ EXPECT_EQ(decoded.type, PacketType::UNKNOWN);
163
+ EXPECT_FALSE(decoded.validLrc);
164
+ }
@@ -0,0 +1,102 @@
1
+ #include <gtest/gtest.h>
2
+
3
+ #include <stdexcept>
4
+ #include <string>
5
+
6
+ #include "Ecr17Protocol/Ecr17Protocol.hpp"
7
+
8
+ using margelo::nitro::ecr17::Ecr17Protocol;
9
+
10
+ // Field layout reference (1-based positions from the Nexi ECR17 spec, Payment
11
+ // request "P"):
12
+ // 1 8 Terminal ID
13
+ // 9 1 Reserved '0'
14
+ // 10 1 Message code 'P'
15
+ // 11 8 Cash register ID
16
+ // 19 1 Presence of additional data for GT
17
+ // 20 2 Reserved "00"
18
+ // 22 1 Start transaction when card already present
19
+ // 23 1 Payment type
20
+ // 24 8 Transaction amount (right aligned, '0' filled)
21
+ // 32 128 Text to print (' ' filled)
22
+ // 160 8 Reserved '0' => total 167 bytes
23
+
24
+ TEST(Protocol, StatusMessageLayout) {
25
+ std::string m = Ecr17Protocol::buildStatusMessage("42");
26
+ ASSERT_EQ(m.size(), 10u);
27
+ EXPECT_EQ(m.substr(0, 8), "00000042"); // terminal id, left-padded
28
+ EXPECT_EQ(m[8], '0'); // reserved
29
+ EXPECT_EQ(m[9], 's'); // lowercase message code per spec
30
+ }
31
+
32
+ TEST(Protocol, StatusMessageKeepsFullWidthId) {
33
+ std::string m = Ecr17Protocol::buildStatusMessage("12345678");
34
+ EXPECT_EQ(m, "123456780s");
35
+ }
36
+
37
+ TEST(Protocol, PaymentMessageIs167Bytes) {
38
+ std::string m = Ecr17Protocol::buildPaymentMessage("1", "2", 650);
39
+ EXPECT_EQ(m.size(), 167u);
40
+ }
41
+
42
+ TEST(Protocol, PaymentMessageFieldLayout) {
43
+ std::string m = Ecr17Protocol::buildPaymentMessage("12345678", "87654321", 650);
44
+ ASSERT_EQ(m.size(), 167u);
45
+ EXPECT_EQ(m.substr(0, 8), "12345678"); // terminal id
46
+ EXPECT_EQ(m[8], '0'); // reserved
47
+ EXPECT_EQ(m[9], 'P'); // message code
48
+ EXPECT_EQ(m.substr(10, 8), "87654321"); // cash register id
49
+ EXPECT_EQ(m[18], '0'); // presence of additional data
50
+ EXPECT_EQ(m.substr(19, 2), "00"); // reserved
51
+ EXPECT_EQ(m[21], '0'); // start-with-card
52
+ EXPECT_EQ(m[22], '0'); // payment type
53
+ EXPECT_EQ(m.substr(23, 8), "00000650"); // amount, right aligned
54
+ EXPECT_EQ(m.substr(31, 128), std::string(128, ' ')); // text field
55
+ EXPECT_EQ(m.substr(159, 8), "00000000"); // trailing reserved
56
+ }
57
+
58
+ TEST(Protocol, PaymentMessageAmountMaxFits) {
59
+ std::string m = Ecr17Protocol::buildPaymentMessage("1", "2", 99999999);
60
+ EXPECT_EQ(m.substr(23, 8), "99999999");
61
+ }
62
+
63
+ TEST(Protocol, PaymentRejectsNegativeAmount) {
64
+ EXPECT_THROW(Ecr17Protocol::buildPaymentMessage("1", "2", -1), std::invalid_argument);
65
+ }
66
+
67
+ TEST(Protocol, PaymentRejectsAmountOverflowingField) {
68
+ // 9 digits does not fit the 8-byte amount field.
69
+ EXPECT_THROW(Ecr17Protocol::buildPaymentMessage("1", "2", 100000000), std::invalid_argument);
70
+ }
71
+
72
+ TEST(Protocol, PaymentRejectsOversizedTerminalId) {
73
+ EXPECT_THROW(Ecr17Protocol::buildPaymentMessage("123456789", "2", 650), std::invalid_argument);
74
+ }
75
+
76
+ TEST(Protocol, StatusRejectsOversizedTerminalId) {
77
+ EXPECT_THROW(Ecr17Protocol::buildStatusMessage("123456789"), std::invalid_argument);
78
+ }
79
+
80
+ // Reversal request "S" layout (1-based spec positions):
81
+ // 1 8 Terminal ID · 9 1 Reserved · 10 1 'S' · 11 8 Cash register ID
82
+ // 19 6 STAN · 25 1 Presence of GT data · 26 1 Reserved => 26 bytes
83
+ TEST(Protocol, ReversalMessageLayout) {
84
+ std::string m = Ecr17Protocol::buildReversalMessage("12345678", "87654321", "000123");
85
+ ASSERT_EQ(m.size(), 26u);
86
+ EXPECT_EQ(m.substr(0, 8), "12345678");
87
+ EXPECT_EQ(m[8], '0');
88
+ EXPECT_EQ(m[9], 'S');
89
+ EXPECT_EQ(m.substr(10, 8), "87654321");
90
+ EXPECT_EQ(m.substr(18, 6), "000123");
91
+ EXPECT_EQ(m[24], '0');
92
+ EXPECT_EQ(m[25], '0');
93
+ }
94
+
95
+ TEST(Protocol, ReversalDefaultStanIsNoCheck) {
96
+ std::string m = Ecr17Protocol::buildReversalMessage("12345678", "87654321");
97
+ EXPECT_EQ(m.substr(18, 6), "000000");
98
+ }
99
+
100
+ TEST(Protocol, ReversalRejectsOversizedStan) {
101
+ EXPECT_THROW(Ecr17Protocol::buildReversalMessage("1", "2", "1234567"), std::invalid_argument);
102
+ }
@@ -0,0 +1,190 @@
1
+ // Byte-layout tests for the full ECR17 command builder set (Phase 1).
2
+ // Positions are checked against the Nexi ECR17 spec tables in docs/.
3
+
4
+ #include <gtest/gtest.h>
5
+
6
+ #include <stdexcept>
7
+ #include <string>
8
+
9
+ #include "Ecr17Protocol/Ecr17Protocol.hpp"
10
+
11
+ using margelo::nitro::ecr17::Ecr17Protocol;
12
+
13
+ namespace {
14
+ constexpr char kFieldSep = 0x1B;
15
+ const std::string T = "12345678"; // terminal id
16
+ const std::string C = "87654321"; // cash register id
17
+ } // namespace
18
+
19
+ // --- Payment family (167 bytes) --------------------------------------------
20
+
21
+ TEST(Commands, ExtendedPaymentLayoutAndFlags) {
22
+ std::string m = Ecr17Protocol::buildExtendedPaymentMessage(T, C, 650, '2', true, true, "ABC");
23
+ ASSERT_EQ(m.size(), 167u);
24
+ EXPECT_EQ(m.substr(0, 8), T);
25
+ EXPECT_EQ(m[8], '0');
26
+ EXPECT_EQ(m[9], 'X');
27
+ EXPECT_EQ(m.substr(10, 8), C);
28
+ EXPECT_EQ(m[18], '1'); // withAdditionalData
29
+ EXPECT_EQ(m.substr(19, 2), "00");
30
+ EXPECT_EQ(m[21], '1'); // cardAlreadyPresent
31
+ EXPECT_EQ(m[22], '2'); // payment type
32
+ EXPECT_EQ(m.substr(23, 8), "00000650"); // amount
33
+ EXPECT_EQ(m.substr(31, 125), std::string(125, ' ')); // text left-padding
34
+ EXPECT_EQ(m.substr(156, 3), "ABC"); // text right-aligned
35
+ EXPECT_EQ(m.substr(159, 8), "00000000");
36
+ }
37
+
38
+ TEST(Commands, PreAuthUsesCodeLowerP) {
39
+ std::string m = Ecr17Protocol::buildPreAuthMessage(T, C, 1000);
40
+ ASSERT_EQ(m.size(), 167u);
41
+ EXPECT_EQ(m[9], 'p');
42
+ EXPECT_EQ(m.substr(23, 8), "00001000");
43
+ }
44
+
45
+ TEST(Commands, PaymentDefaultsMatchBasicLayout) {
46
+ std::string m = Ecr17Protocol::buildPaymentMessage(T, C, 650);
47
+ ASSERT_EQ(m.size(), 167u);
48
+ EXPECT_EQ(m[9], 'P');
49
+ EXPECT_EQ(m[18], '0'); // no additional data by default
50
+ EXPECT_EQ(m[21], '0'); // card not present by default
51
+ EXPECT_EQ(m[22], '0'); // auto payment type by default
52
+ EXPECT_EQ(m.substr(31, 128), std::string(128, ' '));
53
+ }
54
+
55
+ // --- Pre-auth integration / closure (176 bytes) ----------------------------
56
+
57
+ TEST(Commands, IncrementalLayout) {
58
+ std::string m = Ecr17Protocol::buildIncrementalMessage(T, C, 1000, "123456789");
59
+ ASSERT_EQ(m.size(), 176u);
60
+ EXPECT_EQ(m[9], 'i');
61
+ EXPECT_EQ(m.substr(19, 4), "0000");
62
+ EXPECT_EQ(m.substr(23, 8), "00001000");
63
+ EXPECT_EQ(m.substr(159, 9), "123456789"); // original pre-auth code
64
+ EXPECT_EQ(m.substr(168, 8), "00000000");
65
+ }
66
+
67
+ TEST(Commands, PreAuthClosureLayout) {
68
+ std::string m = Ecr17Protocol::buildPreAuthClosureMessage(T, C, 500, "000000042");
69
+ ASSERT_EQ(m.size(), 176u);
70
+ EXPECT_EQ(m[9], 'c');
71
+ EXPECT_EQ(m.substr(159, 9), "000000042");
72
+ }
73
+
74
+ // --- Card verification (39 bytes) ------------------------------------------
75
+
76
+ TEST(Commands, CardVerificationLayout) {
77
+ std::string m = Ecr17Protocol::buildCardVerificationMessage(T, C, '1');
78
+ ASSERT_EQ(m.size(), 39u);
79
+ EXPECT_EQ(m[9], 'H');
80
+ EXPECT_EQ(m.substr(10, 8), C);
81
+ EXPECT_EQ(m[18], '0'); // no additional data
82
+ EXPECT_EQ(m.substr(19, 2), "00");
83
+ EXPECT_EQ(m[21], '0'); // standard verification
84
+ EXPECT_EQ(m[22], '1'); // payment type
85
+ EXPECT_EQ(m.substr(23, 16), std::string(16, '0'));
86
+ }
87
+
88
+ // --- Session commands (26 bytes) -------------------------------------------
89
+
90
+ TEST(Commands, CloseSessionLayout) {
91
+ std::string m = Ecr17Protocol::buildCloseSessionMessage(T, C);
92
+ ASSERT_EQ(m.size(), 26u);
93
+ EXPECT_EQ(m[9], 'C');
94
+ EXPECT_EQ(m.substr(10, 8), C);
95
+ EXPECT_EQ(m[18], '0');
96
+ EXPECT_EQ(m.substr(19, 7), std::string(7, '0'));
97
+ }
98
+
99
+ TEST(Commands, TotalsLayout) {
100
+ std::string m = Ecr17Protocol::buildTotalsMessage(T, C);
101
+ ASSERT_EQ(m.size(), 26u);
102
+ EXPECT_EQ(m[9], 'T');
103
+ }
104
+
105
+ // --- Send last result (22 bytes) -------------------------------------------
106
+
107
+ TEST(Commands, SendLastResultLayout) {
108
+ std::string m = Ecr17Protocol::buildSendLastResultMessage(T, C);
109
+ ASSERT_EQ(m.size(), 22u);
110
+ EXPECT_EQ(m[9], 'G');
111
+ EXPECT_EQ(m.substr(19, 3), "000");
112
+ }
113
+
114
+ // --- Enable/disable ECR printing (11 bytes) --------------------------------
115
+
116
+ TEST(Commands, EnableEcrPrintLayout) {
117
+ EXPECT_EQ(Ecr17Protocol::buildEnableEcrPrintMessage(T, true), "123456780E1");
118
+ EXPECT_EQ(Ecr17Protocol::buildEnableEcrPrintMessage(T, false), "123456780E0");
119
+ }
120
+
121
+ // --- Reprint (22 bytes) -----------------------------------------------------
122
+
123
+ TEST(Commands, ReprintLayout) {
124
+ std::string m = Ecr17Protocol::buildReprintMessage(T, true);
125
+ ASSERT_EQ(m.size(), 22u);
126
+ EXPECT_EQ(m[9], 'R');
127
+ EXPECT_EQ(m[10], '1'); // send to ECR
128
+ EXPECT_EQ(m[11], '0'); // ticket type default
129
+ EXPECT_EQ(m.substr(12, 10), std::string(10, '0'));
130
+ }
131
+
132
+ // --- VAS (variable, length-prefixed) ---------------------------------------
133
+
134
+ TEST(Commands, VasLayoutAndLengthPrefix) {
135
+ std::string m = Ecr17Protocol::buildVasMessage(T, C, "<x/>");
136
+ ASSERT_EQ(m.size(), 30u);
137
+ EXPECT_EQ(m[9], 'K');
138
+ EXPECT_EQ(m.substr(10, 8), C);
139
+ EXPECT_EQ(m.substr(18, 3), "000");
140
+ EXPECT_EQ(m[21], '0');
141
+ EXPECT_EQ(m.substr(22, 4), "0004"); // length of "<x/>"
142
+ EXPECT_EQ(m.substr(26), "<x/>");
143
+ }
144
+
145
+ TEST(Commands, VasRejectsOversizedRequest) {
146
+ EXPECT_THROW(Ecr17Protocol::buildVasMessage(T, C, std::string(1025, 'x')), std::invalid_argument);
147
+ }
148
+
149
+ // --- Additional data / tokenization 'U' ------------------------------------
150
+
151
+ TEST(Commands, AdditionalTagsLayout) {
152
+ const std::string content = "0COF0TRK123|0FNZ03"; // 18 chars
153
+ std::string m = Ecr17Protocol::buildAdditionalTagsMessage(T, content);
154
+ ASSERT_EQ(m.size(), 36u + content.size() + 1u);
155
+ EXPECT_EQ(m[9], 'U');
156
+ EXPECT_EQ(m.substr(10, 6), "000000");
157
+ EXPECT_EQ(m.substr(16, 2), "62");
158
+ EXPECT_EQ(m.substr(18, 8), "DF8D01 "); // left-justified, blank-filled
159
+ EXPECT_EQ(m[26], '0');
160
+ EXPECT_EQ(m.substr(27, 4), "0000");
161
+ EXPECT_EQ(m.substr(31, 5), "00000");
162
+ EXPECT_EQ(m.substr(36, content.size()), content);
163
+ EXPECT_EQ(m.back(), kFieldSep);
164
+ }
165
+
166
+ TEST(Commands, AdditionalTagsRejectsBadContent) {
167
+ EXPECT_THROW(Ecr17Protocol::buildAdditionalTagsMessage(T, ""), std::invalid_argument);
168
+ EXPECT_THROW(Ecr17Protocol::buildAdditionalTagsMessage(T, std::string(101, 'x')),
169
+ std::invalid_argument);
170
+ }
171
+
172
+ TEST(Commands, TokenizationTagFormat) {
173
+ EXPECT_EQ(Ecr17Protocol::formatTokenizationTag(false, "1666354841608"),
174
+ "0COF0TRK1666354841608|0FNZ03");
175
+ EXPECT_EQ(Ecr17Protocol::formatTokenizationTag(true, "ABC"), "0REC0TRKABC|0FNZ03");
176
+ EXPECT_THROW(Ecr17Protocol::formatTokenizationTag(false, ""), std::invalid_argument);
177
+ EXPECT_THROW(Ecr17Protocol::formatTokenizationTag(false, std::string(19, 'x')),
178
+ std::invalid_argument);
179
+ }
180
+
181
+ // --- Validation shared via leftPad -----------------------------------------
182
+
183
+ TEST(Commands, IncrementalRejectsOversizedPreAuthCode) {
184
+ EXPECT_THROW(Ecr17Protocol::buildIncrementalMessage(T, C, 100, "1234567890"),
185
+ std::invalid_argument); // 10 digits > 9-byte field
186
+ }
187
+
188
+ TEST(Commands, PreAuthRejectsNegativeAmount) {
189
+ EXPECT_THROW(Ecr17Protocol::buildPreAuthMessage(T, C, -1), std::invalid_argument);
190
+ }
@@ -0,0 +1,164 @@
1
+ // Tests for the ECR17 response parsers. Payloads are synthesized field-by-field
2
+ // at the exact 1-based offsets from the spec response tables in docs/. Helpers
3
+ // guarantee each field's width so offsets can't drift from a miscounted space.
4
+
5
+ #include <gtest/gtest.h>
6
+
7
+ #include <string>
8
+
9
+ #include "Ecr17Response/Ecr17Response.hpp"
10
+
11
+ using namespace margelo::nitro::ecr17;
12
+
13
+ namespace {
14
+
15
+ // Left-justified field, right-padded with spaces to `width` (alpha fields).
16
+ std::string a(const std::string& value, size_t width) {
17
+ std::string s = value;
18
+ s.resize(width, ' ');
19
+ return s;
20
+ }
21
+
22
+ // Right-justified numeric field, left-padded with '0' to `width`.
23
+ std::string n(const std::string& value, size_t width) {
24
+ return std::string(width - value.size(), '0') + value;
25
+ }
26
+
27
+ } // namespace
28
+
29
+ TEST(Response, PaymentPositive) {
30
+ std::string p = a("12345678", 8) + "0" + "E" + "00" + // header + result
31
+ n("4111111111", 19) + // PAN(19)
32
+ a("ICC", 3) + a("ABC123", 6) + "2111520" + // txType authCode dateTime
33
+ "2" + // cardType
34
+ a("ACQ", 11) + n("42", 6) + n("99", 6); // acquirer STAN idOnline
35
+ PaymentResponse r = Ecr17Response::parsePayment(p);
36
+ EXPECT_EQ(r.outcome, Outcome::Ok);
37
+ EXPECT_EQ(r.resultCode, "00");
38
+ EXPECT_EQ(r.pan, n("4111111111", 19));
39
+ EXPECT_EQ(r.transactionType, "ICC");
40
+ EXPECT_EQ(r.authCode, "ABC123");
41
+ EXPECT_EQ(r.hostDateTime, "2111520");
42
+ EXPECT_EQ(r.cardType, "2");
43
+ EXPECT_EQ(r.acquirerId, "ACQ");
44
+ EXPECT_EQ(r.stan, "000042");
45
+ EXPECT_EQ(r.onlineId, "000099");
46
+ EXPECT_FALSE(r.currency.applied);
47
+ }
48
+
49
+ TEST(Response, PaymentNegative) {
50
+ std::string p = a("12345678", 8) + "0" + "E" + "01" + a("CARTA RIFIUTATA", 24) +
51
+ n("", 11) + // reserved 37-47
52
+ "3" + a("AC2", 11) + n("7", 6) + n("3", 6);
53
+ PaymentResponse r = Ecr17Response::parsePayment(p);
54
+ EXPECT_EQ(r.outcome, Outcome::Ko);
55
+ EXPECT_EQ(r.resultCode, "01");
56
+ EXPECT_EQ(r.errorDescription, "CARTA RIFIUTATA");
57
+ EXPECT_EQ(r.cardType, "3");
58
+ EXPECT_EQ(r.stan, "000007");
59
+ }
60
+
61
+ TEST(Response, PaymentWithCurrencyExchange) {
62
+ std::string base = a("12345678", 8) + "0" + "V" + "00" + n("4111111111", 19) + a("ICC", 3) +
63
+ a("ABC123", 6) + "2111520" + "2" + a("ACQ", 11) + n("42", 6) + n("99", 6);
64
+ // actionCode(3) origAmount(8) flag(1) rate(8) ccy(3) amount(12) precision(1)
65
+ std::string p = base + "000" + n("650", 8) + "1" + n("12345", 8) + "USD" + n("650", 12) + "2";
66
+ PaymentResponse r = Ecr17Response::parsePayment(p);
67
+ EXPECT_EQ(r.outcome, Outcome::Ok);
68
+ EXPECT_TRUE(r.currency.applied);
69
+ EXPECT_EQ(r.currency.rate, "00012345");
70
+ EXPECT_EQ(r.currency.currencyCode, "USD");
71
+ EXPECT_EQ(r.currency.amount, "000000000650");
72
+ EXPECT_EQ(r.currency.precision, "2");
73
+ }
74
+
75
+ TEST(Response, Status) {
76
+ std::string p = a("12345678", 8) + "0" + "s" + n("", 10) + // reserved 11-20
77
+ "0102251530" + "2" + "V1.2.3"; // dateTime status sw
78
+ StatusResponse r = Ecr17Response::parseStatus(p);
79
+ EXPECT_EQ(r.terminalId, "12345678");
80
+ EXPECT_EQ(r.dateTimeRaw, "0102251530");
81
+ EXPECT_EQ(r.status, 2);
82
+ EXPECT_EQ(r.softwareRelease, "V1.2.3");
83
+ }
84
+
85
+ TEST(Response, Totals) {
86
+ std::string p = a("12345678", 8) + "0" + "T" + "00" + n("123456", 16) + n("", 6);
87
+ TotalsResponse r = Ecr17Response::parseTotals(p);
88
+ EXPECT_EQ(r.outcome, Outcome::Ok);
89
+ EXPECT_EQ(r.posTotal, n("123456", 16));
90
+ }
91
+
92
+ TEST(Response, ClosePositive) {
93
+ std::string p = a("12345678", 8) + "0" + "C" + "00" + n("1000", 16) + n("1000", 16);
94
+ CloseResponse r = Ecr17Response::parseClose(p);
95
+ EXPECT_EQ(r.outcome, Outcome::Ok);
96
+ EXPECT_EQ(r.posTotal, n("1000", 16));
97
+ EXPECT_EQ(r.hostTotal, n("1000", 16));
98
+ }
99
+
100
+ TEST(Response, CloseNegative) {
101
+ std::string p = a("12345678", 8) + "0" + "C" + "01" + a("SBILANCIO", 19) + "100";
102
+ CloseResponse r = Ecr17Response::parseClose(p);
103
+ EXPECT_EQ(r.outcome, Outcome::Ko);
104
+ EXPECT_EQ(r.errorDescription, "SBILANCIO");
105
+ EXPECT_EQ(r.actionCode, "100");
106
+ }
107
+
108
+ TEST(Response, PreAuthPositive) {
109
+ std::string p = a("12345678", 8) + "0" + "e" + "00" + n("4111111111", 19) + a("CLI", 3) +
110
+ a("AUTH01", 6) + n("50000", 8) + n("123", 9) + "000" + "2111520";
111
+ PreAuthResponse r = Ecr17Response::parsePreAuth(p);
112
+ EXPECT_EQ(r.outcome, Outcome::Ok);
113
+ EXPECT_EQ(r.transactionType, "CLI");
114
+ EXPECT_EQ(r.authCode, "AUTH01");
115
+ EXPECT_EQ(r.preAuthorizedAmount, "00050000");
116
+ EXPECT_EQ(r.preAuthCode, "000000123");
117
+ EXPECT_EQ(r.hostDateTime, "2111520");
118
+ }
119
+
120
+ // Regression: on an approved pre-auth the amount field occupies positions 41-48,
121
+ // so its last digit sits exactly where cardType would be read. An amount ending
122
+ // in 1/2/3 must NOT be surfaced as debit/credit/other. cardType is only
123
+ // meaningful for the KO layout.
124
+ TEST(Response, PreAuthPositiveDoesNotLeakAmountDigitAsCardType) {
125
+ std::string p = a("12345678", 8) + "0" + "e" + "00" + n("4111111111", 19) + a("CLI", 3) +
126
+ a("AUTH01", 6) + n("50001", 8) + n("123", 9) + "000" + "2111520";
127
+ PreAuthResponse r = Ecr17Response::parsePreAuth(p);
128
+ EXPECT_EQ(r.outcome, Outcome::Ok);
129
+ EXPECT_EQ(r.preAuthorizedAmount, "00050001"); // ends in '1'
130
+ EXPECT_EQ(r.cardType, ""); // must stay empty, not "1"
131
+ }
132
+
133
+ TEST(Response, Vas) {
134
+ std::string xml =
135
+ "<ecrres><p k=\"RESPID\">0</p><p k=\"RESPMSG\">OK-APPROVED</p>"
136
+ "<p k=\"ORDER_ID\">ABC123</p></ecrres>";
137
+ // header(10) reserved(4) concatFlag(1) idMessage(3) filler-to-pos27(8) xml
138
+ std::string p = a("12345678", 8) + "0" + "K" + n("", 4) + "0" + "001" + n("", 8) + xml;
139
+ VasResponse r = Ecr17Response::parseVas(p);
140
+ EXPECT_FALSE(r.moreMessages);
141
+ EXPECT_EQ(r.idMessage, "001");
142
+ EXPECT_EQ(r.responseId, "0");
143
+ EXPECT_EQ(r.responseMessage, "OK-APPROVED");
144
+ EXPECT_EQ(r.orderId, "ABC123");
145
+ EXPECT_EQ(r.rawXml, xml);
146
+ }
147
+
148
+ TEST(Response, DefensiveOnShortOrEmptyPayload) {
149
+ PaymentResponse r = Ecr17Response::parsePayment("");
150
+ EXPECT_EQ(r.outcome, Outcome::Unknown);
151
+ EXPECT_EQ(r.resultCode, "");
152
+ EXPECT_EQ(r.pan, "");
153
+
154
+ StatusResponse s = Ecr17Response::parseStatus("123"); // truncated, must not crash
155
+ EXPECT_EQ(s.status, -1);
156
+ }
157
+
158
+ TEST(Response, OutcomeMapping) {
159
+ EXPECT_EQ(outcomeFromCode("00"), Outcome::Ok);
160
+ EXPECT_EQ(outcomeFromCode("01"), Outcome::Ko);
161
+ EXPECT_EQ(outcomeFromCode("05"), Outcome::CardNotPresent);
162
+ EXPECT_EQ(outcomeFromCode("09"), Outcome::UnknownTag);
163
+ EXPECT_EQ(outcomeFromCode("zz"), Outcome::Unknown);
164
+ }
@@ -0,0 +1,28 @@
1
+ // Money-critical safety tests for the auto-reconnect retry decision.
2
+ // The terminal handles real payments, so a financial command must NEVER be
3
+ // blindly re-sent after a connection drop (double-charge risk).
4
+
5
+ #include <gtest/gtest.h>
6
+
7
+ #include "Session/RetryPolicy.hpp"
8
+
9
+ using margelo::nitro::ecr17::shouldRetryAfterReconnect;
10
+
11
+ // A financial command (safeToRetry == false) must never be retried, regardless
12
+ // of autoReconnect / drop state. This is the invariant that prevents double
13
+ // charging; recovery is via sendLastResult ('G'), not a re-send.
14
+ TEST(RetryPolicy, FinancialCommandIsNeverRetried) {
15
+ EXPECT_FALSE(shouldRetryAfterReconnect(/*autoReconnect=*/true, /*dropped=*/true, /*safe=*/false));
16
+ EXPECT_FALSE(shouldRetryAfterReconnect(/*autoReconnect=*/false, /*dropped=*/true, /*safe=*/false));
17
+ EXPECT_FALSE(shouldRetryAfterReconnect(/*autoReconnect=*/true, /*dropped=*/false, /*safe=*/false));
18
+ EXPECT_FALSE(shouldRetryAfterReconnect(/*autoReconnect=*/false, /*dropped=*/false, /*safe=*/false));
19
+ }
20
+
21
+ // A safe/idempotent command is retried ONLY when autoReconnect is on AND the
22
+ // transport actually dropped.
23
+ TEST(RetryPolicy, SafeCommandRetriedOnlyOnReconnectAfterDrop) {
24
+ EXPECT_TRUE(shouldRetryAfterReconnect(/*autoReconnect=*/true, /*dropped=*/true, /*safe=*/true));
25
+ EXPECT_FALSE(shouldRetryAfterReconnect(/*autoReconnect=*/false, /*dropped=*/true, /*safe=*/true));
26
+ EXPECT_FALSE(shouldRetryAfterReconnect(/*autoReconnect=*/true, /*dropped=*/false, /*safe=*/true));
27
+ EXPECT_FALSE(shouldRetryAfterReconnect(/*autoReconnect=*/false, /*dropped=*/false, /*safe=*/true));
28
+ }