@padosoft/react-native-ecr17 0.0.0 → 2.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 -39
- package/README.md +348 -348
- package/android/CMakeLists.txt +41 -41
- package/android/build.gradle +148 -148
- package/android/fix-prefab.gradle +50 -50
- package/android/gradle.properties +5 -5
- package/android/src/main/AndroidManifest.xml +2 -2
- package/android/src/main/cpp/cpp-adapter.cpp +8 -8
- package/android/src/main/java/com/margelo/nitro/ecr17/HybridEcr17Transport.kt +233 -233
- package/android/src/main/java/com/padosoft/ecr17/Ecr17Package.kt +30 -30
- package/cpp/Ecr17.hpp +1 -1
- package/cpp/Ecr17Client/HybridEcr17Client.cpp +598 -598
- package/cpp/Ecr17Client/HybridEcr17Client.hpp +85 -85
- package/cpp/Ecr17Protocol/Ecr17Protocol.cpp +277 -277
- package/cpp/Ecr17Protocol/Ecr17Protocol.hpp +103 -103
- package/cpp/Ecr17Response/Ecr17Response.cpp +155 -155
- package/cpp/Ecr17Response/Ecr17Response.hpp +113 -113
- package/cpp/Lcr/Lcr.cpp +42 -42
- package/cpp/Lcr/Lcr.hpp +21 -21
- package/cpp/PacketCodec/PacketCodec.cpp +145 -145
- package/cpp/PacketCodec/PacketCodec.hpp +47 -47
- package/cpp/Session/Ecr17Session.cpp +260 -260
- package/cpp/Session/Ecr17Session.hpp +97 -97
- package/cpp/Session/RetryPolicy.hpp +23 -23
- package/cpp/Transport/FakeTransport.hpp +95 -95
- package/cpp/Transport/NativeTransportAdapter.cpp +42 -42
- package/cpp/Transport/NativeTransportAdapter.hpp +32 -32
- package/cpp/Transport/Transport.hpp +30 -30
- package/cpp/tests/CMakeLists.txt +55 -55
- package/cpp/tests/PosixTcpTransport.hpp +105 -105
- package/cpp/tests/stubs/LrcMode.hpp +25 -25
- package/cpp/tests/test_flows.cpp +148 -148
- package/cpp/tests/test_integration_terminal.cpp +72 -72
- package/cpp/tests/test_lrc.cpp +66 -66
- package/cpp/tests/test_packet_codec.cpp +164 -164
- package/cpp/tests/test_protocol.cpp +102 -102
- package/cpp/tests/test_protocol_commands.cpp +190 -190
- package/cpp/tests/test_response.cpp +164 -164
- package/cpp/tests/test_retry_policy.cpp +28 -28
- package/cpp/tests/test_session.cpp +262 -262
- package/ios/HybridEcr17Transport.swift +103 -103
- package/lib/commonjs/index.js +50 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/specs/client.nitro.js +17 -0
- package/lib/commonjs/specs/client.nitro.js.map +1 -0
- package/lib/commonjs/specs/transport.nitro.js +6 -0
- package/lib/commonjs/specs/transport.nitro.js.map +1 -0
- package/lib/commonjs/types/client.js +2 -0
- package/lib/commonjs/types/client.js.map +1 -0
- package/lib/commonjs/utils/client.js +13 -0
- package/lib/commonjs/utils/client.js.map +1 -0
- package/lib/module/index.js +7 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/specs/client.nitro.js +13 -0
- package/lib/module/specs/client.nitro.js.map +1 -0
- package/lib/module/specs/transport.nitro.js +4 -0
- package/lib/module/specs/transport.nitro.js.map +1 -0
- package/lib/module/types/client.js +2 -0
- package/lib/module/types/client.js.map +1 -0
- package/lib/module/utils/client.js +9 -0
- package/lib/module/utils/client.js.map +1 -0
- package/lib/typescript/src/index.d.ts +5 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/specs/client.nitro.d.ts +63 -0
- package/lib/typescript/src/specs/client.nitro.d.ts.map +1 -0
- package/lib/typescript/src/specs/transport.nitro.d.ts +13 -0
- package/lib/typescript/src/specs/transport.nitro.d.ts.map +1 -0
- package/lib/typescript/src/types/client.d.ts +138 -0
- package/lib/typescript/src/types/client.d.ts.map +1 -0
- package/lib/typescript/src/utils/client.d.ts +3 -0
- package/lib/typescript/src/utils/client.d.ts.map +1 -0
- package/nitro.json +30 -30
- package/package.json +4 -4
- package/react-native.config.js +18 -18
- package/src/index.ts +4 -4
- package/src/specs/client.nitro.ts +102 -102
- package/src/specs/transport.nitro.ts +25 -25
- package/src/types/client.ts +196 -196
- package/src/utils/client.ts +10 -10
|
@@ -1,598 +1,598 @@
|
|
|
1
|
-
#include "HybridEcr17Client.hpp"
|
|
2
|
-
|
|
3
|
-
#include <NitroModules/HybridObjectRegistry.hpp>
|
|
4
|
-
|
|
5
|
-
#include <chrono>
|
|
6
|
-
#include <ctime>
|
|
7
|
-
#include <exception>
|
|
8
|
-
#include <optional>
|
|
9
|
-
#include <stdexcept>
|
|
10
|
-
#include <string>
|
|
11
|
-
#include <type_traits>
|
|
12
|
-
#include <utility>
|
|
13
|
-
|
|
14
|
-
#include "Ecr17Protocol/Ecr17Protocol.hpp"
|
|
15
|
-
#include "Ecr17Response/Ecr17Response.hpp"
|
|
16
|
-
#include "Session/RetryPolicy.hpp"
|
|
17
|
-
|
|
18
|
-
// Commands run on Nitro's C++ thread pool. On Android, those worker threads are
|
|
19
|
-
// NOT attached to the JVM, and — even once attached — JNI `FindClass` on an
|
|
20
|
-
// attached worker thread resolves against the *system* class loader, which can't
|
|
21
|
-
// see app/NitroModules classes (e.g. com.margelo.nitro.core.ArrayBuffer, looked
|
|
22
|
-
// up lazily by the generated transport bridge in `send()` via JArrayBuffer::wrap).
|
|
23
|
-
// That yields "Unable to retrieve jni environment" (no JNIEnv) or
|
|
24
|
-
// "ClassNotFoundException ... DexPathList[... /system/lib64 ...]" (wrong loader).
|
|
25
|
-
#ifdef __ANDROID__
|
|
26
|
-
#include <fbjni/fbjni.h>
|
|
27
|
-
#endif
|
|
28
|
-
|
|
29
|
-
namespace margelo::nitro::ecr17 {
|
|
30
|
-
|
|
31
|
-
namespace {
|
|
32
|
-
|
|
33
|
-
// Runs `fn` on Android under fbjni's ThreadScope::WithClassLoader, which attaches
|
|
34
|
-
// the current thread to the JVM AND installs fbjni's cached app class loader for
|
|
35
|
-
// the duration — so every JNI FindClass inside (including NitroModules' lazy
|
|
36
|
-
// ArrayBuffer lookup) resolves app classes, not the system loader. On iOS (no JVM)
|
|
37
|
-
// `fn` is just called directly. Returns whatever `fn` returns (incl. void) and
|
|
38
|
-
// propagates exceptions, so the caller's try/catch and return-value logic is
|
|
39
|
-
// unchanged. WithClassLoader takes a `std::function<void()>`, so on Android the
|
|
40
|
-
// result is captured in a local and any exception via std::exception_ptr, then
|
|
41
|
-
// rethrown after the scope. `fn` is a lambda, so a `return` inside it returns from
|
|
42
|
-
// the lambda (not the caller) on BOTH platforms — safe in value-returning callers.
|
|
43
|
-
template <typename Fn>
|
|
44
|
-
auto runOnJvmThread(Fn&& fn) -> decltype(fn()) {
|
|
45
|
-
#ifdef __ANDROID__
|
|
46
|
-
using Ret = decltype(fn());
|
|
47
|
-
std::exception_ptr err;
|
|
48
|
-
if constexpr (std::is_void_v<Ret>) {
|
|
49
|
-
::facebook::jni::ThreadScope::WithClassLoader([&]() {
|
|
50
|
-
try {
|
|
51
|
-
fn();
|
|
52
|
-
} catch (...) {
|
|
53
|
-
err = std::current_exception();
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
if (err) std::rethrow_exception(err);
|
|
57
|
-
} else {
|
|
58
|
-
std::optional<Ret> result;
|
|
59
|
-
::facebook::jni::ThreadScope::WithClassLoader([&]() {
|
|
60
|
-
try {
|
|
61
|
-
result.emplace(fn());
|
|
62
|
-
} catch (...) {
|
|
63
|
-
err = std::current_exception();
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
if (err) std::rethrow_exception(err);
|
|
67
|
-
return std::move(*result);
|
|
68
|
-
}
|
|
69
|
-
#else
|
|
70
|
-
return fn();
|
|
71
|
-
#endif
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
} // namespace
|
|
75
|
-
|
|
76
|
-
using margelo::nitro::HybridObjectRegistry;
|
|
77
|
-
using margelo::nitro::Promise;
|
|
78
|
-
|
|
79
|
-
namespace {
|
|
80
|
-
|
|
81
|
-
std::optional<std::string> optStr(const std::string& s) {
|
|
82
|
-
return s.empty() ? std::nullopt : std::optional<std::string>(s);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
std::optional<double> optNum(const std::string& s) {
|
|
86
|
-
if (s.empty()) {
|
|
87
|
-
return std::nullopt;
|
|
88
|
-
}
|
|
89
|
-
try {
|
|
90
|
-
return std::stod(s);
|
|
91
|
-
} catch (...) {
|
|
92
|
-
return std::nullopt;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
TransactionOutcome mapOutcome(Outcome o) {
|
|
97
|
-
switch (o) {
|
|
98
|
-
case Outcome::Ok: return TransactionOutcome::OK;
|
|
99
|
-
case Outcome::Ko: return TransactionOutcome::KO;
|
|
100
|
-
case Outcome::CardNotPresent: return TransactionOutcome::CARDNOTPRESENT;
|
|
101
|
-
case Outcome::UnknownTag: return TransactionOutcome::UNKNOWNTAG;
|
|
102
|
-
default: return TransactionOutcome::UNKNOWN;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
std::optional<CardType> mapCardType(const std::string& raw) {
|
|
107
|
-
if (raw == "1") return CardType::DEBIT;
|
|
108
|
-
if (raw == "2") return CardType::CREDIT;
|
|
109
|
-
if (raw == "3") return CardType::OTHER;
|
|
110
|
-
return std::nullopt;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
std::optional<TransactionEntryMode> mapEntryMode(const std::string& raw) {
|
|
114
|
-
if (raw == "ICC") return TransactionEntryMode::ICC;
|
|
115
|
-
if (raw == "MAG") return TransactionEntryMode::MAG;
|
|
116
|
-
if (raw == "MAN") return TransactionEntryMode::MANUAL;
|
|
117
|
-
if (raw == "CLM") return TransactionEntryMode::CLESSMAG;
|
|
118
|
-
if (raw == "CLI") return TransactionEntryMode::CLESSICC;
|
|
119
|
-
return std::nullopt;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
char mapPaymentType(const std::optional<PaymentCardType>& t) {
|
|
123
|
-
if (!t.has_value()) return '0';
|
|
124
|
-
switch (*t) {
|
|
125
|
-
case PaymentCardType::DEBIT: return '1';
|
|
126
|
-
case PaymentCardType::CREDIT: return '2';
|
|
127
|
-
case PaymentCardType::OTHER: return '3';
|
|
128
|
-
default: return '0'; // AUTO
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
PaymentResult mapPayment(const PaymentResponse& p) {
|
|
133
|
-
PaymentResult r;
|
|
134
|
-
r.outcome = mapOutcome(p.outcome);
|
|
135
|
-
r.resultCode = p.resultCode;
|
|
136
|
-
r.pan = optStr(p.pan);
|
|
137
|
-
r.entryMode = mapEntryMode(p.transactionType);
|
|
138
|
-
r.authCode = optStr(p.authCode);
|
|
139
|
-
r.hostDateTime = optStr(p.hostDateTime);
|
|
140
|
-
r.cardType = mapCardType(p.cardType);
|
|
141
|
-
r.acquirerId = optStr(p.acquirerId);
|
|
142
|
-
r.stan = optStr(p.stan);
|
|
143
|
-
r.onlineId = optStr(p.onlineId);
|
|
144
|
-
r.errorDescription = optStr(p.errorDescription);
|
|
145
|
-
if (p.currency.applied) {
|
|
146
|
-
CurrencyExchange ce; // the Nitro-generated struct
|
|
147
|
-
ce.applied = true;
|
|
148
|
-
ce.rate = optNum(p.currency.rate);
|
|
149
|
-
ce.currencyCode = optStr(p.currency.currencyCode);
|
|
150
|
-
ce.amountCents = optNum(p.currency.amount);
|
|
151
|
-
ce.precision = optNum(p.currency.precision);
|
|
152
|
-
r.currencyExchange = ce;
|
|
153
|
-
}
|
|
154
|
-
return r;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
ReversalResult mapReversal(const PaymentResponse& p) {
|
|
158
|
-
ReversalResult r;
|
|
159
|
-
r.outcome = mapOutcome(p.outcome);
|
|
160
|
-
r.resultCode = p.resultCode;
|
|
161
|
-
r.pan = optStr(p.pan);
|
|
162
|
-
r.entryMode = mapEntryMode(p.transactionType);
|
|
163
|
-
r.hostDateTime = optStr(p.hostDateTime);
|
|
164
|
-
r.cardType = mapCardType(p.cardType);
|
|
165
|
-
r.acquirerId = optStr(p.acquirerId);
|
|
166
|
-
r.stan = optStr(p.stan);
|
|
167
|
-
r.onlineId = optStr(p.onlineId);
|
|
168
|
-
r.errorDescription = optStr(p.errorDescription);
|
|
169
|
-
return r;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
CardVerificationResult mapCardVerify(const PaymentResponse& p) {
|
|
173
|
-
CardVerificationResult r;
|
|
174
|
-
r.outcome = mapOutcome(p.outcome);
|
|
175
|
-
r.resultCode = p.resultCode;
|
|
176
|
-
r.pan = optStr(p.pan);
|
|
177
|
-
r.entryMode = mapEntryMode(p.transactionType);
|
|
178
|
-
r.authCode = optStr(p.authCode);
|
|
179
|
-
r.hostDateTime = optStr(p.hostDateTime);
|
|
180
|
-
r.cardType = mapCardType(p.cardType);
|
|
181
|
-
r.acquirerId = optStr(p.acquirerId);
|
|
182
|
-
r.stan = optStr(p.stan);
|
|
183
|
-
r.onlineId = optStr(p.onlineId);
|
|
184
|
-
r.errorDescription = optStr(p.errorDescription);
|
|
185
|
-
return r;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
PreAuthResult mapPreAuth(const PreAuthResponse& p) {
|
|
189
|
-
PreAuthResult r;
|
|
190
|
-
r.outcome = mapOutcome(p.outcome);
|
|
191
|
-
r.resultCode = p.resultCode;
|
|
192
|
-
r.pan = optStr(p.pan);
|
|
193
|
-
r.entryMode = mapEntryMode(p.transactionType);
|
|
194
|
-
r.authCode = optStr(p.authCode);
|
|
195
|
-
r.preAuthorizedAmountCents = optNum(p.preAuthorizedAmount);
|
|
196
|
-
r.preAuthCode = optStr(p.preAuthCode);
|
|
197
|
-
r.actionCode = optStr(p.actionCode);
|
|
198
|
-
r.hostDateTime = optStr(p.hostDateTime);
|
|
199
|
-
r.cardType = mapCardType(p.cardType);
|
|
200
|
-
r.acquirerId = optStr(p.acquirerId);
|
|
201
|
-
r.stan = optStr(p.stan);
|
|
202
|
-
r.onlineId = optStr(p.onlineId);
|
|
203
|
-
r.errorDescription = optStr(p.errorDescription);
|
|
204
|
-
return r;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
PosStatusResponse mapStatus(const StatusResponse& s) {
|
|
208
|
-
PosStatusResponse r;
|
|
209
|
-
r.terminalId = s.terminalId;
|
|
210
|
-
r.status = static_cast<double>(s.status);
|
|
211
|
-
r.softwareRelease = s.softwareRelease;
|
|
212
|
-
// Parse "DDMMYYhhmm" into a time_point; fall back to epoch on bad input.
|
|
213
|
-
std::chrono::system_clock::time_point tp{};
|
|
214
|
-
if (s.dateTimeRaw.size() >= 10) {
|
|
215
|
-
try {
|
|
216
|
-
std::tm tm{};
|
|
217
|
-
tm.tm_mday = std::stoi(s.dateTimeRaw.substr(0, 2));
|
|
218
|
-
tm.tm_mon = std::stoi(s.dateTimeRaw.substr(2, 2)) - 1;
|
|
219
|
-
tm.tm_year = 100 + std::stoi(s.dateTimeRaw.substr(4, 2)); // 20YY
|
|
220
|
-
tm.tm_hour = std::stoi(s.dateTimeRaw.substr(6, 2));
|
|
221
|
-
tm.tm_min = std::stoi(s.dateTimeRaw.substr(8, 2));
|
|
222
|
-
tm.tm_isdst = -1;
|
|
223
|
-
std::time_t t = std::mktime(&tm);
|
|
224
|
-
if (t != -1) {
|
|
225
|
-
tp = std::chrono::system_clock::from_time_t(t);
|
|
226
|
-
}
|
|
227
|
-
} catch (...) {
|
|
228
|
-
// keep epoch
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
r.terminalDateTime = tp;
|
|
232
|
-
return r;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
TotalsResult mapTotals(const TotalsResponse& t) {
|
|
236
|
-
TotalsResult r;
|
|
237
|
-
r.outcome = mapOutcome(t.outcome);
|
|
238
|
-
r.resultCode = t.resultCode;
|
|
239
|
-
r.posTotalCents = optNum(t.posTotal).value_or(0.0);
|
|
240
|
-
return r;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
CloseSessionResult mapClose(const CloseResponse& c) {
|
|
244
|
-
CloseSessionResult r;
|
|
245
|
-
r.outcome = mapOutcome(c.outcome);
|
|
246
|
-
r.resultCode = c.resultCode;
|
|
247
|
-
r.posTotalCents = optNum(c.posTotal);
|
|
248
|
-
r.hostTotalCents = optNum(c.hostTotal);
|
|
249
|
-
r.actionCode = optStr(c.actionCode);
|
|
250
|
-
r.errorDescription = optStr(c.errorDescription);
|
|
251
|
-
return r;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
VasResult mapVas(const VasResponse& v) {
|
|
255
|
-
VasResult r;
|
|
256
|
-
r.responseId = v.responseId;
|
|
257
|
-
r.responseMessage = v.responseMessage;
|
|
258
|
-
r.orderId = optStr(v.orderId);
|
|
259
|
-
r.rawXml = v.rawXml;
|
|
260
|
-
return r;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
} // namespace
|
|
264
|
-
|
|
265
|
-
void HybridEcr17Client::configure(const Ecr17Config& config) {
|
|
266
|
-
config_ = config;
|
|
267
|
-
// Close any open socket before tearing down the old transport, otherwise the
|
|
268
|
-
// prior native connection leaks until the HybridObject is collected.
|
|
269
|
-
if (transport_) {
|
|
270
|
-
transport_->disconnect();
|
|
271
|
-
}
|
|
272
|
-
// Force re-init so a new configuration rebuilds the session/timeouts.
|
|
273
|
-
session_.reset();
|
|
274
|
-
adapter_.reset();
|
|
275
|
-
transport_.reset();
|
|
276
|
-
// Create the transport HybridObject NOW, on this (JS) thread. createHybridObject
|
|
277
|
-
// does a JNI FindClass for the Kotlin transport, which resolves only against the
|
|
278
|
-
// app class loader — and the JS thread has it. Doing it lazily on a Nitro worker
|
|
279
|
-
// thread (attached via ThreadScope) would use the system class loader and throw
|
|
280
|
-
// ClassNotFoundException. fbjni caches the resolved jclass globally, so later
|
|
281
|
-
// method calls from worker threads work (they only need a JNIEnv, see the guards).
|
|
282
|
-
ensureInit();
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
Ecr17Config HybridEcr17Client::configuration() { return config_; }
|
|
286
|
-
|
|
287
|
-
void HybridEcr17Client::ensureInit() {
|
|
288
|
-
if (session_) {
|
|
289
|
-
return;
|
|
290
|
-
}
|
|
291
|
-
auto obj = HybridObjectRegistry::createHybridObject("Ecr17Transport");
|
|
292
|
-
// HybridObject is a *virtual* base, so static_pointer_cast can't downcast
|
|
293
|
-
// from it — must use dynamic_pointer_cast.
|
|
294
|
-
transport_ = std::dynamic_pointer_cast<HybridEcr17TransportSpec>(obj);
|
|
295
|
-
if (!transport_) {
|
|
296
|
-
throw std::runtime_error("ECR17: registry returned an incompatible Ecr17Transport object");
|
|
297
|
-
}
|
|
298
|
-
adapter_ = std::make_shared<NativeTransportAdapter>(transport_);
|
|
299
|
-
|
|
300
|
-
SessionConfig sc;
|
|
301
|
-
sc.lrcMode = config_.lrcMode.value_or(LrcMode::STD);
|
|
302
|
-
sc.ackTimeoutMs = static_cast<int>(config_.ackTimeoutMs.value_or(2000));
|
|
303
|
-
sc.responseTimeoutMs = static_cast<int>(config_.responseTimeoutMs.value_or(60000));
|
|
304
|
-
sc.retryCount = static_cast<int>(config_.retryCount.value_or(3));
|
|
305
|
-
sc.retryDelayMs = static_cast<int>(config_.retryDelayMs.value_or(200));
|
|
306
|
-
sc.receiptDrainMs = static_cast<int>(config_.receiptDrainMs.value_or(0));
|
|
307
|
-
session_ = std::make_unique<Ecr17Session>(*adapter_, sc);
|
|
308
|
-
|
|
309
|
-
session_->setOnProgress([this](const std::string& message) {
|
|
310
|
-
if (onProgress_) onProgress_(ProgressEvent{message});
|
|
311
|
-
});
|
|
312
|
-
session_->setOnReceiptLine([this](const std::string& line) {
|
|
313
|
-
if (onReceiptLine_) onReceiptLine_(ReceiptLine{line});
|
|
314
|
-
});
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
void HybridEcr17Client::ensureConnected() {
|
|
318
|
-
// Run the transport JNI work under the app class loader (Android); inline on iOS.
|
|
319
|
-
runOnJvmThread([&]() {
|
|
320
|
-
ensureInit();
|
|
321
|
-
// PROACTIVE reconnect: isConnected() performs a synchronous, non-destructive
|
|
322
|
-
// liveness probe (Android: a 1-byte peek-with-pushback that detects a peer
|
|
323
|
-
// FIN without writing to or consuming from the stream) so a peer-closed/
|
|
324
|
-
// half-open socket — common because ECR17/Nexi terminals close TCP between
|
|
325
|
-
// transactions — is detected HERE, before any command is sent. This is what
|
|
326
|
-
// stops a financial command from being sent on a stale socket and then
|
|
327
|
-
// hitting the (correct) money-safety "never replay" path with a FALSE
|
|
328
|
-
// "transport disconnected".
|
|
329
|
-
if (transport_->isConnected()) {
|
|
330
|
-
return; // verified live — returns from this lambda only, no further JNI
|
|
331
|
-
}
|
|
332
|
-
// Auto-connect: block this worker thread until the native transport connects
|
|
333
|
-
// (or throw on failure). keepAlive leaves the socket open for reuse.
|
|
334
|
-
if (onConnectionStateChange_) onConnectionStateChange_(ConnectionState::CONNECTING);
|
|
335
|
-
const double port = config_.port.value_or(10000);
|
|
336
|
-
const double timeout = config_.connectionTimeoutMs.value_or(5000);
|
|
337
|
-
try {
|
|
338
|
-
transport_->connect(config_.host, port, timeout)->await().get();
|
|
339
|
-
} catch (...) {
|
|
340
|
-
// Don't leave listeners stuck on CONNECTING when the connection fails.
|
|
341
|
-
if (onConnectionStateChange_) onConnectionStateChange_(ConnectionState::DISCONNECTED);
|
|
342
|
-
throw;
|
|
343
|
-
}
|
|
344
|
-
if (onConnectionStateChange_) onConnectionStateChange_(ConnectionState::CONNECTED);
|
|
345
|
-
});
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
std::string HybridEcr17Client::cashRegisterIdOr(const std::optional<std::string>& override) const {
|
|
349
|
-
return override.value_or(config_.cashRegisterId);
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
DecodedPacket HybridEcr17Client::runTransaction(
|
|
353
|
-
const std::string& mainPayload, const std::optional<TokenizationRequest>& tokenization,
|
|
354
|
-
bool safeToRetry) {
|
|
355
|
-
std::lock_guard<std::mutex> txLock(txMutex_); // serialize exchanges on the shared session
|
|
356
|
-
auto doExchange = [&]() -> DecodedPacket {
|
|
357
|
-
if (tokenization.has_value()) {
|
|
358
|
-
const bool recurring = tokenization->service == TokenizationService::RECURRING;
|
|
359
|
-
const std::string tag =
|
|
360
|
-
Ecr17Protocol::formatTokenizationTag(recurring, tokenization->contractCode);
|
|
361
|
-
const std::string additional =
|
|
362
|
-
Ecr17Protocol::buildAdditionalTagsMessage(config_.terminalId, tag);
|
|
363
|
-
return session_->exchangeWithAdditionalData(mainPayload, additional);
|
|
364
|
-
}
|
|
365
|
-
return session_->exchange(mainPayload);
|
|
366
|
-
};
|
|
367
|
-
|
|
368
|
-
// All exchange JNI (session_ -> adapter_ -> Kotlin transport, incl. the
|
|
369
|
-
// ArrayBuffer lookup in send()) must run under the app class loader on Android.
|
|
370
|
-
return runOnJvmThread([&]() -> DecodedPacket {
|
|
371
|
-
try {
|
|
372
|
-
return doExchange();
|
|
373
|
-
} catch (const std::exception&) {
|
|
374
|
-
const auto originalError = std::current_exception();
|
|
375
|
-
const bool autoReconnect = config_.autoReconnect.value_or(false);
|
|
376
|
-
const bool dropped = !transport_ || !transport_->isConnected();
|
|
377
|
-
if (autoReconnect && dropped) {
|
|
378
|
-
try {
|
|
379
|
-
ensureConnected(); // restore the socket for subsequent commands
|
|
380
|
-
} catch (...) {
|
|
381
|
-
// Reconnect failed: surface the original exchange error, not the
|
|
382
|
-
// reconnect failure (the former is what the caller needs to see).
|
|
383
|
-
std::rethrow_exception(originalError);
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
if (shouldRetryAfterReconnect(autoReconnect, dropped, safeToRetry)) {
|
|
387
|
-
return doExchange(); // only read-only/idempotent ops may be replayed
|
|
388
|
-
}
|
|
389
|
-
throw; // financial op: surface the error (recover via sendLastResult / 'G')
|
|
390
|
-
}
|
|
391
|
-
});
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
void HybridEcr17Client::runAckOnly(const std::string& payload, bool safeToRetry) {
|
|
395
|
-
std::lock_guard<std::mutex> txLock(txMutex_); // serialize exchanges on the shared session
|
|
396
|
-
// Send under the app class loader on Android (ArrayBuffer lookup in send()).
|
|
397
|
-
runOnJvmThread([&]() {
|
|
398
|
-
try {
|
|
399
|
-
session_->sendAckOnly(payload);
|
|
400
|
-
} catch (const std::exception&) {
|
|
401
|
-
const auto originalError = std::current_exception();
|
|
402
|
-
const bool autoReconnect = config_.autoReconnect.value_or(false);
|
|
403
|
-
const bool dropped = !transport_ || !transport_->isConnected();
|
|
404
|
-
if (autoReconnect && dropped) {
|
|
405
|
-
try {
|
|
406
|
-
ensureConnected();
|
|
407
|
-
} catch (...) {
|
|
408
|
-
std::rethrow_exception(originalError); // surface the original error
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
if (!shouldRetryAfterReconnect(autoReconnect, dropped, safeToRetry)) {
|
|
412
|
-
throw; // not retryable: surface the original error
|
|
413
|
-
}
|
|
414
|
-
session_->sendAckOnly(payload); // read-only/idempotent op: safe to replay
|
|
415
|
-
}
|
|
416
|
-
});
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
std::shared_ptr<Promise<void>> HybridEcr17Client::connect() {
|
|
420
|
-
// Delegate to ensureConnected so the explicit Connect path emits CONNECTING
|
|
421
|
-
// and then CONNECTED on success (consistent with command auto-connect);
|
|
422
|
-
// returning the raw transport promise would leave listeners stuck on
|
|
423
|
-
// CONNECTING. Runs on a worker thread (ensureConnected blocks until ready).
|
|
424
|
-
return Promise<void>::async([this]() { ensureConnected(); });
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
void HybridEcr17Client::disconnect() {
|
|
428
|
-
if (transport_) {
|
|
429
|
-
transport_->disconnect();
|
|
430
|
-
}
|
|
431
|
-
if (onConnectionStateChange_) onConnectionStateChange_(ConnectionState::DISCONNECTED);
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
bool HybridEcr17Client::isConnected() { return transport_ && transport_->isConnected(); }
|
|
435
|
-
|
|
436
|
-
std::shared_ptr<Promise<PosStatusResponse>> HybridEcr17Client::status() {
|
|
437
|
-
return Promise<PosStatusResponse>::async([this]() -> PosStatusResponse {
|
|
438
|
-
ensureConnected();
|
|
439
|
-
auto pkt = runTransaction(Ecr17Protocol::buildStatusMessage(config_.terminalId),
|
|
440
|
-
std::nullopt, /*safeToRetry=*/true);
|
|
441
|
-
return mapStatus(Ecr17Response::parseStatus(pkt.payload));
|
|
442
|
-
});
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
std::shared_ptr<Promise<PaymentResult>> HybridEcr17Client::pay(const PaymentRequest& request) {
|
|
446
|
-
return Promise<PaymentResult>::async([this, request]() -> PaymentResult {
|
|
447
|
-
ensureConnected();
|
|
448
|
-
const bool tok = request.tokenization.has_value();
|
|
449
|
-
auto payload = Ecr17Protocol::buildPaymentMessage(
|
|
450
|
-
config_.terminalId, cashRegisterIdOr(request.cashRegisterId),
|
|
451
|
-
static_cast<int>(request.amountCents), mapPaymentType(request.paymentType),
|
|
452
|
-
request.cardAlreadyPresent.value_or(false), tok, request.receiptText.value_or(""));
|
|
453
|
-
auto pkt = runTransaction(payload, request.tokenization, false);
|
|
454
|
-
return mapPayment(Ecr17Response::parsePayment(pkt.payload));
|
|
455
|
-
});
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
std::shared_ptr<Promise<PaymentResult>> HybridEcr17Client::payExtended(const PaymentRequest& request) {
|
|
459
|
-
return Promise<PaymentResult>::async([this, request]() -> PaymentResult {
|
|
460
|
-
ensureConnected();
|
|
461
|
-
const bool tok = request.tokenization.has_value();
|
|
462
|
-
auto payload = Ecr17Protocol::buildExtendedPaymentMessage(
|
|
463
|
-
config_.terminalId, cashRegisterIdOr(request.cashRegisterId),
|
|
464
|
-
static_cast<int>(request.amountCents), mapPaymentType(request.paymentType),
|
|
465
|
-
request.cardAlreadyPresent.value_or(false), tok, request.receiptText.value_or(""));
|
|
466
|
-
auto pkt = runTransaction(payload, request.tokenization, false);
|
|
467
|
-
return mapPayment(Ecr17Response::parsePayment(pkt.payload));
|
|
468
|
-
});
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
std::shared_ptr<Promise<ReversalResult>> HybridEcr17Client::reverse(const ReversalRequest& request) {
|
|
472
|
-
return Promise<ReversalResult>::async([this, request]() -> ReversalResult {
|
|
473
|
-
ensureConnected();
|
|
474
|
-
auto payload = Ecr17Protocol::buildReversalMessage(
|
|
475
|
-
config_.terminalId, cashRegisterIdOr(request.cashRegisterId),
|
|
476
|
-
request.stan.value_or("000000"));
|
|
477
|
-
auto pkt = runTransaction(payload, std::nullopt, /*safeToRetry=*/false);
|
|
478
|
-
return mapReversal(Ecr17Response::parsePayment(pkt.payload));
|
|
479
|
-
});
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
std::shared_ptr<Promise<PreAuthResult>> HybridEcr17Client::preAuth(const PreAuthRequest& request) {
|
|
483
|
-
return Promise<PreAuthResult>::async([this, request]() -> PreAuthResult {
|
|
484
|
-
ensureConnected();
|
|
485
|
-
const bool tok = request.tokenization.has_value();
|
|
486
|
-
auto payload = Ecr17Protocol::buildPreAuthMessage(
|
|
487
|
-
config_.terminalId, cashRegisterIdOr(request.cashRegisterId),
|
|
488
|
-
static_cast<int>(request.amountCents), mapPaymentType(request.paymentType),
|
|
489
|
-
request.cardAlreadyPresent.value_or(false), tok, request.receiptText.value_or(""));
|
|
490
|
-
auto pkt = runTransaction(payload, request.tokenization, false);
|
|
491
|
-
return mapPreAuth(Ecr17Response::parsePreAuth(pkt.payload));
|
|
492
|
-
});
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
std::shared_ptr<Promise<PreAuthResult>> HybridEcr17Client::incrementalAuth(
|
|
496
|
-
const IncrementalAuthRequest& request) {
|
|
497
|
-
return Promise<PreAuthResult>::async([this, request]() -> PreAuthResult {
|
|
498
|
-
ensureConnected();
|
|
499
|
-
auto payload = Ecr17Protocol::buildIncrementalMessage(
|
|
500
|
-
config_.terminalId, cashRegisterIdOr(request.cashRegisterId),
|
|
501
|
-
static_cast<int>(request.amountCents), request.originalPreAuthCode, false,
|
|
502
|
-
request.receiptText.value_or(""));
|
|
503
|
-
auto pkt = runTransaction(payload, std::nullopt, /*safeToRetry=*/false);
|
|
504
|
-
return mapPreAuth(Ecr17Response::parsePreAuth(pkt.payload));
|
|
505
|
-
});
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
std::shared_ptr<Promise<PaymentResult>> HybridEcr17Client::preAuthClosure(
|
|
509
|
-
const PreAuthClosureRequest& request) {
|
|
510
|
-
return Promise<PaymentResult>::async([this, request]() -> PaymentResult {
|
|
511
|
-
ensureConnected();
|
|
512
|
-
auto payload = Ecr17Protocol::buildPreAuthClosureMessage(
|
|
513
|
-
config_.terminalId, cashRegisterIdOr(request.cashRegisterId),
|
|
514
|
-
static_cast<int>(request.amountCents), request.originalPreAuthCode, false,
|
|
515
|
-
request.receiptText.value_or(""));
|
|
516
|
-
auto pkt = runTransaction(payload, std::nullopt, /*safeToRetry=*/false);
|
|
517
|
-
return mapPayment(Ecr17Response::parsePayment(pkt.payload));
|
|
518
|
-
});
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
std::shared_ptr<Promise<CardVerificationResult>> HybridEcr17Client::verifyCard(
|
|
522
|
-
const CardVerificationRequest& request) {
|
|
523
|
-
return Promise<CardVerificationResult>::async([this, request]() -> CardVerificationResult {
|
|
524
|
-
ensureConnected();
|
|
525
|
-
const bool tok = request.tokenization.has_value();
|
|
526
|
-
auto payload = Ecr17Protocol::buildCardVerificationMessage(
|
|
527
|
-
config_.terminalId, cashRegisterIdOr(request.cashRegisterId),
|
|
528
|
-
mapPaymentType(request.paymentType), tok);
|
|
529
|
-
auto pkt = runTransaction(payload, request.tokenization, false);
|
|
530
|
-
return mapCardVerify(Ecr17Response::parsePayment(pkt.payload));
|
|
531
|
-
});
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
std::shared_ptr<Promise<CloseSessionResult>> HybridEcr17Client::closeSession() {
|
|
535
|
-
return Promise<CloseSessionResult>::async([this]() -> CloseSessionResult {
|
|
536
|
-
ensureConnected();
|
|
537
|
-
auto payload = Ecr17Protocol::buildCloseSessionMessage(config_.terminalId, config_.cashRegisterId);
|
|
538
|
-
auto pkt = runTransaction(payload, std::nullopt, /*safeToRetry=*/false);
|
|
539
|
-
return mapClose(Ecr17Response::parseClose(pkt.payload));
|
|
540
|
-
});
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
std::shared_ptr<Promise<TotalsResult>> HybridEcr17Client::totals() {
|
|
544
|
-
return Promise<TotalsResult>::async([this]() -> TotalsResult {
|
|
545
|
-
ensureConnected();
|
|
546
|
-
auto payload = Ecr17Protocol::buildTotalsMessage(config_.terminalId, config_.cashRegisterId);
|
|
547
|
-
auto pkt = runTransaction(payload, std::nullopt, /*safeToRetry=*/true);
|
|
548
|
-
return mapTotals(Ecr17Response::parseTotals(pkt.payload));
|
|
549
|
-
});
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
std::shared_ptr<Promise<PaymentResult>> HybridEcr17Client::sendLastResult() {
|
|
553
|
-
return Promise<PaymentResult>::async([this]() -> PaymentResult {
|
|
554
|
-
ensureConnected();
|
|
555
|
-
auto payload = Ecr17Protocol::buildSendLastResultMessage(config_.terminalId, config_.cashRegisterId);
|
|
556
|
-
auto pkt = runTransaction(payload, std::nullopt, /*safeToRetry=*/true);
|
|
557
|
-
return mapPayment(Ecr17Response::parsePayment(pkt.payload));
|
|
558
|
-
});
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
std::shared_ptr<Promise<void>> HybridEcr17Client::enableEcrPrinting(bool enabled) {
|
|
562
|
-
return Promise<void>::async([this, enabled]() {
|
|
563
|
-
ensureConnected();
|
|
564
|
-
runAckOnly(Ecr17Protocol::buildEnableEcrPrintMessage(config_.terminalId, enabled),
|
|
565
|
-
/*safeToRetry=*/true);
|
|
566
|
-
});
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
std::shared_ptr<Promise<void>> HybridEcr17Client::reprint(bool toEcr) {
|
|
570
|
-
return Promise<void>::async([this, toEcr]() {
|
|
571
|
-
ensureConnected();
|
|
572
|
-
runAckOnly(Ecr17Protocol::buildReprintMessage(config_.terminalId, toEcr),
|
|
573
|
-
/*safeToRetry=*/false);
|
|
574
|
-
});
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
std::shared_ptr<Promise<VasResult>> HybridEcr17Client::vas(const std::string& xmlRequest) {
|
|
578
|
-
return Promise<VasResult>::async([this, xmlRequest]() -> VasResult {
|
|
579
|
-
ensureConnected();
|
|
580
|
-
auto payload = Ecr17Protocol::buildVasMessage(config_.terminalId, config_.cashRegisterId, xmlRequest);
|
|
581
|
-
auto pkt = runTransaction(payload, std::nullopt, /*safeToRetry=*/false);
|
|
582
|
-
return mapVas(Ecr17Response::parseVas(pkt.payload));
|
|
583
|
-
});
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
void HybridEcr17Client::setOnProgress(const std::function<void(const ProgressEvent&)>& callback) {
|
|
587
|
-
onProgress_ = callback;
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
void HybridEcr17Client::setOnReceiptLine(const std::function<void(const ReceiptLine&)>& callback) {
|
|
591
|
-
onReceiptLine_ = callback;
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
void HybridEcr17Client::setOnConnectionStateChange(const std::function<void(ConnectionState)>& callback) {
|
|
595
|
-
onConnectionStateChange_ = callback;
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
} // namespace margelo::nitro::ecr17
|
|
1
|
+
#include "HybridEcr17Client.hpp"
|
|
2
|
+
|
|
3
|
+
#include <NitroModules/HybridObjectRegistry.hpp>
|
|
4
|
+
|
|
5
|
+
#include <chrono>
|
|
6
|
+
#include <ctime>
|
|
7
|
+
#include <exception>
|
|
8
|
+
#include <optional>
|
|
9
|
+
#include <stdexcept>
|
|
10
|
+
#include <string>
|
|
11
|
+
#include <type_traits>
|
|
12
|
+
#include <utility>
|
|
13
|
+
|
|
14
|
+
#include "Ecr17Protocol/Ecr17Protocol.hpp"
|
|
15
|
+
#include "Ecr17Response/Ecr17Response.hpp"
|
|
16
|
+
#include "Session/RetryPolicy.hpp"
|
|
17
|
+
|
|
18
|
+
// Commands run on Nitro's C++ thread pool. On Android, those worker threads are
|
|
19
|
+
// NOT attached to the JVM, and — even once attached — JNI `FindClass` on an
|
|
20
|
+
// attached worker thread resolves against the *system* class loader, which can't
|
|
21
|
+
// see app/NitroModules classes (e.g. com.margelo.nitro.core.ArrayBuffer, looked
|
|
22
|
+
// up lazily by the generated transport bridge in `send()` via JArrayBuffer::wrap).
|
|
23
|
+
// That yields "Unable to retrieve jni environment" (no JNIEnv) or
|
|
24
|
+
// "ClassNotFoundException ... DexPathList[... /system/lib64 ...]" (wrong loader).
|
|
25
|
+
#ifdef __ANDROID__
|
|
26
|
+
#include <fbjni/fbjni.h>
|
|
27
|
+
#endif
|
|
28
|
+
|
|
29
|
+
namespace margelo::nitro::ecr17 {
|
|
30
|
+
|
|
31
|
+
namespace {
|
|
32
|
+
|
|
33
|
+
// Runs `fn` on Android under fbjni's ThreadScope::WithClassLoader, which attaches
|
|
34
|
+
// the current thread to the JVM AND installs fbjni's cached app class loader for
|
|
35
|
+
// the duration — so every JNI FindClass inside (including NitroModules' lazy
|
|
36
|
+
// ArrayBuffer lookup) resolves app classes, not the system loader. On iOS (no JVM)
|
|
37
|
+
// `fn` is just called directly. Returns whatever `fn` returns (incl. void) and
|
|
38
|
+
// propagates exceptions, so the caller's try/catch and return-value logic is
|
|
39
|
+
// unchanged. WithClassLoader takes a `std::function<void()>`, so on Android the
|
|
40
|
+
// result is captured in a local and any exception via std::exception_ptr, then
|
|
41
|
+
// rethrown after the scope. `fn` is a lambda, so a `return` inside it returns from
|
|
42
|
+
// the lambda (not the caller) on BOTH platforms — safe in value-returning callers.
|
|
43
|
+
template <typename Fn>
|
|
44
|
+
auto runOnJvmThread(Fn&& fn) -> decltype(fn()) {
|
|
45
|
+
#ifdef __ANDROID__
|
|
46
|
+
using Ret = decltype(fn());
|
|
47
|
+
std::exception_ptr err;
|
|
48
|
+
if constexpr (std::is_void_v<Ret>) {
|
|
49
|
+
::facebook::jni::ThreadScope::WithClassLoader([&]() {
|
|
50
|
+
try {
|
|
51
|
+
fn();
|
|
52
|
+
} catch (...) {
|
|
53
|
+
err = std::current_exception();
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
if (err) std::rethrow_exception(err);
|
|
57
|
+
} else {
|
|
58
|
+
std::optional<Ret> result;
|
|
59
|
+
::facebook::jni::ThreadScope::WithClassLoader([&]() {
|
|
60
|
+
try {
|
|
61
|
+
result.emplace(fn());
|
|
62
|
+
} catch (...) {
|
|
63
|
+
err = std::current_exception();
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
if (err) std::rethrow_exception(err);
|
|
67
|
+
return std::move(*result);
|
|
68
|
+
}
|
|
69
|
+
#else
|
|
70
|
+
return fn();
|
|
71
|
+
#endif
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
} // namespace
|
|
75
|
+
|
|
76
|
+
using margelo::nitro::HybridObjectRegistry;
|
|
77
|
+
using margelo::nitro::Promise;
|
|
78
|
+
|
|
79
|
+
namespace {
|
|
80
|
+
|
|
81
|
+
std::optional<std::string> optStr(const std::string& s) {
|
|
82
|
+
return s.empty() ? std::nullopt : std::optional<std::string>(s);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
std::optional<double> optNum(const std::string& s) {
|
|
86
|
+
if (s.empty()) {
|
|
87
|
+
return std::nullopt;
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
return std::stod(s);
|
|
91
|
+
} catch (...) {
|
|
92
|
+
return std::nullopt;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
TransactionOutcome mapOutcome(Outcome o) {
|
|
97
|
+
switch (o) {
|
|
98
|
+
case Outcome::Ok: return TransactionOutcome::OK;
|
|
99
|
+
case Outcome::Ko: return TransactionOutcome::KO;
|
|
100
|
+
case Outcome::CardNotPresent: return TransactionOutcome::CARDNOTPRESENT;
|
|
101
|
+
case Outcome::UnknownTag: return TransactionOutcome::UNKNOWNTAG;
|
|
102
|
+
default: return TransactionOutcome::UNKNOWN;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
std::optional<CardType> mapCardType(const std::string& raw) {
|
|
107
|
+
if (raw == "1") return CardType::DEBIT;
|
|
108
|
+
if (raw == "2") return CardType::CREDIT;
|
|
109
|
+
if (raw == "3") return CardType::OTHER;
|
|
110
|
+
return std::nullopt;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
std::optional<TransactionEntryMode> mapEntryMode(const std::string& raw) {
|
|
114
|
+
if (raw == "ICC") return TransactionEntryMode::ICC;
|
|
115
|
+
if (raw == "MAG") return TransactionEntryMode::MAG;
|
|
116
|
+
if (raw == "MAN") return TransactionEntryMode::MANUAL;
|
|
117
|
+
if (raw == "CLM") return TransactionEntryMode::CLESSMAG;
|
|
118
|
+
if (raw == "CLI") return TransactionEntryMode::CLESSICC;
|
|
119
|
+
return std::nullopt;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
char mapPaymentType(const std::optional<PaymentCardType>& t) {
|
|
123
|
+
if (!t.has_value()) return '0';
|
|
124
|
+
switch (*t) {
|
|
125
|
+
case PaymentCardType::DEBIT: return '1';
|
|
126
|
+
case PaymentCardType::CREDIT: return '2';
|
|
127
|
+
case PaymentCardType::OTHER: return '3';
|
|
128
|
+
default: return '0'; // AUTO
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
PaymentResult mapPayment(const PaymentResponse& p) {
|
|
133
|
+
PaymentResult r;
|
|
134
|
+
r.outcome = mapOutcome(p.outcome);
|
|
135
|
+
r.resultCode = p.resultCode;
|
|
136
|
+
r.pan = optStr(p.pan);
|
|
137
|
+
r.entryMode = mapEntryMode(p.transactionType);
|
|
138
|
+
r.authCode = optStr(p.authCode);
|
|
139
|
+
r.hostDateTime = optStr(p.hostDateTime);
|
|
140
|
+
r.cardType = mapCardType(p.cardType);
|
|
141
|
+
r.acquirerId = optStr(p.acquirerId);
|
|
142
|
+
r.stan = optStr(p.stan);
|
|
143
|
+
r.onlineId = optStr(p.onlineId);
|
|
144
|
+
r.errorDescription = optStr(p.errorDescription);
|
|
145
|
+
if (p.currency.applied) {
|
|
146
|
+
CurrencyExchange ce; // the Nitro-generated struct
|
|
147
|
+
ce.applied = true;
|
|
148
|
+
ce.rate = optNum(p.currency.rate);
|
|
149
|
+
ce.currencyCode = optStr(p.currency.currencyCode);
|
|
150
|
+
ce.amountCents = optNum(p.currency.amount);
|
|
151
|
+
ce.precision = optNum(p.currency.precision);
|
|
152
|
+
r.currencyExchange = ce;
|
|
153
|
+
}
|
|
154
|
+
return r;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
ReversalResult mapReversal(const PaymentResponse& p) {
|
|
158
|
+
ReversalResult r;
|
|
159
|
+
r.outcome = mapOutcome(p.outcome);
|
|
160
|
+
r.resultCode = p.resultCode;
|
|
161
|
+
r.pan = optStr(p.pan);
|
|
162
|
+
r.entryMode = mapEntryMode(p.transactionType);
|
|
163
|
+
r.hostDateTime = optStr(p.hostDateTime);
|
|
164
|
+
r.cardType = mapCardType(p.cardType);
|
|
165
|
+
r.acquirerId = optStr(p.acquirerId);
|
|
166
|
+
r.stan = optStr(p.stan);
|
|
167
|
+
r.onlineId = optStr(p.onlineId);
|
|
168
|
+
r.errorDescription = optStr(p.errorDescription);
|
|
169
|
+
return r;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
CardVerificationResult mapCardVerify(const PaymentResponse& p) {
|
|
173
|
+
CardVerificationResult r;
|
|
174
|
+
r.outcome = mapOutcome(p.outcome);
|
|
175
|
+
r.resultCode = p.resultCode;
|
|
176
|
+
r.pan = optStr(p.pan);
|
|
177
|
+
r.entryMode = mapEntryMode(p.transactionType);
|
|
178
|
+
r.authCode = optStr(p.authCode);
|
|
179
|
+
r.hostDateTime = optStr(p.hostDateTime);
|
|
180
|
+
r.cardType = mapCardType(p.cardType);
|
|
181
|
+
r.acquirerId = optStr(p.acquirerId);
|
|
182
|
+
r.stan = optStr(p.stan);
|
|
183
|
+
r.onlineId = optStr(p.onlineId);
|
|
184
|
+
r.errorDescription = optStr(p.errorDescription);
|
|
185
|
+
return r;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
PreAuthResult mapPreAuth(const PreAuthResponse& p) {
|
|
189
|
+
PreAuthResult r;
|
|
190
|
+
r.outcome = mapOutcome(p.outcome);
|
|
191
|
+
r.resultCode = p.resultCode;
|
|
192
|
+
r.pan = optStr(p.pan);
|
|
193
|
+
r.entryMode = mapEntryMode(p.transactionType);
|
|
194
|
+
r.authCode = optStr(p.authCode);
|
|
195
|
+
r.preAuthorizedAmountCents = optNum(p.preAuthorizedAmount);
|
|
196
|
+
r.preAuthCode = optStr(p.preAuthCode);
|
|
197
|
+
r.actionCode = optStr(p.actionCode);
|
|
198
|
+
r.hostDateTime = optStr(p.hostDateTime);
|
|
199
|
+
r.cardType = mapCardType(p.cardType);
|
|
200
|
+
r.acquirerId = optStr(p.acquirerId);
|
|
201
|
+
r.stan = optStr(p.stan);
|
|
202
|
+
r.onlineId = optStr(p.onlineId);
|
|
203
|
+
r.errorDescription = optStr(p.errorDescription);
|
|
204
|
+
return r;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
PosStatusResponse mapStatus(const StatusResponse& s) {
|
|
208
|
+
PosStatusResponse r;
|
|
209
|
+
r.terminalId = s.terminalId;
|
|
210
|
+
r.status = static_cast<double>(s.status);
|
|
211
|
+
r.softwareRelease = s.softwareRelease;
|
|
212
|
+
// Parse "DDMMYYhhmm" into a time_point; fall back to epoch on bad input.
|
|
213
|
+
std::chrono::system_clock::time_point tp{};
|
|
214
|
+
if (s.dateTimeRaw.size() >= 10) {
|
|
215
|
+
try {
|
|
216
|
+
std::tm tm{};
|
|
217
|
+
tm.tm_mday = std::stoi(s.dateTimeRaw.substr(0, 2));
|
|
218
|
+
tm.tm_mon = std::stoi(s.dateTimeRaw.substr(2, 2)) - 1;
|
|
219
|
+
tm.tm_year = 100 + std::stoi(s.dateTimeRaw.substr(4, 2)); // 20YY
|
|
220
|
+
tm.tm_hour = std::stoi(s.dateTimeRaw.substr(6, 2));
|
|
221
|
+
tm.tm_min = std::stoi(s.dateTimeRaw.substr(8, 2));
|
|
222
|
+
tm.tm_isdst = -1;
|
|
223
|
+
std::time_t t = std::mktime(&tm);
|
|
224
|
+
if (t != -1) {
|
|
225
|
+
tp = std::chrono::system_clock::from_time_t(t);
|
|
226
|
+
}
|
|
227
|
+
} catch (...) {
|
|
228
|
+
// keep epoch
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
r.terminalDateTime = tp;
|
|
232
|
+
return r;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
TotalsResult mapTotals(const TotalsResponse& t) {
|
|
236
|
+
TotalsResult r;
|
|
237
|
+
r.outcome = mapOutcome(t.outcome);
|
|
238
|
+
r.resultCode = t.resultCode;
|
|
239
|
+
r.posTotalCents = optNum(t.posTotal).value_or(0.0);
|
|
240
|
+
return r;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
CloseSessionResult mapClose(const CloseResponse& c) {
|
|
244
|
+
CloseSessionResult r;
|
|
245
|
+
r.outcome = mapOutcome(c.outcome);
|
|
246
|
+
r.resultCode = c.resultCode;
|
|
247
|
+
r.posTotalCents = optNum(c.posTotal);
|
|
248
|
+
r.hostTotalCents = optNum(c.hostTotal);
|
|
249
|
+
r.actionCode = optStr(c.actionCode);
|
|
250
|
+
r.errorDescription = optStr(c.errorDescription);
|
|
251
|
+
return r;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
VasResult mapVas(const VasResponse& v) {
|
|
255
|
+
VasResult r;
|
|
256
|
+
r.responseId = v.responseId;
|
|
257
|
+
r.responseMessage = v.responseMessage;
|
|
258
|
+
r.orderId = optStr(v.orderId);
|
|
259
|
+
r.rawXml = v.rawXml;
|
|
260
|
+
return r;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
} // namespace
|
|
264
|
+
|
|
265
|
+
void HybridEcr17Client::configure(const Ecr17Config& config) {
|
|
266
|
+
config_ = config;
|
|
267
|
+
// Close any open socket before tearing down the old transport, otherwise the
|
|
268
|
+
// prior native connection leaks until the HybridObject is collected.
|
|
269
|
+
if (transport_) {
|
|
270
|
+
transport_->disconnect();
|
|
271
|
+
}
|
|
272
|
+
// Force re-init so a new configuration rebuilds the session/timeouts.
|
|
273
|
+
session_.reset();
|
|
274
|
+
adapter_.reset();
|
|
275
|
+
transport_.reset();
|
|
276
|
+
// Create the transport HybridObject NOW, on this (JS) thread. createHybridObject
|
|
277
|
+
// does a JNI FindClass for the Kotlin transport, which resolves only against the
|
|
278
|
+
// app class loader — and the JS thread has it. Doing it lazily on a Nitro worker
|
|
279
|
+
// thread (attached via ThreadScope) would use the system class loader and throw
|
|
280
|
+
// ClassNotFoundException. fbjni caches the resolved jclass globally, so later
|
|
281
|
+
// method calls from worker threads work (they only need a JNIEnv, see the guards).
|
|
282
|
+
ensureInit();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
Ecr17Config HybridEcr17Client::configuration() { return config_; }
|
|
286
|
+
|
|
287
|
+
void HybridEcr17Client::ensureInit() {
|
|
288
|
+
if (session_) {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
auto obj = HybridObjectRegistry::createHybridObject("Ecr17Transport");
|
|
292
|
+
// HybridObject is a *virtual* base, so static_pointer_cast can't downcast
|
|
293
|
+
// from it — must use dynamic_pointer_cast.
|
|
294
|
+
transport_ = std::dynamic_pointer_cast<HybridEcr17TransportSpec>(obj);
|
|
295
|
+
if (!transport_) {
|
|
296
|
+
throw std::runtime_error("ECR17: registry returned an incompatible Ecr17Transport object");
|
|
297
|
+
}
|
|
298
|
+
adapter_ = std::make_shared<NativeTransportAdapter>(transport_);
|
|
299
|
+
|
|
300
|
+
SessionConfig sc;
|
|
301
|
+
sc.lrcMode = config_.lrcMode.value_or(LrcMode::STD);
|
|
302
|
+
sc.ackTimeoutMs = static_cast<int>(config_.ackTimeoutMs.value_or(2000));
|
|
303
|
+
sc.responseTimeoutMs = static_cast<int>(config_.responseTimeoutMs.value_or(60000));
|
|
304
|
+
sc.retryCount = static_cast<int>(config_.retryCount.value_or(3));
|
|
305
|
+
sc.retryDelayMs = static_cast<int>(config_.retryDelayMs.value_or(200));
|
|
306
|
+
sc.receiptDrainMs = static_cast<int>(config_.receiptDrainMs.value_or(0));
|
|
307
|
+
session_ = std::make_unique<Ecr17Session>(*adapter_, sc);
|
|
308
|
+
|
|
309
|
+
session_->setOnProgress([this](const std::string& message) {
|
|
310
|
+
if (onProgress_) onProgress_(ProgressEvent{message});
|
|
311
|
+
});
|
|
312
|
+
session_->setOnReceiptLine([this](const std::string& line) {
|
|
313
|
+
if (onReceiptLine_) onReceiptLine_(ReceiptLine{line});
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
void HybridEcr17Client::ensureConnected() {
|
|
318
|
+
// Run the transport JNI work under the app class loader (Android); inline on iOS.
|
|
319
|
+
runOnJvmThread([&]() {
|
|
320
|
+
ensureInit();
|
|
321
|
+
// PROACTIVE reconnect: isConnected() performs a synchronous, non-destructive
|
|
322
|
+
// liveness probe (Android: a 1-byte peek-with-pushback that detects a peer
|
|
323
|
+
// FIN without writing to or consuming from the stream) so a peer-closed/
|
|
324
|
+
// half-open socket — common because ECR17/Nexi terminals close TCP between
|
|
325
|
+
// transactions — is detected HERE, before any command is sent. This is what
|
|
326
|
+
// stops a financial command from being sent on a stale socket and then
|
|
327
|
+
// hitting the (correct) money-safety "never replay" path with a FALSE
|
|
328
|
+
// "transport disconnected".
|
|
329
|
+
if (transport_->isConnected()) {
|
|
330
|
+
return; // verified live — returns from this lambda only, no further JNI
|
|
331
|
+
}
|
|
332
|
+
// Auto-connect: block this worker thread until the native transport connects
|
|
333
|
+
// (or throw on failure). keepAlive leaves the socket open for reuse.
|
|
334
|
+
if (onConnectionStateChange_) onConnectionStateChange_(ConnectionState::CONNECTING);
|
|
335
|
+
const double port = config_.port.value_or(10000);
|
|
336
|
+
const double timeout = config_.connectionTimeoutMs.value_or(5000);
|
|
337
|
+
try {
|
|
338
|
+
transport_->connect(config_.host, port, timeout)->await().get();
|
|
339
|
+
} catch (...) {
|
|
340
|
+
// Don't leave listeners stuck on CONNECTING when the connection fails.
|
|
341
|
+
if (onConnectionStateChange_) onConnectionStateChange_(ConnectionState::DISCONNECTED);
|
|
342
|
+
throw;
|
|
343
|
+
}
|
|
344
|
+
if (onConnectionStateChange_) onConnectionStateChange_(ConnectionState::CONNECTED);
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
std::string HybridEcr17Client::cashRegisterIdOr(const std::optional<std::string>& override) const {
|
|
349
|
+
return override.value_or(config_.cashRegisterId);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
DecodedPacket HybridEcr17Client::runTransaction(
|
|
353
|
+
const std::string& mainPayload, const std::optional<TokenizationRequest>& tokenization,
|
|
354
|
+
bool safeToRetry) {
|
|
355
|
+
std::lock_guard<std::mutex> txLock(txMutex_); // serialize exchanges on the shared session
|
|
356
|
+
auto doExchange = [&]() -> DecodedPacket {
|
|
357
|
+
if (tokenization.has_value()) {
|
|
358
|
+
const bool recurring = tokenization->service == TokenizationService::RECURRING;
|
|
359
|
+
const std::string tag =
|
|
360
|
+
Ecr17Protocol::formatTokenizationTag(recurring, tokenization->contractCode);
|
|
361
|
+
const std::string additional =
|
|
362
|
+
Ecr17Protocol::buildAdditionalTagsMessage(config_.terminalId, tag);
|
|
363
|
+
return session_->exchangeWithAdditionalData(mainPayload, additional);
|
|
364
|
+
}
|
|
365
|
+
return session_->exchange(mainPayload);
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
// All exchange JNI (session_ -> adapter_ -> Kotlin transport, incl. the
|
|
369
|
+
// ArrayBuffer lookup in send()) must run under the app class loader on Android.
|
|
370
|
+
return runOnJvmThread([&]() -> DecodedPacket {
|
|
371
|
+
try {
|
|
372
|
+
return doExchange();
|
|
373
|
+
} catch (const std::exception&) {
|
|
374
|
+
const auto originalError = std::current_exception();
|
|
375
|
+
const bool autoReconnect = config_.autoReconnect.value_or(false);
|
|
376
|
+
const bool dropped = !transport_ || !transport_->isConnected();
|
|
377
|
+
if (autoReconnect && dropped) {
|
|
378
|
+
try {
|
|
379
|
+
ensureConnected(); // restore the socket for subsequent commands
|
|
380
|
+
} catch (...) {
|
|
381
|
+
// Reconnect failed: surface the original exchange error, not the
|
|
382
|
+
// reconnect failure (the former is what the caller needs to see).
|
|
383
|
+
std::rethrow_exception(originalError);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if (shouldRetryAfterReconnect(autoReconnect, dropped, safeToRetry)) {
|
|
387
|
+
return doExchange(); // only read-only/idempotent ops may be replayed
|
|
388
|
+
}
|
|
389
|
+
throw; // financial op: surface the error (recover via sendLastResult / 'G')
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
void HybridEcr17Client::runAckOnly(const std::string& payload, bool safeToRetry) {
|
|
395
|
+
std::lock_guard<std::mutex> txLock(txMutex_); // serialize exchanges on the shared session
|
|
396
|
+
// Send under the app class loader on Android (ArrayBuffer lookup in send()).
|
|
397
|
+
runOnJvmThread([&]() {
|
|
398
|
+
try {
|
|
399
|
+
session_->sendAckOnly(payload);
|
|
400
|
+
} catch (const std::exception&) {
|
|
401
|
+
const auto originalError = std::current_exception();
|
|
402
|
+
const bool autoReconnect = config_.autoReconnect.value_or(false);
|
|
403
|
+
const bool dropped = !transport_ || !transport_->isConnected();
|
|
404
|
+
if (autoReconnect && dropped) {
|
|
405
|
+
try {
|
|
406
|
+
ensureConnected();
|
|
407
|
+
} catch (...) {
|
|
408
|
+
std::rethrow_exception(originalError); // surface the original error
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
if (!shouldRetryAfterReconnect(autoReconnect, dropped, safeToRetry)) {
|
|
412
|
+
throw; // not retryable: surface the original error
|
|
413
|
+
}
|
|
414
|
+
session_->sendAckOnly(payload); // read-only/idempotent op: safe to replay
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
std::shared_ptr<Promise<void>> HybridEcr17Client::connect() {
|
|
420
|
+
// Delegate to ensureConnected so the explicit Connect path emits CONNECTING
|
|
421
|
+
// and then CONNECTED on success (consistent with command auto-connect);
|
|
422
|
+
// returning the raw transport promise would leave listeners stuck on
|
|
423
|
+
// CONNECTING. Runs on a worker thread (ensureConnected blocks until ready).
|
|
424
|
+
return Promise<void>::async([this]() { ensureConnected(); });
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
void HybridEcr17Client::disconnect() {
|
|
428
|
+
if (transport_) {
|
|
429
|
+
transport_->disconnect();
|
|
430
|
+
}
|
|
431
|
+
if (onConnectionStateChange_) onConnectionStateChange_(ConnectionState::DISCONNECTED);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
bool HybridEcr17Client::isConnected() { return transport_ && transport_->isConnected(); }
|
|
435
|
+
|
|
436
|
+
std::shared_ptr<Promise<PosStatusResponse>> HybridEcr17Client::status() {
|
|
437
|
+
return Promise<PosStatusResponse>::async([this]() -> PosStatusResponse {
|
|
438
|
+
ensureConnected();
|
|
439
|
+
auto pkt = runTransaction(Ecr17Protocol::buildStatusMessage(config_.terminalId),
|
|
440
|
+
std::nullopt, /*safeToRetry=*/true);
|
|
441
|
+
return mapStatus(Ecr17Response::parseStatus(pkt.payload));
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
std::shared_ptr<Promise<PaymentResult>> HybridEcr17Client::pay(const PaymentRequest& request) {
|
|
446
|
+
return Promise<PaymentResult>::async([this, request]() -> PaymentResult {
|
|
447
|
+
ensureConnected();
|
|
448
|
+
const bool tok = request.tokenization.has_value();
|
|
449
|
+
auto payload = Ecr17Protocol::buildPaymentMessage(
|
|
450
|
+
config_.terminalId, cashRegisterIdOr(request.cashRegisterId),
|
|
451
|
+
static_cast<int>(request.amountCents), mapPaymentType(request.paymentType),
|
|
452
|
+
request.cardAlreadyPresent.value_or(false), tok, request.receiptText.value_or(""));
|
|
453
|
+
auto pkt = runTransaction(payload, request.tokenization, false);
|
|
454
|
+
return mapPayment(Ecr17Response::parsePayment(pkt.payload));
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
std::shared_ptr<Promise<PaymentResult>> HybridEcr17Client::payExtended(const PaymentRequest& request) {
|
|
459
|
+
return Promise<PaymentResult>::async([this, request]() -> PaymentResult {
|
|
460
|
+
ensureConnected();
|
|
461
|
+
const bool tok = request.tokenization.has_value();
|
|
462
|
+
auto payload = Ecr17Protocol::buildExtendedPaymentMessage(
|
|
463
|
+
config_.terminalId, cashRegisterIdOr(request.cashRegisterId),
|
|
464
|
+
static_cast<int>(request.amountCents), mapPaymentType(request.paymentType),
|
|
465
|
+
request.cardAlreadyPresent.value_or(false), tok, request.receiptText.value_or(""));
|
|
466
|
+
auto pkt = runTransaction(payload, request.tokenization, false);
|
|
467
|
+
return mapPayment(Ecr17Response::parsePayment(pkt.payload));
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
std::shared_ptr<Promise<ReversalResult>> HybridEcr17Client::reverse(const ReversalRequest& request) {
|
|
472
|
+
return Promise<ReversalResult>::async([this, request]() -> ReversalResult {
|
|
473
|
+
ensureConnected();
|
|
474
|
+
auto payload = Ecr17Protocol::buildReversalMessage(
|
|
475
|
+
config_.terminalId, cashRegisterIdOr(request.cashRegisterId),
|
|
476
|
+
request.stan.value_or("000000"));
|
|
477
|
+
auto pkt = runTransaction(payload, std::nullopt, /*safeToRetry=*/false);
|
|
478
|
+
return mapReversal(Ecr17Response::parsePayment(pkt.payload));
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
std::shared_ptr<Promise<PreAuthResult>> HybridEcr17Client::preAuth(const PreAuthRequest& request) {
|
|
483
|
+
return Promise<PreAuthResult>::async([this, request]() -> PreAuthResult {
|
|
484
|
+
ensureConnected();
|
|
485
|
+
const bool tok = request.tokenization.has_value();
|
|
486
|
+
auto payload = Ecr17Protocol::buildPreAuthMessage(
|
|
487
|
+
config_.terminalId, cashRegisterIdOr(request.cashRegisterId),
|
|
488
|
+
static_cast<int>(request.amountCents), mapPaymentType(request.paymentType),
|
|
489
|
+
request.cardAlreadyPresent.value_or(false), tok, request.receiptText.value_or(""));
|
|
490
|
+
auto pkt = runTransaction(payload, request.tokenization, false);
|
|
491
|
+
return mapPreAuth(Ecr17Response::parsePreAuth(pkt.payload));
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
std::shared_ptr<Promise<PreAuthResult>> HybridEcr17Client::incrementalAuth(
|
|
496
|
+
const IncrementalAuthRequest& request) {
|
|
497
|
+
return Promise<PreAuthResult>::async([this, request]() -> PreAuthResult {
|
|
498
|
+
ensureConnected();
|
|
499
|
+
auto payload = Ecr17Protocol::buildIncrementalMessage(
|
|
500
|
+
config_.terminalId, cashRegisterIdOr(request.cashRegisterId),
|
|
501
|
+
static_cast<int>(request.amountCents), request.originalPreAuthCode, false,
|
|
502
|
+
request.receiptText.value_or(""));
|
|
503
|
+
auto pkt = runTransaction(payload, std::nullopt, /*safeToRetry=*/false);
|
|
504
|
+
return mapPreAuth(Ecr17Response::parsePreAuth(pkt.payload));
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
std::shared_ptr<Promise<PaymentResult>> HybridEcr17Client::preAuthClosure(
|
|
509
|
+
const PreAuthClosureRequest& request) {
|
|
510
|
+
return Promise<PaymentResult>::async([this, request]() -> PaymentResult {
|
|
511
|
+
ensureConnected();
|
|
512
|
+
auto payload = Ecr17Protocol::buildPreAuthClosureMessage(
|
|
513
|
+
config_.terminalId, cashRegisterIdOr(request.cashRegisterId),
|
|
514
|
+
static_cast<int>(request.amountCents), request.originalPreAuthCode, false,
|
|
515
|
+
request.receiptText.value_or(""));
|
|
516
|
+
auto pkt = runTransaction(payload, std::nullopt, /*safeToRetry=*/false);
|
|
517
|
+
return mapPayment(Ecr17Response::parsePayment(pkt.payload));
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
std::shared_ptr<Promise<CardVerificationResult>> HybridEcr17Client::verifyCard(
|
|
522
|
+
const CardVerificationRequest& request) {
|
|
523
|
+
return Promise<CardVerificationResult>::async([this, request]() -> CardVerificationResult {
|
|
524
|
+
ensureConnected();
|
|
525
|
+
const bool tok = request.tokenization.has_value();
|
|
526
|
+
auto payload = Ecr17Protocol::buildCardVerificationMessage(
|
|
527
|
+
config_.terminalId, cashRegisterIdOr(request.cashRegisterId),
|
|
528
|
+
mapPaymentType(request.paymentType), tok);
|
|
529
|
+
auto pkt = runTransaction(payload, request.tokenization, false);
|
|
530
|
+
return mapCardVerify(Ecr17Response::parsePayment(pkt.payload));
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
std::shared_ptr<Promise<CloseSessionResult>> HybridEcr17Client::closeSession() {
|
|
535
|
+
return Promise<CloseSessionResult>::async([this]() -> CloseSessionResult {
|
|
536
|
+
ensureConnected();
|
|
537
|
+
auto payload = Ecr17Protocol::buildCloseSessionMessage(config_.terminalId, config_.cashRegisterId);
|
|
538
|
+
auto pkt = runTransaction(payload, std::nullopt, /*safeToRetry=*/false);
|
|
539
|
+
return mapClose(Ecr17Response::parseClose(pkt.payload));
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
std::shared_ptr<Promise<TotalsResult>> HybridEcr17Client::totals() {
|
|
544
|
+
return Promise<TotalsResult>::async([this]() -> TotalsResult {
|
|
545
|
+
ensureConnected();
|
|
546
|
+
auto payload = Ecr17Protocol::buildTotalsMessage(config_.terminalId, config_.cashRegisterId);
|
|
547
|
+
auto pkt = runTransaction(payload, std::nullopt, /*safeToRetry=*/true);
|
|
548
|
+
return mapTotals(Ecr17Response::parseTotals(pkt.payload));
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
std::shared_ptr<Promise<PaymentResult>> HybridEcr17Client::sendLastResult() {
|
|
553
|
+
return Promise<PaymentResult>::async([this]() -> PaymentResult {
|
|
554
|
+
ensureConnected();
|
|
555
|
+
auto payload = Ecr17Protocol::buildSendLastResultMessage(config_.terminalId, config_.cashRegisterId);
|
|
556
|
+
auto pkt = runTransaction(payload, std::nullopt, /*safeToRetry=*/true);
|
|
557
|
+
return mapPayment(Ecr17Response::parsePayment(pkt.payload));
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
std::shared_ptr<Promise<void>> HybridEcr17Client::enableEcrPrinting(bool enabled) {
|
|
562
|
+
return Promise<void>::async([this, enabled]() {
|
|
563
|
+
ensureConnected();
|
|
564
|
+
runAckOnly(Ecr17Protocol::buildEnableEcrPrintMessage(config_.terminalId, enabled),
|
|
565
|
+
/*safeToRetry=*/true);
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
std::shared_ptr<Promise<void>> HybridEcr17Client::reprint(bool toEcr) {
|
|
570
|
+
return Promise<void>::async([this, toEcr]() {
|
|
571
|
+
ensureConnected();
|
|
572
|
+
runAckOnly(Ecr17Protocol::buildReprintMessage(config_.terminalId, toEcr),
|
|
573
|
+
/*safeToRetry=*/false);
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
std::shared_ptr<Promise<VasResult>> HybridEcr17Client::vas(const std::string& xmlRequest) {
|
|
578
|
+
return Promise<VasResult>::async([this, xmlRequest]() -> VasResult {
|
|
579
|
+
ensureConnected();
|
|
580
|
+
auto payload = Ecr17Protocol::buildVasMessage(config_.terminalId, config_.cashRegisterId, xmlRequest);
|
|
581
|
+
auto pkt = runTransaction(payload, std::nullopt, /*safeToRetry=*/false);
|
|
582
|
+
return mapVas(Ecr17Response::parseVas(pkt.payload));
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
void HybridEcr17Client::setOnProgress(const std::function<void(const ProgressEvent&)>& callback) {
|
|
587
|
+
onProgress_ = callback;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
void HybridEcr17Client::setOnReceiptLine(const std::function<void(const ReceiptLine&)>& callback) {
|
|
591
|
+
onReceiptLine_ = callback;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
void HybridEcr17Client::setOnConnectionStateChange(const std::function<void(ConnectionState)>& callback) {
|
|
595
|
+
onConnectionStateChange_ = callback;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
} // namespace margelo::nitro::ecr17
|