@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,95 @@
1
+ #pragma once
2
+
3
+ #include <cstdint>
4
+ #include <functional>
5
+ #include <vector>
6
+
7
+ #include "Transport/Transport.hpp"
8
+
9
+ namespace margelo::nitro::ecr17 {
10
+
11
+ // In-memory Transport for unit tests. Deterministic and synchronous:
12
+ //
13
+ // - `enqueueResponse(bytes)` queues a scripted terminal reply.
14
+ // - Every time the session sends an APPLICATION request (a frame starting with
15
+ // STX, i.e. an initial send or a retransmit) the next queued response is
16
+ // delivered synchronously via the data callback. Control sends (ACK/NAK,
17
+ // which start with 0x06/0x15) are just recorded and never trigger a reply.
18
+ //
19
+ // This lets a test script "ACK + result", "NAK then (on retransmit) ACK+result",
20
+ // progress/receipt streams, or no reply at all (to exercise timeouts), without
21
+ // any real sockets or threads.
22
+ class FakeTransport : public Transport {
23
+ public:
24
+ static constexpr uint8_t STX = 0x02;
25
+
26
+ void connect() override { connected_ = true; }
27
+ void disconnect() override { connected_ = false; }
28
+ bool isConnected() const override { return connected_; }
29
+
30
+ void send(const std::vector<uint8_t>& bytes) override {
31
+ sent_.push_back(bytes);
32
+ const bool isApplicationRequest = !bytes.empty() && bytes.front() == STX;
33
+ if (isApplicationRequest && disconnectOnRequest_) {
34
+ triggerDisconnect();
35
+ return;
36
+ }
37
+ if (isApplicationRequest && !responses_.empty()) {
38
+ std::vector<uint8_t> next = std::move(responses_.front());
39
+ responses_.erase(responses_.begin());
40
+ if (dataCallback_) {
41
+ dataCallback_(next);
42
+ }
43
+ }
44
+ }
45
+
46
+ void setDataCallback(DataCallback cb) override { dataCallback_ = std::move(cb); }
47
+ void setDisconnectCallback(DisconnectCallback cb) override { disconnectCallback_ = std::move(cb); }
48
+
49
+ // --- Test helpers ---
50
+ void enqueueResponse(std::vector<uint8_t> bytes) { responses_.push_back(std::move(bytes)); }
51
+
52
+ // Make the next application-request send drop the connection instead of replying.
53
+ void disconnectOnNextRequest() { disconnectOnRequest_ = true; }
54
+
55
+ // Simulate a successful reconnect: clear the drop flag and mark connected.
56
+ void rearm() {
57
+ disconnectOnRequest_ = false;
58
+ connected_ = true;
59
+ }
60
+
61
+ // Deliver bytes immediately as if received from the terminal.
62
+ void pushIncoming(const std::vector<uint8_t>& bytes) {
63
+ if (dataCallback_) {
64
+ dataCallback_(bytes);
65
+ }
66
+ }
67
+
68
+ void triggerDisconnect() {
69
+ connected_ = false;
70
+ if (disconnectCallback_) {
71
+ disconnectCallback_();
72
+ }
73
+ }
74
+
75
+ const std::vector<std::vector<uint8_t>>& sentFrames() const { return sent_; }
76
+ size_t applicationRequestCount() const {
77
+ size_t count = 0;
78
+ for (const auto& f : sent_) {
79
+ if (!f.empty() && f.front() == STX) {
80
+ ++count;
81
+ }
82
+ }
83
+ return count;
84
+ }
85
+
86
+ private:
87
+ bool connected_ = false;
88
+ DataCallback dataCallback_{};
89
+ DisconnectCallback disconnectCallback_{};
90
+ std::vector<std::vector<uint8_t>> sent_{};
91
+ std::vector<std::vector<uint8_t>> responses_{};
92
+ bool disconnectOnRequest_ = false;
93
+ };
94
+
95
+ } // namespace margelo::nitro::ecr17
@@ -0,0 +1,42 @@
1
+ #include "Transport/NativeTransportAdapter.hpp"
2
+
3
+ #include <NitroModules/ArrayBuffer.hpp>
4
+
5
+ #include <utility>
6
+
7
+ namespace margelo::nitro::ecr17 {
8
+
9
+ NativeTransportAdapter::NativeTransportAdapter(std::shared_ptr<HybridEcr17TransportSpec> transport)
10
+ : transport_(std::move(transport)) {}
11
+
12
+ void NativeTransportAdapter::connect() {
13
+ // Connection is initiated by HybridEcr17Client via the spec's async connect();
14
+ // nothing to do here.
15
+ }
16
+
17
+ void NativeTransportAdapter::disconnect() { transport_->disconnect(); }
18
+
19
+ bool NativeTransportAdapter::isConnected() const { return transport_->isConnected(); }
20
+
21
+ void NativeTransportAdapter::send(const std::vector<uint8_t>& bytes) {
22
+ transport_->send(ArrayBuffer::copy(bytes));
23
+ }
24
+
25
+ void NativeTransportAdapter::setDataCallback(DataCallback cb) {
26
+ transport_->setOnData([cb = std::move(cb)](const std::shared_ptr<ArrayBuffer>& buffer) {
27
+ if (!cb || buffer == nullptr) {
28
+ return;
29
+ }
30
+ const uint8_t* data = buffer->data();
31
+ if (data == nullptr) {
32
+ return;
33
+ }
34
+ cb(std::vector<uint8_t>(data, data + buffer->size()));
35
+ });
36
+ }
37
+
38
+ void NativeTransportAdapter::setDisconnectCallback(DisconnectCallback cb) {
39
+ transport_->setOnDisconnect(std::move(cb));
40
+ }
41
+
42
+ } // namespace margelo::nitro::ecr17
@@ -0,0 +1,32 @@
1
+ #pragma once
2
+
3
+ #include <memory>
4
+ #include <string>
5
+ #include <vector>
6
+
7
+ #include "HybridEcr17TransportSpec.hpp" // generated Nitro spec (Swift/Kotlin impl)
8
+ #include "Transport/Transport.hpp"
9
+
10
+ namespace margelo::nitro::ecr17 {
11
+
12
+ // Adapts the native Nitro transport (Ecr17Transport HybridObject, implemented in
13
+ // Swift/Kotlin) to the C++ Transport interface used by Ecr17Session. Converts
14
+ // between std::vector<uint8_t> and Nitro's ArrayBuffer. Connection lifecycle is
15
+ // driven by HybridEcr17Client directly via the spec (async Promise); the session
16
+ // only uses send + the data/disconnect callbacks.
17
+ class NativeTransportAdapter : public Transport {
18
+ public:
19
+ explicit NativeTransportAdapter(std::shared_ptr<HybridEcr17TransportSpec> transport);
20
+
21
+ void connect() override; // no-op: client drives connect() via the spec (async)
22
+ void disconnect() override;
23
+ bool isConnected() const override;
24
+ void send(const std::vector<uint8_t>& bytes) override;
25
+ void setDataCallback(DataCallback cb) override;
26
+ void setDisconnectCallback(DisconnectCallback cb) override;
27
+
28
+ private:
29
+ std::shared_ptr<HybridEcr17TransportSpec> transport_;
30
+ };
31
+
32
+ } // namespace margelo::nitro::ecr17
@@ -0,0 +1,31 @@
1
+ #pragma once
2
+
3
+ #include <cstdint>
4
+ #include <functional>
5
+ #include <string>
6
+ #include <vector>
7
+
8
+ namespace margelo::nitro::ecr17 {
9
+
10
+ using DataCallback = std::function<void(const std::vector<uint8_t>&)>;
11
+
12
+ using DisconnectCallback = std::function<void()>;
13
+
14
+ class Transport {
15
+ public:
16
+ virtual ~Transport() = default;
17
+
18
+ virtual void connect() = 0;
19
+
20
+ virtual void disconnect() = 0;
21
+
22
+ virtual bool isConnected() const = 0;
23
+
24
+ virtual void send(const std::vector<uint8_t>& bytes) = 0;
25
+
26
+ virtual void setDataCallback(DataCallback cb) = 0;
27
+
28
+ virtual void setDisconnectCallback(DisconnectCallback cb) = 0;
29
+ };
30
+
31
+ } // namespace margelo::nitro::ecr17
@@ -0,0 +1,55 @@
1
+ cmake_minimum_required(VERSION 3.16)
2
+ project(ecr17_cpp_tests CXX)
3
+
4
+ set(CMAKE_CXX_STANDARD 20)
5
+ set(CMAKE_CXX_STANDARD_REQUIRED ON)
6
+ set(CMAKE_CXX_EXTENSIONS OFF)
7
+
8
+ # GoogleTest via FetchContent (CI has network access).
9
+ include(FetchContent)
10
+ FetchContent_Declare(
11
+ googletest
12
+ URL https://github.com/google/googletest/archive/refs/tags/v1.15.2.tar.gz
13
+ )
14
+ # Keep CRT linkage consistent on Windows runners.
15
+ set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
16
+ FetchContent_MakeAvailable(googletest)
17
+
18
+ enable_testing()
19
+
20
+ # Production C++ sources under test (../ is package/cpp).
21
+ set(ECR17_CPP_DIR ${CMAKE_CURRENT_SOURCE_DIR}/..)
22
+
23
+ add_executable(ecr17_cpp_tests
24
+ test_lrc.cpp
25
+ test_packet_codec.cpp
26
+ test_protocol.cpp
27
+ test_protocol_commands.cpp
28
+ test_response.cpp
29
+ test_session.cpp
30
+ test_retry_policy.cpp
31
+ test_integration_terminal.cpp
32
+ test_flows.cpp
33
+ ${ECR17_CPP_DIR}/Lcr/Lcr.cpp
34
+ ${ECR17_CPP_DIR}/PacketCodec/PacketCodec.cpp
35
+ ${ECR17_CPP_DIR}/Ecr17Protocol/Ecr17Protocol.cpp
36
+ ${ECR17_CPP_DIR}/Ecr17Response/Ecr17Response.cpp
37
+ ${ECR17_CPP_DIR}/Session/Ecr17Session.cpp
38
+ )
39
+
40
+ target_include_directories(ecr17_cpp_tests PRIVATE
41
+ ${ECR17_CPP_DIR} # resolves "Lcr/Lcr.hpp", "PacketCodec/..."
42
+ ${CMAKE_CURRENT_SOURCE_DIR}/stubs # test-only LrcMode.hpp (Nitrogen-generated in real builds)
43
+ )
44
+
45
+ if(MSVC)
46
+ target_compile_options(ecr17_cpp_tests PRIVATE /W4)
47
+ else()
48
+ target_compile_options(ecr17_cpp_tests PRIVATE -Wall -Wextra)
49
+ endif()
50
+
51
+ find_package(Threads REQUIRED)
52
+ target_link_libraries(ecr17_cpp_tests PRIVATE GTest::gtest_main Threads::Threads)
53
+
54
+ include(GoogleTest)
55
+ gtest_discover_tests(ecr17_cpp_tests)
@@ -0,0 +1,105 @@
1
+ #pragma once
2
+
3
+ // Test-only POSIX TCP transport, used by the opt-in real-terminal integration
4
+ // test. Lets a developer run the C++ protocol core (builders + session + parsers)
5
+ // against an actual terminal over the LAN, without the native (Kotlin/Swift)
6
+ // transport. POSIX-only (Linux/macOS); excluded on Windows.
7
+ #if !defined(_WIN32)
8
+
9
+ #include <arpa/inet.h>
10
+ #include <netdb.h>
11
+ #include <sys/socket.h>
12
+ #include <unistd.h>
13
+
14
+ #include <atomic>
15
+ #include <cstdint>
16
+ #include <stdexcept>
17
+ #include <string>
18
+ #include <thread>
19
+ #include <vector>
20
+
21
+ #include "Transport/Transport.hpp"
22
+
23
+ namespace margelo::nitro::ecr17 {
24
+
25
+ class PosixTcpTransport : public Transport {
26
+ public:
27
+ PosixTcpTransport(std::string host, int port) : host_(std::move(host)), port_(port) {}
28
+ ~PosixTcpTransport() override { disconnect(); }
29
+
30
+ void connect() override {
31
+ addrinfo hints{};
32
+ hints.ai_family = AF_UNSPEC;
33
+ hints.ai_socktype = SOCK_STREAM;
34
+ addrinfo* res = nullptr;
35
+ if (getaddrinfo(host_.c_str(), std::to_string(port_).c_str(), &hints, &res) != 0) {
36
+ throw std::runtime_error("PosixTcpTransport: host resolution failed");
37
+ }
38
+ fd_ = ::socket(res->ai_family, res->ai_socktype, res->ai_protocol);
39
+ if (fd_ < 0) {
40
+ freeaddrinfo(res);
41
+ throw std::runtime_error("PosixTcpTransport: socket() failed");
42
+ }
43
+ if (::connect(fd_, res->ai_addr, res->ai_addrlen) != 0) {
44
+ freeaddrinfo(res);
45
+ ::close(fd_);
46
+ fd_ = -1;
47
+ throw std::runtime_error("PosixTcpTransport: connect() failed");
48
+ }
49
+ freeaddrinfo(res);
50
+ running_ = true;
51
+ reader_ = std::thread([this]() { readLoop(); });
52
+ }
53
+
54
+ void disconnect() override {
55
+ running_ = false;
56
+ if (fd_ >= 0) {
57
+ ::shutdown(fd_, SHUT_RDWR);
58
+ ::close(fd_);
59
+ fd_ = -1;
60
+ }
61
+ if (reader_.joinable()) {
62
+ reader_.join();
63
+ }
64
+ }
65
+
66
+ bool isConnected() const override { return fd_ >= 0; }
67
+
68
+ void send(const std::vector<uint8_t>& bytes) override {
69
+ if (fd_ >= 0) {
70
+ ::send(fd_, bytes.data(), bytes.size(), 0);
71
+ }
72
+ }
73
+
74
+ void setDataCallback(DataCallback cb) override { onData_ = std::move(cb); }
75
+ void setDisconnectCallback(DisconnectCallback cb) override { onDisconnect_ = std::move(cb); }
76
+
77
+ private:
78
+ void readLoop() {
79
+ uint8_t buffer[4096];
80
+ while (running_) {
81
+ ssize_t n = ::recv(fd_, buffer, sizeof(buffer), 0);
82
+ if (n <= 0) {
83
+ break;
84
+ }
85
+ if (onData_) {
86
+ onData_(std::vector<uint8_t>(buffer, buffer + n));
87
+ }
88
+ }
89
+ if (onDisconnect_) {
90
+ onDisconnect_();
91
+ }
92
+ }
93
+
94
+ std::string host_;
95
+ int port_;
96
+ int fd_ = -1;
97
+ std::atomic<bool> running_{false};
98
+ std::thread reader_;
99
+ DataCallback onData_{};
100
+ DisconnectCallback onDisconnect_{};
101
+ };
102
+
103
+ } // namespace margelo::nitro::ecr17
104
+
105
+ #endif // !_WIN32
@@ -0,0 +1,25 @@
1
+ #pragma once
2
+
3
+ // TEST-ONLY STUB.
4
+ //
5
+ // In a real build this header is generated by Nitrogen from the TypeScript
6
+ // union `type LrcMode = "stx" | "std" | "noext" | "stx_noext"` (see
7
+ // package/src/types/client.ts). The unit tests compile the pure-logic C++
8
+ // units (Lrc, PacketCodec, Ecr17Protocol) in isolation, without running
9
+ // Nitrogen, so we provide a minimal enum that mirrors the generated one.
10
+ //
11
+ // The member names MUST match what the production code references
12
+ // (Lrc.cpp uses LrcMode::STX, LrcMode::STX_NOEXT, LrcMode::NOEXT). If Nitrogen
13
+ // turns out to emit different casing, that is a real bug to reconcile when the
14
+ // codegen is run — this stub documents the contract the code assumes today.
15
+
16
+ namespace margelo::nitro::ecr17 {
17
+
18
+ enum class LrcMode {
19
+ STX,
20
+ STD,
21
+ NOEXT,
22
+ STX_NOEXT,
23
+ };
24
+
25
+ } // namespace margelo::nitro::ecr17
@@ -0,0 +1,148 @@
1
+ // End-to-end protocol *flow* tests, modelled on the documented Nexi ECR17
2
+ // sequences (basic payment, reversal/"annullamento", re-payment, status,
3
+ // NAK retransmission).
4
+ //
5
+ // These exercise the layers that exist today: request building
6
+ // (Ecr17Protocol) + physical framing (PacketCodec). Response *field* parsing
7
+ // is not implemented yet, so terminal responses are synthesized as spec-shaped
8
+ // payloads and asserted at the framing/classification level (packet type, LRC
9
+ // validity, message code position), not by parsed fields.
10
+
11
+ #include <gtest/gtest.h>
12
+
13
+ #include <cstdint>
14
+ #include <string>
15
+ #include <vector>
16
+
17
+ #include "Ecr17Protocol/Ecr17Protocol.hpp"
18
+ #include "PacketCodec/PacketCodec.hpp"
19
+
20
+ using namespace margelo::nitro::ecr17;
21
+
22
+ namespace {
23
+
24
+ constexpr uint8_t kStx = 0x02;
25
+ constexpr uint8_t kEtx = 0x03;
26
+ constexpr uint8_t kSoh = 0x01;
27
+ constexpr uint8_t kEot = 0x04;
28
+ constexpr uint8_t kAck = 0x06;
29
+ constexpr uint8_t kNak = 0x15;
30
+
31
+ const std::string kTerminal = "12345678";
32
+ const std::string kCashReg = "00000001";
33
+
34
+ // Terminal-side framing of a progress-update packet: SOH + 20-char message + EOT.
35
+ std::vector<uint8_t> progressFrame(const std::string& msg20) {
36
+ std::vector<uint8_t> f{kSoh};
37
+ f.insert(f.end(), msg20.begin(), msg20.end());
38
+ f.push_back(kEot);
39
+ return f;
40
+ }
41
+
42
+ // Terminal-side framing of a spec-shaped result message ('E', result "00" = OK).
43
+ std::vector<uint8_t> okResultFrame(PacketCodec& codec) {
44
+ std::string e = kTerminal + "0" + "E" + "00" + std::string(60, '0');
45
+ return codec.encodeApplication(e);
46
+ }
47
+
48
+ } // namespace
49
+
50
+ // Documented "Basic Payment Flow": request -> ACK -> progress -> result -> ACK.
51
+ TEST(Flow, BasicPayment) {
52
+ PacketCodec codec(LrcMode::STD);
53
+
54
+ // 1. ECR -> POS: payment request on the wire (STX .. ETX .. LRC).
55
+ std::string req = Ecr17Protocol::buildPaymentMessage(kTerminal, kCashReg, 650);
56
+ auto reqFrame = codec.encodeApplication(req);
57
+ EXPECT_EQ(reqFrame.front(), kStx);
58
+ EXPECT_EQ(reqFrame[reqFrame.size() - 2], kEtx);
59
+
60
+ // POS validates and recovers the exact request payload.
61
+ DecodedPacket atPos = codec.decode(reqFrame);
62
+ EXPECT_EQ(atPos.type, PacketType::APPLICATION);
63
+ EXPECT_TRUE(atPos.validLrc);
64
+ EXPECT_EQ(atPos.payload, req);
65
+ EXPECT_EQ(atPos.payload[9], 'P');
66
+
67
+ // 2. POS -> ECR: physical ACK.
68
+ EXPECT_EQ(codec.decode(codec.encodeControl(kAck)).type, PacketType::ACK);
69
+
70
+ // 3. POS -> ECR: progress update while contacting the host (no LRC).
71
+ DecodedPacket prog = codec.decode(progressFrame("ATTENDERE PREGO "));
72
+ EXPECT_EQ(prog.type, PacketType::PROGRESS);
73
+ EXPECT_EQ(prog.payload, "ATTENDERE PREGO ");
74
+
75
+ // 4. POS -> ECR: positive result.
76
+ DecodedPacket result = codec.decode(okResultFrame(codec));
77
+ EXPECT_EQ(result.type, PacketType::APPLICATION);
78
+ EXPECT_TRUE(result.validLrc);
79
+ EXPECT_EQ(result.payload[9], 'E');
80
+ EXPECT_EQ(result.payload.substr(10, 2), "00");
81
+
82
+ // 5. ECR -> POS: ACK confirming receipt of the result.
83
+ EXPECT_EQ(codec.decode(codec.encodeControl(kAck)).type, PacketType::ACK);
84
+ }
85
+
86
+ // Documented NAK handling: a NAK triggers retransmission of the same message
87
+ // (up to 3 times). Framing is deterministic, so the retransmitted bytes match.
88
+ TEST(Flow, NakTriggersIdenticalRetransmit) {
89
+ PacketCodec codec(LrcMode::STD);
90
+ std::string req = Ecr17Protocol::buildPaymentMessage(kTerminal, kCashReg, 1999);
91
+ auto first = codec.encodeApplication(req);
92
+
93
+ EXPECT_EQ(codec.decode(codec.encodeControl(kNak)).type, PacketType::NAK);
94
+
95
+ auto retransmit = codec.encodeApplication(req);
96
+ EXPECT_EQ(first, retransmit);
97
+ }
98
+
99
+ // Documented "Reversal Last Transaction" (annullamento), command 'S'.
100
+ TEST(Flow, ReversalAnnullamento) {
101
+ PacketCodec codec(LrcMode::STD);
102
+
103
+ std::string req = Ecr17Protocol::buildReversalMessage(kTerminal, kCashReg, "000123");
104
+ ASSERT_EQ(req.size(), 26u);
105
+ EXPECT_EQ(req[9], 'S');
106
+ EXPECT_EQ(req.substr(18, 6), "000123");
107
+
108
+ DecodedPacket atPos = codec.decode(codec.encodeApplication(req));
109
+ EXPECT_EQ(atPos.type, PacketType::APPLICATION);
110
+ EXPECT_TRUE(atPos.validLrc);
111
+ EXPECT_EQ(atPos.payload, req);
112
+
113
+ EXPECT_EQ(codec.decode(codec.encodeControl(kAck)).type, PacketType::ACK);
114
+ DecodedPacket result = codec.decode(okResultFrame(codec));
115
+ EXPECT_TRUE(result.validLrc);
116
+ EXPECT_EQ(result.payload[9], 'E');
117
+ }
118
+
119
+ // Pay -> annullamento -> ripaga (re-payment with the corrected amount).
120
+ TEST(Flow, PayReverseRepay) {
121
+ PacketCodec codec(LrcMode::STD);
122
+
123
+ auto pay1 = codec.encodeApplication(Ecr17Protocol::buildPaymentMessage(kTerminal, kCashReg, 650));
124
+ auto rev = codec.encodeApplication(Ecr17Protocol::buildReversalMessage(kTerminal, kCashReg));
125
+ auto pay2 = codec.encodeApplication(Ecr17Protocol::buildPaymentMessage(kTerminal, kCashReg, 720));
126
+
127
+ for (const auto* frame : {&pay1, &rev, &pay2}) {
128
+ DecodedPacket d = codec.decode(*frame);
129
+ EXPECT_EQ(d.type, PacketType::APPLICATION);
130
+ EXPECT_TRUE(d.validLrc);
131
+ }
132
+
133
+ EXPECT_EQ(codec.decode(pay1).payload.substr(23, 8), "00000650");
134
+ EXPECT_EQ(codec.decode(pay2).payload.substr(23, 8), "00000720");
135
+ EXPECT_NE(pay1, pay2); // the corrected re-payment differs on the wire
136
+ }
137
+
138
+ // Terminal status request/round-trip ('s').
139
+ TEST(Flow, StatusRequest) {
140
+ PacketCodec codec(LrcMode::STD);
141
+ std::string req = Ecr17Protocol::buildStatusMessage(kTerminal);
142
+ EXPECT_EQ(req, "123456780s");
143
+
144
+ DecodedPacket atPos = codec.decode(codec.encodeApplication(req));
145
+ EXPECT_EQ(atPos.type, PacketType::APPLICATION);
146
+ EXPECT_TRUE(atPos.validLrc);
147
+ EXPECT_EQ(atPos.payload, req);
148
+ }
@@ -0,0 +1,72 @@
1
+ // OPT-IN real-terminal integration test.
2
+ //
3
+ // Skipped unless ECR17_TERMINAL_HOST is set, so it never affects CI. To run it
4
+ // against a real Nexi terminal on your LAN:
5
+ //
6
+ // ECR17_TERMINAL_HOST=192.168.1.50 \
7
+ // ECR17_TERMINAL_PORT=10000 \
8
+ // ECR17_TERMINAL_ID=00000000 \
9
+ // ECR17_LRC_MODE=std \
10
+ // ctest --test-dir build --output-on-failure -R Integration
11
+ //
12
+ // It connects over real TCP, sends a Terminal Status ('s') request through the
13
+ // full C++ core (builder -> session/ACK-NAK -> parser) and prints the result.
14
+
15
+ #if !defined(_WIN32)
16
+
17
+ #include <gtest/gtest.h>
18
+
19
+ #include <cstdlib>
20
+ #include <iostream>
21
+ #include <string>
22
+
23
+ #include "Ecr17Protocol/Ecr17Protocol.hpp"
24
+ #include "Ecr17Response/Ecr17Response.hpp"
25
+ #include "PosixTcpTransport.hpp"
26
+ #include "Session/Ecr17Session.hpp"
27
+
28
+ using namespace margelo::nitro::ecr17;
29
+
30
+ namespace {
31
+ LrcMode lrcModeFromEnv() {
32
+ const char* m = std::getenv("ECR17_LRC_MODE");
33
+ const std::string mode = m ? m : "std";
34
+ if (mode == "stx") return LrcMode::STX;
35
+ if (mode == "noext") return LrcMode::NOEXT;
36
+ if (mode == "stx_noext") return LrcMode::STX_NOEXT;
37
+ return LrcMode::STD;
38
+ }
39
+ } // namespace
40
+
41
+ TEST(Integration, RealTerminalStatus) {
42
+ const char* host = std::getenv("ECR17_TERMINAL_HOST");
43
+ if (host == nullptr) {
44
+ GTEST_SKIP() << "set ECR17_TERMINAL_HOST to run against a real terminal";
45
+ }
46
+ const char* portEnv = std::getenv("ECR17_TERMINAL_PORT");
47
+ const char* idEnv = std::getenv("ECR17_TERMINAL_ID");
48
+ const int port = portEnv ? std::atoi(portEnv) : 10000;
49
+ const std::string terminalId = idEnv ? idEnv : "00000000";
50
+
51
+ PosixTcpTransport transport(host, port);
52
+ transport.connect();
53
+
54
+ SessionConfig cfg;
55
+ cfg.lrcMode = lrcModeFromEnv();
56
+ cfg.ackTimeoutMs = 3000;
57
+ cfg.responseTimeoutMs = 15000;
58
+ Ecr17Session session(transport, cfg);
59
+
60
+ DecodedPacket pkt = session.exchange(Ecr17Protocol::buildStatusMessage(terminalId));
61
+ StatusResponse status = Ecr17Response::parseStatus(pkt.payload);
62
+
63
+ std::cout << "[ECR17] terminalId=" << status.terminalId << " status=" << status.status
64
+ << " dateTime=" << status.dateTimeRaw << " sw=" << status.softwareRelease << std::endl;
65
+
66
+ EXPECT_TRUE(pkt.validLrc);
67
+ EXPECT_FALSE(status.terminalId.empty());
68
+
69
+ transport.disconnect();
70
+ }
71
+
72
+ #endif // !_WIN32
@@ -0,0 +1,66 @@
1
+ #include <gtest/gtest.h>
2
+
3
+ #include <cstdint>
4
+ #include <string>
5
+ #include <vector>
6
+
7
+ #include "Lcr/Lcr.hpp"
8
+
9
+ using margelo::nitro::ecr17::Lrc;
10
+ using margelo::nitro::ecr17::LrcMode;
11
+
12
+ namespace {
13
+
14
+ constexpr uint8_t kBase = 0x7F;
15
+ constexpr uint8_t kStx = 0x02;
16
+ constexpr uint8_t kEtx = 0x03;
17
+
18
+ // Reference implementation kept intentionally independent from the production
19
+ // code, so the tests assert against first principles rather than a copy.
20
+ uint8_t reference(const std::vector<uint8_t>& payload, LrcMode mode) {
21
+ uint8_t lrc = kBase;
22
+ if (mode == LrcMode::STX || mode == LrcMode::STX_NOEXT) {
23
+ lrc ^= kStx;
24
+ }
25
+ for (uint8_t b : payload) {
26
+ lrc ^= b;
27
+ }
28
+ if (mode == LrcMode::STX || mode == LrcMode::NOEXT) {
29
+ lrc ^= kEtx;
30
+ }
31
+ return lrc;
32
+ }
33
+
34
+ } // namespace
35
+
36
+ TEST(Lrc, EmptyPayloadStdIsBase) {
37
+ EXPECT_EQ(Lrc::compute(std::vector<uint8_t>{}, LrcMode::STD), kBase);
38
+ }
39
+
40
+ TEST(Lrc, EmptyPayloadStxFoldsStxAndEtx) {
41
+ // 0x7F ^ 0x02 ^ 0x03 == 0x7E
42
+ EXPECT_EQ(Lrc::compute(std::vector<uint8_t>{}, LrcMode::STX), 0x7E);
43
+ }
44
+
45
+ TEST(Lrc, KnownVectorAllModes) {
46
+ const std::vector<uint8_t> payload{'A'}; // 0x41
47
+ EXPECT_EQ(Lrc::compute(payload, LrcMode::STD), 0x3E);
48
+ EXPECT_EQ(Lrc::compute(payload, LrcMode::STX), 0x3F);
49
+ EXPECT_EQ(Lrc::compute(payload, LrcMode::NOEXT), 0x3D);
50
+ EXPECT_EQ(Lrc::compute(payload, LrcMode::STX_NOEXT), 0x3C);
51
+ }
52
+
53
+ TEST(Lrc, MatchesReferenceForEveryMode) {
54
+ const std::vector<uint8_t> payload{0x00, 0x7F, 0x55, 0xAA, 'Z', 0x10};
55
+ for (LrcMode mode : {LrcMode::STX, LrcMode::STD, LrcMode::NOEXT, LrcMode::STX_NOEXT}) {
56
+ EXPECT_EQ(Lrc::compute(payload, mode), reference(payload, mode));
57
+ }
58
+ }
59
+
60
+ TEST(Lrc, StringAndVectorOverloadsAgree) {
61
+ const std::string payload = "12345678P0";
62
+ const std::vector<uint8_t> bytes(payload.begin(), payload.end());
63
+ for (LrcMode mode : {LrcMode::STX, LrcMode::STD, LrcMode::NOEXT, LrcMode::STX_NOEXT}) {
64
+ EXPECT_EQ(Lrc::compute(payload, mode), Lrc::compute(bytes, mode));
65
+ }
66
+ }