@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.
- package/Ecr17.podspec +39 -0
- package/README.md +348 -0
- package/android/CMakeLists.txt +41 -0
- package/android/build.gradle +149 -0
- package/android/fix-prefab.gradle +51 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/cpp-adapter.cpp +9 -0
- package/android/src/main/java/com/margelo/nitro/ecr17/HybridEcr17Transport.kt +233 -0
- package/android/src/main/java/com/padosoft/ecr17/Ecr17Package.kt +30 -0
- package/cpp/Ecr17.cpp +1 -0
- package/cpp/Ecr17.hpp +2 -0
- package/cpp/Ecr17Client/HybridEcr17Client.cpp +598 -0
- package/cpp/Ecr17Client/HybridEcr17Client.hpp +85 -0
- package/cpp/Ecr17Protocol/Ecr17Protocol.cpp +277 -0
- package/cpp/Ecr17Protocol/Ecr17Protocol.hpp +103 -0
- package/cpp/Ecr17Response/Ecr17Response.cpp +155 -0
- package/cpp/Ecr17Response/Ecr17Response.hpp +113 -0
- package/cpp/Lcr/Lcr.cpp +42 -0
- package/cpp/Lcr/Lcr.hpp +22 -0
- package/cpp/PacketCodec/PacketCodec.cpp +146 -0
- package/cpp/PacketCodec/PacketCodec.hpp +48 -0
- package/cpp/Session/Ecr17Session.cpp +260 -0
- package/cpp/Session/Ecr17Session.hpp +97 -0
- package/cpp/Session/RetryPolicy.hpp +23 -0
- package/cpp/Transport/FakeTransport.hpp +95 -0
- package/cpp/Transport/NativeTransportAdapter.cpp +42 -0
- package/cpp/Transport/NativeTransportAdapter.hpp +32 -0
- package/cpp/Transport/Transport.hpp +31 -0
- package/cpp/tests/CMakeLists.txt +55 -0
- package/cpp/tests/PosixTcpTransport.hpp +105 -0
- package/cpp/tests/stubs/LrcMode.hpp +25 -0
- package/cpp/tests/test_flows.cpp +148 -0
- package/cpp/tests/test_integration_terminal.cpp +72 -0
- package/cpp/tests/test_lrc.cpp +66 -0
- package/cpp/tests/test_packet_codec.cpp +164 -0
- package/cpp/tests/test_protocol.cpp +102 -0
- package/cpp/tests/test_protocol_commands.cpp +190 -0
- package/cpp/tests/test_response.cpp +164 -0
- package/cpp/tests/test_retry_policy.cpp +28 -0
- package/cpp/tests/test_session.cpp +262 -0
- package/ios/Bridge.h +1 -0
- package/ios/HybridEcr17Transport.swift +103 -0
- package/nitro.json +30 -0
- package/nitrogen/generated/.gitattributes +1 -0
- package/nitrogen/generated/android/Ecr17+autolinking.cmake +82 -0
- package/nitrogen/generated/android/Ecr17+autolinking.gradle +27 -0
- package/nitrogen/generated/android/Ecr17OnLoad.cpp +68 -0
- package/nitrogen/generated/android/Ecr17OnLoad.hpp +34 -0
- package/nitrogen/generated/android/c++/JFunc_void.hpp +75 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__shared_ptr_ArrayBuffer_.hpp +77 -0
- package/nitrogen/generated/android/c++/JHybridEcr17TransportSpec.cpp +93 -0
- package/nitrogen/generated/android/c++/JHybridEcr17TransportSpec.hpp +68 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/ecr17/Ecr17OnLoad.kt +35 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/ecr17/Func_void.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/ecr17/Func_void_std__shared_ptr_ArrayBuffer_.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/ecr17/HybridEcr17TransportSpec.kt +86 -0
- package/nitrogen/generated/ios/Ecr17+autolinking.rb +62 -0
- package/nitrogen/generated/ios/Ecr17-Swift-Cxx-Bridge.cpp +57 -0
- package/nitrogen/generated/ios/Ecr17-Swift-Cxx-Bridge.hpp +154 -0
- package/nitrogen/generated/ios/Ecr17-Swift-Cxx-Umbrella.hpp +47 -0
- package/nitrogen/generated/ios/Ecr17Autolinking.mm +43 -0
- package/nitrogen/generated/ios/Ecr17Autolinking.swift +26 -0
- package/nitrogen/generated/ios/c++/HybridEcr17TransportSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridEcr17TransportSpecSwift.hpp +119 -0
- package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__shared_ptr_ArrayBuffer_.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridEcr17TransportSpec.swift +60 -0
- package/nitrogen/generated/ios/swift/HybridEcr17TransportSpec_cxx.swift +211 -0
- package/nitrogen/generated/shared/c++/CardType.hpp +84 -0
- package/nitrogen/generated/shared/c++/CardVerificationRequest.hpp +97 -0
- package/nitrogen/generated/shared/c++/CardVerificationResult.hpp +136 -0
- package/nitrogen/generated/shared/c++/CloseSessionResult.hpp +106 -0
- package/nitrogen/generated/shared/c++/ConnectionState.hpp +80 -0
- package/nitrogen/generated/shared/c++/CurrencyExchange.hpp +100 -0
- package/nitrogen/generated/shared/c++/Ecr17Config.hpp +138 -0
- package/nitrogen/generated/shared/c++/HybridEcr17ClientSpec.cpp +42 -0
- package/nitrogen/generated/shared/c++/HybridEcr17ClientSpec.hpp +138 -0
- package/nitrogen/generated/shared/c++/HybridEcr17TransportSpec.cpp +26 -0
- package/nitrogen/generated/shared/c++/HybridEcr17TransportSpec.hpp +70 -0
- package/nitrogen/generated/shared/c++/IncrementalAuthRequest.hpp +96 -0
- package/nitrogen/generated/shared/c++/LrcMode.hpp +84 -0
- package/nitrogen/generated/shared/c++/PaymentCardType.hpp +84 -0
- package/nitrogen/generated/shared/c++/PaymentRequest.hpp +109 -0
- package/nitrogen/generated/shared/c++/PaymentResult.hpp +139 -0
- package/nitrogen/generated/shared/c++/PosStatusResponse.hpp +96 -0
- package/nitrogen/generated/shared/c++/PreAuthClosureRequest.hpp +96 -0
- package/nitrogen/generated/shared/c++/PreAuthRequest.hpp +109 -0
- package/nitrogen/generated/shared/c++/PreAuthResult.hpp +144 -0
- package/nitrogen/generated/shared/c++/ProgressEvent.hpp +83 -0
- package/nitrogen/generated/shared/c++/ReceiptLine.hpp +83 -0
- package/nitrogen/generated/shared/c++/ReversalRequest.hpp +88 -0
- package/nitrogen/generated/shared/c++/ReversalResult.hpp +132 -0
- package/nitrogen/generated/shared/c++/TokenizationRequest.hpp +89 -0
- package/nitrogen/generated/shared/c++/TokenizationService.hpp +76 -0
- package/nitrogen/generated/shared/c++/TotalsResult.hpp +93 -0
- package/nitrogen/generated/shared/c++/TransactionEntryMode.hpp +92 -0
- package/nitrogen/generated/shared/c++/TransactionOutcome.hpp +88 -0
- package/nitrogen/generated/shared/c++/VasResult.hpp +96 -0
- package/package.json +102 -0
- package/react-native.config.js +18 -0
- package/src/index.ts +4 -0
- package/src/specs/client.nitro.ts +102 -0
- package/src/specs/transport.nitro.ts +25 -0
- package/src/types/client.ts +196 -0
- 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
|
+
}
|