@kreiseck/kasseneck-api 0.7.0 → 0.7.2
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/CHANGELOG.md +32 -0
- package/README.md +87 -15
- package/dist/cjs/client/aufrufe.d.ts +1 -1
- package/dist/cjs/client/aufrufe.js +1 -0
- package/dist/cjs/client/errors.d.ts +8 -4
- package/dist/cjs/client/errors.js +12 -9
- package/dist/cjs/client/receipts.d.ts +22 -0
- package/dist/cjs/client/receipts.js +16 -0
- package/dist/cjs/client/transport.js +5 -4
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.js +4 -2
- package/dist/cjs/models/cancellation.d.ts +28 -0
- package/dist/cjs/models/cancellation.js +31 -1
- package/dist/cjs/models/hobex-receipt.js +9 -2
- package/dist/cjs/models/index.d.ts +1 -1
- package/dist/cjs/models/index.js +3 -1
- package/dist/cjs/models/receipt-summary.d.ts +2 -0
- package/dist/cjs/models/receipt-summary.js +4 -1
- package/dist/cjs/models/receipt.js +8 -1
- package/dist/cjs/models/voucher.js +18 -2
- package/dist/cjs/payments/hobex-hps/connect-client.d.ts +104 -0
- package/dist/cjs/payments/hobex-hps/connect-client.js +159 -0
- package/dist/cjs/payments/hobex-hps/errors.d.ts +122 -0
- package/dist/cjs/payments/hobex-hps/errors.js +158 -0
- package/dist/cjs/payments/hobex-hps/events.d.ts +22 -0
- package/dist/cjs/payments/hobex-hps/events.js +2 -0
- package/dist/cjs/payments/hobex-hps/index.d.ts +27 -0
- package/dist/cjs/payments/hobex-hps/index.js +67 -0
- package/dist/cjs/payments/hobex-hps/outcome.d.ts +46 -0
- package/dist/cjs/payments/hobex-hps/outcome.js +7 -0
- package/dist/cjs/payments/hobex-hps/payments.d.ts +149 -0
- package/dist/cjs/payments/hobex-hps/payments.js +573 -0
- package/dist/cjs/payments/hobex-hps/receipt.d.ts +28 -0
- package/dist/cjs/payments/hobex-hps/receipt.js +60 -0
- package/dist/cjs/payments/hobex-hps/transaction-id.d.ts +65 -0
- package/dist/cjs/payments/hobex-hps/transaction-id.js +91 -0
- package/dist/cjs/payments/hobex-hps/transaction-response.d.ts +175 -0
- package/dist/cjs/payments/hobex-hps/transaction-response.js +266 -0
- package/dist/cjs/payments/hobex.js +9 -6
- package/dist/cjs/payments/index.d.ts +26 -14
- package/dist/cjs/payments/index.js +59 -15
- package/dist/cjs/printing/index.d.ts +1 -1
- package/dist/cjs/printing/index.js +2 -1
- package/dist/cjs/printing/webusb.d.ts +37 -7
- package/dist/cjs/printing/webusb.js +76 -18
- package/dist/cjs/receipt/epos.d.ts +18 -0
- package/dist/cjs/receipt/epos.js +78 -6
- package/dist/cjs/receipt/index.d.ts +1 -1
- package/dist/cjs/receipt/index.js +2 -1
- package/dist/cjs/receipt/layout.js +19 -0
- package/dist/cjs/register/index.d.ts +1 -1
- package/dist/cjs/register/index.js +2 -1
- package/dist/cjs/register/pairing.d.ts +59 -0
- package/dist/cjs/register/pairing.js +51 -2
- package/dist/esm/client/aufrufe.d.ts +1 -1
- package/dist/esm/client/aufrufe.js +1 -0
- package/dist/esm/client/errors.d.ts +8 -4
- package/dist/esm/client/errors.js +12 -9
- package/dist/esm/client/receipts.d.ts +22 -0
- package/dist/esm/client/receipts.js +16 -0
- package/dist/esm/client/transport.js +5 -4
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/models/cancellation.d.ts +28 -0
- package/dist/esm/models/cancellation.js +29 -0
- package/dist/esm/models/hobex-receipt.js +10 -3
- package/dist/esm/models/index.d.ts +1 -1
- package/dist/esm/models/index.js +1 -1
- package/dist/esm/models/receipt-summary.d.ts +2 -0
- package/dist/esm/models/receipt-summary.js +4 -1
- package/dist/esm/models/receipt.js +8 -1
- package/dist/esm/models/voucher.js +18 -2
- package/dist/esm/payments/hobex-hps/connect-client.d.ts +104 -0
- package/dist/esm/payments/hobex-hps/connect-client.js +156 -0
- package/dist/esm/payments/hobex-hps/errors.d.ts +122 -0
- package/dist/esm/payments/hobex-hps/errors.js +149 -0
- package/dist/esm/payments/hobex-hps/events.d.ts +22 -0
- package/dist/esm/payments/hobex-hps/events.js +1 -0
- package/dist/esm/payments/hobex-hps/index.d.ts +27 -0
- package/dist/esm/payments/hobex-hps/index.js +26 -0
- package/dist/esm/payments/hobex-hps/outcome.d.ts +46 -0
- package/dist/esm/payments/hobex-hps/outcome.js +4 -0
- package/dist/esm/payments/hobex-hps/payments.d.ts +149 -0
- package/dist/esm/payments/hobex-hps/payments.js +570 -0
- package/dist/esm/payments/hobex-hps/receipt.d.ts +28 -0
- package/dist/esm/payments/hobex-hps/receipt.js +57 -0
- package/dist/esm/payments/hobex-hps/transaction-id.d.ts +65 -0
- package/dist/esm/payments/hobex-hps/transaction-id.js +86 -0
- package/dist/esm/payments/hobex-hps/transaction-response.d.ts +175 -0
- package/dist/esm/payments/hobex-hps/transaction-response.js +254 -0
- package/dist/esm/payments/hobex.js +9 -6
- package/dist/esm/payments/index.d.ts +26 -14
- package/dist/esm/payments/index.js +26 -14
- package/dist/esm/printing/index.d.ts +1 -1
- package/dist/esm/printing/index.js +1 -1
- package/dist/esm/printing/webusb.d.ts +37 -7
- package/dist/esm/printing/webusb.js +74 -17
- package/dist/esm/receipt/epos.d.ts +18 -0
- package/dist/esm/receipt/epos.js +76 -6
- package/dist/esm/receipt/index.d.ts +1 -1
- package/dist/esm/receipt/index.js +1 -1
- package/dist/esm/receipt/layout.js +19 -0
- package/dist/esm/register/index.d.ts +1 -1
- package/dist/esm/register/index.js +1 -1
- package/dist/esm/register/pairing.d.ts +59 -0
- package/dist/esm/register/pairing.js +50 -2
- package/fixtures/belege/storno-rabatt.json +3 -2
- package/fixtures/belege/storno-teil.json +2 -1
- package/fixtures/belege/storno-voll.json +2 -1
- package/fixtures/erwartet/storno-rabatt.grid32.txt +1 -0
- package/fixtures/erwartet/storno-rabatt.grid48.txt +1 -0
- package/fixtures/erwartet/storno-rabatt.lines.json +6 -0
- package/fixtures/erwartet/storno-teil.grid32.txt +1 -0
- package/fixtures/erwartet/storno-teil.grid48.txt +1 -0
- package/fixtures/erwartet/storno-teil.lines.json +6 -0
- package/fixtures/erwartet/storno-voll.grid32.txt +1 -0
- package/fixtures/erwartet/storno-voll.grid48.txt +1 -0
- package/fixtures/erwartet/storno-voll.lines.json +6 -0
- package/fixtures/hobex-hps-codes.json +83 -0
- package/fixtures/manifest.json +12 -12
- package/fixtures/oberflaeche.json +2 -1
- package/package.json +3 -2
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createHpsPayments = createHpsPayments;
|
|
4
|
+
const errors_js_1 = require("./errors.js");
|
|
5
|
+
const transaction_id_js_1 = require("./transaction-id.js");
|
|
6
|
+
const transaction_response_js_1 = require("./transaction-response.js");
|
|
7
|
+
/** Teiler, mit dem das Abbruchbudget aus [HpsPaymentsOptions.resolveBudgetMs] entsteht. */
|
|
8
|
+
const ABORT_BUDGET_DIVISOR = 6;
|
|
9
|
+
function createHpsPayments(client, target, options = {}) {
|
|
10
|
+
const resolveBudgetMs = options.resolveBudgetMs ?? 90_000;
|
|
11
|
+
const maxBackoffMs = options.maxBackoffMs ?? 10_000;
|
|
12
|
+
const maxTransportFailures = options.maxTransportFailures ?? 3;
|
|
13
|
+
const sleep = options.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
14
|
+
const now = options.now ?? Date.now;
|
|
15
|
+
const observer = options.observer;
|
|
16
|
+
const abortBudgetMs = Math.trunc(resolveBudgetMs / ABORT_BUDGET_DIVISOR);
|
|
17
|
+
function emit(kind, message, transactionId) {
|
|
18
|
+
if (!observer)
|
|
19
|
+
return;
|
|
20
|
+
try {
|
|
21
|
+
observer({ kind, message, transactionId });
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// bewusst still -- das Protokoll darf den Zahlweg nie mitreissen
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Meldet eine Ausnahme, die KEIN erwarteter Connect-Fehler ist -- also
|
|
29
|
+
* einen Fehler im eigenen Auswerten statt einen an Connect oder am
|
|
30
|
+
* Terminal (z. B. eine unlesbare Antwortform).
|
|
31
|
+
*/
|
|
32
|
+
function noteUnexpected(error, transactionId) {
|
|
33
|
+
if (error instanceof errors_js_1.HpsConnectException)
|
|
34
|
+
return;
|
|
35
|
+
if (!observer)
|
|
36
|
+
return;
|
|
37
|
+
try {
|
|
38
|
+
observer({
|
|
39
|
+
kind: 'unexpectedError',
|
|
40
|
+
message: 'Unerwarteter Fehler beim Auswerten der Terminal-Antwort',
|
|
41
|
+
transactionId,
|
|
42
|
+
error,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// bewusst still
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function describe(error) {
|
|
50
|
+
return error instanceof Error ? error.message : String(error);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* `null`, wenn [error] nicht der gemessene "Terminal beschaeftigt"-Fall ist.
|
|
54
|
+
* Gilt AUSDRUECKLICH nur fuer die ERZEUGENDE Anfrage (siehe `errors.ts`),
|
|
55
|
+
* niemals beim Abbruch oder beim Pollen der Statusabfrage.
|
|
56
|
+
*/
|
|
57
|
+
function fromTerminalBusy(error, id, steps) {
|
|
58
|
+
if (!(error instanceof errors_js_1.HpsConnectTerminalError) || !error.isTerminalBusy)
|
|
59
|
+
return null;
|
|
60
|
+
steps.push('Terminal beschaeftigt (HTTP 409) -- die Anfrage wurde nicht angenommen, es ist nichts geschehen');
|
|
61
|
+
emit('resolved', steps[steps.length - 1], id);
|
|
62
|
+
return { outcome: 'declined', transactionId: id, steps: [...steps] };
|
|
63
|
+
}
|
|
64
|
+
/** Verlaufseintrag fuer eine direkte Antwort, die den Ausgang NICHT festschreibt. */
|
|
65
|
+
function offeneAntwort(res) {
|
|
66
|
+
if (res.responseCode === undefined) {
|
|
67
|
+
return 'Antwort ohne Ergebniscode -- Ausgang wird geklaert';
|
|
68
|
+
}
|
|
69
|
+
if ((0, transaction_response_js_1.isTechnicalError)(res)) {
|
|
70
|
+
return `Antwort mit technischem Fehler (${transaction_response_js_1.TECHNICAL_ERROR_CODE}) -- keine Aussage ueber den Vorgang, Ausgang wird geklaert`;
|
|
71
|
+
}
|
|
72
|
+
if ((0, transaction_response_js_1.isUnknownCode)(res)) {
|
|
73
|
+
return `Terminal nennt einen unbekannten Code (${res.responseCode})${klartext(res)} -- Ausgang wird geklaert`;
|
|
74
|
+
}
|
|
75
|
+
return `Antwort ohne Aussage (${res.responseCode}) -- Ausgang wird geklaert`;
|
|
76
|
+
}
|
|
77
|
+
/** Dasselbe fuer eine Statusabfrage waehrend der Klaerung, oder `null`. */
|
|
78
|
+
function statusOhneErgebnis(status) {
|
|
79
|
+
if ((0, transaction_response_js_1.isNoStatement)(status)) {
|
|
80
|
+
return `Status: keine Auskunft (${status.responseCode})`;
|
|
81
|
+
}
|
|
82
|
+
if ((0, transaction_response_js_1.isTechnicalError)(status)) {
|
|
83
|
+
return `Status: technischer Fehler (${transaction_response_js_1.TECHNICAL_ERROR_CODE}) -- keine Aussage ueber den Vorgang`;
|
|
84
|
+
}
|
|
85
|
+
if ((0, transaction_response_js_1.isUnknownCode)(status)) {
|
|
86
|
+
return `Status: unbekannter Code (${status.responseCode})${klartext(status)} -- keine Aussage`;
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Der Klartext des Terminals zu einem UNBEKANNTEN Code, als Zusatz fuer den
|
|
92
|
+
* Nachweis -- oder leer, wenn keiner mitkam. Nur hier, nicht bei den
|
|
93
|
+
* gemessenen Codes: deren Bedeutung ist benannt. Bei einem unbekannten Code
|
|
94
|
+
* ist er dagegen das Einzige, was ein Mensch lesen kann -- "PIN falsch"
|
|
95
|
+
* neben `55` haette am 02.09.2026 gereicht, um nicht "bezahlt" zu buchen.
|
|
96
|
+
*/
|
|
97
|
+
function klartext(res) {
|
|
98
|
+
const text = res.responseText?.trim();
|
|
99
|
+
return text ? ` "${text}"` : '';
|
|
100
|
+
}
|
|
101
|
+
/** Ordnet eine Terminal-Antwort ein. `null`, wenn sie nichts entscheidet. */
|
|
102
|
+
function fromResponse(res, id, steps) {
|
|
103
|
+
if (!(0, transaction_response_js_1.isConclusive)(res))
|
|
104
|
+
return null;
|
|
105
|
+
const approved = res.responseCode === '0';
|
|
106
|
+
steps.push(approved ? 'Terminal: genehmigt' : `Terminal: abgelehnt (${res.responseCode})`);
|
|
107
|
+
emit('resolved', steps[steps.length - 1], id);
|
|
108
|
+
return { outcome: approved ? 'approved' : 'declined', transactionId: id, response: res, steps: [...steps] };
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Ordnet die DIREKTE Antwort auf einen Aufhebungs-Request ein. Fast dasselbe
|
|
112
|
+
* wie [fromResponse] -- diese Antwort betrifft die Aufhebung selbst und
|
|
113
|
+
* traegt deren eigenen `responseCode`. Mit genau einer Ausnahme:
|
|
114
|
+
* [TRANSACTION_CANCELED_CODE] (`9011`).
|
|
115
|
+
*
|
|
116
|
+
* Ueber [fromResponse] wuerde `9011` zu `declined` -- also "die Aufhebung
|
|
117
|
+
* hat nicht gegriffen, es ist weiterhin belastet". Das waere die teure
|
|
118
|
+
* Richtung: es meldet "belastet" fuer einen Vorgang, der aufgehoben ist, und
|
|
119
|
+
* laedt zu einer Rueckerstattung ein, die der Kunde ein zweites Mal bekaeme.
|
|
120
|
+
* Es widerspraeche ausserdem [fromCancelStatus], die aus demselben Code das
|
|
121
|
+
* Gegenteil ableitet.
|
|
122
|
+
*
|
|
123
|
+
* Was `9011` auf dem DIREKTEN Weg genau heisst, ist UNGEMESSEN -- am
|
|
124
|
+
* naechstliegenden "der Vorgang ist (bereits) aufgehoben", aber das ist eine
|
|
125
|
+
* Lesart, keine Messung. Deshalb weder Erfolg noch Ablehnung hier, sondern
|
|
126
|
+
* NICHT SCHLUESSIG: die Klaerung fragt den Zustand der Originalzahlung ab
|
|
127
|
+
* und entscheidet ihn dort mit dem gemessenen Diskriminator, statt zu raten.
|
|
128
|
+
*/
|
|
129
|
+
function fromCancelResponse(res, id, steps) {
|
|
130
|
+
if ((0, transaction_response_js_1.isCanceled)(res)) {
|
|
131
|
+
steps.push(`Aufhebung mit ${transaction_response_js_1.TRANSACTION_CANCELED_CODE} beantwortet -- mehrdeutig, der Zustand der Originalzahlung wird abgefragt`);
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
return fromResponse(res, id, steps);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Ordnet die Statusabfrage einer OFFENEN AUFHEBUNG ein. `null`, wenn sie
|
|
138
|
+
* nichts entscheidet.
|
|
139
|
+
*
|
|
140
|
+
* Die Abfrage laeuft auf die Kennung der ORIGINALZAHLUNG. Ihr
|
|
141
|
+
* `responseCode` beschreibt deshalb den Zustand DIESER Zahlung, nicht den
|
|
142
|
+
* Ausgang der Aufhebung -- er wird hier UEBERSETZT, nicht wie bei
|
|
143
|
+
* [fromResponse] gelesen. Am 26./28.08.2026 gemessen, nachdem eine
|
|
144
|
+
* genehmigte Zahlung per Void aufgehoben wurde:
|
|
145
|
+
*
|
|
146
|
+
* - `9011` "Transaction Canceled" -> die Aufhebung hat gewirkt -> `approved`.
|
|
147
|
+
* - `'0'` -> die Originalzahlung steht unveraendert -> die Aufhebung hat
|
|
148
|
+
* NICHT gewirkt -> `declined`. Keine schlechte Nachricht ueber die
|
|
149
|
+
* Zahlung, sondern ueber die Aufhebung: es ist weiterhin belastet, und
|
|
150
|
+
* die Aufhebung muss wiederholt werden. ABER erst ab der ZWEITEN
|
|
151
|
+
* beantworteten Abfrage, siehe [firstQuery].
|
|
152
|
+
* - `9027` und jeder andere oder fehlende Code -> keine Auskunft, weiter
|
|
153
|
+
* klaeren; am Ende `unresolved`, niemals ein geratenes Ergebnis.
|
|
154
|
+
*
|
|
155
|
+
* ## Warum `'0'` eine Karenz braucht
|
|
156
|
+
*
|
|
157
|
+
* Die Klaerung startet unmittelbar, nachdem der Void-Request abgerissen
|
|
158
|
+
* ist, und ihre erste Abfrage laeuft ohne Pause. Anders als bei [pay] liegt
|
|
159
|
+
* kein Abbruch-Roundtrip dazwischen, der Zeit verstreichen liesse. Genau in
|
|
160
|
+
* diesem Fenster kann der Void beim Terminal noch unterwegs sein und die
|
|
161
|
+
* Abfrage trotzdem schon `'0'` melden.
|
|
162
|
+
*
|
|
163
|
+
* Ein voreiliges "hat nicht gegriffen" ist NICHT harmlos: nach dem
|
|
164
|
+
* Tagesabschluss ist die Folgehandlung eine RUECKERSTATTUNG -- und dann
|
|
165
|
+
* bekommt der Kunde sein Geld zweimal. Deshalb entscheidet `'0'` erst ab der
|
|
166
|
+
* zweiten beantworteten Abfrage. Ein tatsaechlich nicht gelandeter Void
|
|
167
|
+
* antwortet eine Sekunde spaeter wieder `'0'`; der Preis ist diese eine
|
|
168
|
+
* Sekunde. Reicht das Budget nur fuer eine einzige Abfrage, endet die
|
|
169
|
+
* Klaerung bei `unresolved` -- wir sagen dann, dass wir es nicht wissen,
|
|
170
|
+
* statt es zu raten.
|
|
171
|
+
*
|
|
172
|
+
* [state] `=== 'VOID'` gilt zusaetzlich als Beleg, aber niemals als
|
|
173
|
+
* notwendige Bedingung -- auf der gemessenen Firmware ist `state` in jeder
|
|
174
|
+
* bisher gesehenen Antwort `undefined`. Bleibt nur mitgelesen, weil ein
|
|
175
|
+
* ausdrueckliches `'VOID'` -- wo eine Firmware es denn liefert -- eine
|
|
176
|
+
* unmissverstaendliche positive Aussage ist, die kein falsches `approved`
|
|
177
|
+
* erzeugen kann.
|
|
178
|
+
*
|
|
179
|
+
* [firstQuery] ist `true`, wenn dies die erste BEANTWORTETE Statusabfrage
|
|
180
|
+
* dieser Klaerung ist. Gescheiterte Abfragen zaehlen nicht mit: sie lassen
|
|
181
|
+
* zwar Zeit verstreichen, liefern aber keine Auskunft, an der sich ein
|
|
182
|
+
* `'0'` bestaetigen liesse.
|
|
183
|
+
*/
|
|
184
|
+
function fromCancelStatus(status, id, steps, firstQuery) {
|
|
185
|
+
const voided = (0, transaction_response_js_1.isCanceled)(status) || status.state?.trim().toUpperCase() === 'VOID';
|
|
186
|
+
if (voided) {
|
|
187
|
+
steps.push(`Terminal: Aufhebung bestaetigt (${status.responseCode ?? status.state})`);
|
|
188
|
+
emit('resolved', steps[steps.length - 1], id);
|
|
189
|
+
return { outcome: 'approved', transactionId: id, response: status, steps: [...steps] };
|
|
190
|
+
}
|
|
191
|
+
if ((0, transaction_response_js_1.isApproved)(status)) {
|
|
192
|
+
if (firstQuery) {
|
|
193
|
+
steps.push('Terminal: Originalzahlung noch unveraendert (0) -- die Aufhebung koennte noch unterwegs sein, wird erneut abgefragt');
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
steps.push('Terminal: Originalzahlung steht unveraendert (0) -- die Aufhebung hat nicht gegriffen');
|
|
197
|
+
emit('resolved', steps[steps.length - 1], id);
|
|
198
|
+
return { outcome: 'declined', transactionId: id, response: status, steps: [...steps] };
|
|
199
|
+
}
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* [letzteAntwort] ist die letzte Antwort, die das Terminal in dieser
|
|
204
|
+
* Klaerung gab -- als `lastResponse` fuer Anzeige und Katalog,
|
|
205
|
+
* ausdruecklich NICHT als `response`.
|
|
206
|
+
*/
|
|
207
|
+
function open(id, steps, letzteAntwort) {
|
|
208
|
+
emit('resolved', steps[steps.length - 1], id);
|
|
209
|
+
return {
|
|
210
|
+
outcome: 'unresolved',
|
|
211
|
+
transactionId: id,
|
|
212
|
+
...(letzteAntwort ? { lastResponse: letzteAntwort } : {}),
|
|
213
|
+
steps: [...steps],
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Fuehrt [call] aus, aber hoechstens so lange, wie vom [resolveBudgetMs]
|
|
218
|
+
* uebrig ist -- sonst waere die Klaerung nicht durch das Budget begrenzt,
|
|
219
|
+
* sondern durch das (deutlich groessere) Zeitlimit des Connect-Clients je
|
|
220
|
+
* einzelnem Aufruf. [cap] deckelt zusaetzlich einen einzelnen Schritt
|
|
221
|
+
* (siehe [abortBudgetMs]).
|
|
222
|
+
*/
|
|
223
|
+
function withinBudget(elapsedMs, call, cap) {
|
|
224
|
+
let left = resolveBudgetMs - elapsedMs();
|
|
225
|
+
if (cap !== undefined && cap < left)
|
|
226
|
+
left = cap;
|
|
227
|
+
if (left <= 0) {
|
|
228
|
+
return Promise.reject(new errors_js_1.HpsClarifyTimeoutError(resolveBudgetMs));
|
|
229
|
+
}
|
|
230
|
+
return withTimeout(call(), left);
|
|
231
|
+
}
|
|
232
|
+
function withTimeout(promise, ms) {
|
|
233
|
+
return new Promise((resolve, reject) => {
|
|
234
|
+
const timer = setTimeout(() => reject(new errors_js_1.HpsClarifyTimeoutError(ms)), ms);
|
|
235
|
+
promise.then((v) => {
|
|
236
|
+
clearTimeout(timer);
|
|
237
|
+
resolve(v);
|
|
238
|
+
}, (e) => {
|
|
239
|
+
clearTimeout(timer);
|
|
240
|
+
reject(e);
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
function nextWait(current) {
|
|
245
|
+
if (current === 0)
|
|
246
|
+
return 1000;
|
|
247
|
+
const doubled = current * 2;
|
|
248
|
+
return doubled > maxBackoffMs ? maxBackoffMs : doubled;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Versucht den Abbruch GENAU EINMAL. Liefert ein Ergebnis nur im einen
|
|
252
|
+
* beweisbaren Fall: Connect quittiert den Abbruch mit `responseCode === '0'`.
|
|
253
|
+
* In jedem anderen Fall `null` -- der Aufrufer pollt dann weiter.
|
|
254
|
+
*/
|
|
255
|
+
async function tryAbort(id, steps, elapsedMs) {
|
|
256
|
+
let res;
|
|
257
|
+
try {
|
|
258
|
+
res = await withinBudget(elapsedMs, () => client.abort({ ...target, transactionId: id }), abortBudgetMs);
|
|
259
|
+
}
|
|
260
|
+
catch (e) {
|
|
261
|
+
steps.push(`Abbruch nicht bestaetigt (${describe(e)}) -- ob er wirkte, ist offen, Ausgang wird abgefragt`);
|
|
262
|
+
noteUnexpected(e, id);
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
if (res.responseCode === undefined) {
|
|
266
|
+
steps.push('Abbruch ohne Ergebniscode quittiert -- das beweist nichts, Ausgang wird abgefragt');
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
if (res.responseCode !== '0') {
|
|
270
|
+
steps.push((0, transaction_response_js_1.isNotAbortable)(res)
|
|
271
|
+
? `Abbruch abgelehnt (${transaction_response_js_1.NOT_ABORTABLE_CODE}) -- der Vorgang ist nicht mehr abbrechbar, Ausgang wird abgefragt`
|
|
272
|
+
: `Abbruch abgelehnt (${res.responseCode}) -- Grund unbekannt, Ausgang wird abgefragt`);
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
steps.push('Abbruch bestaetigt -- der Vorgang war noch abbrechbar, es ist nichts belastet');
|
|
276
|
+
emit('resolved', steps[steps.length - 1], id);
|
|
277
|
+
return { outcome: 'declined', transactionId: id, response: res, steps: [...steps] };
|
|
278
|
+
}
|
|
279
|
+
/** Klaert einen offenen Ausgang: erst abbrechen, dann abfragen -- bis das Terminal etwas sagt oder das Budget aufgebraucht ist. */
|
|
280
|
+
/**
|
|
281
|
+
* `antwortMitCode` heisst: das Terminal hat auf die ERZEUGENDE Anfrage eine
|
|
282
|
+
* Antwort MIT Ergebniscode geliefert, deren Bedeutung wir nur nicht kennen.
|
|
283
|
+
* Dann ist der Vorgang am Geraet abgeschlossen -- und erst dadurch bekommt
|
|
284
|
+
* [NO_STATEMENT_CODE] (`9027`) beim Pollen einen Aussagewert, den es sonst
|
|
285
|
+
* nicht hat. Siehe [ausGeschlossenerAntwort].
|
|
286
|
+
*/
|
|
287
|
+
async function resolve(id, steps, antwortMitCode = false, letzteAntwort) {
|
|
288
|
+
emit('resolving', 'Ausgang offen, Klaerung laeuft', id);
|
|
289
|
+
const start = now();
|
|
290
|
+
const elapsedMs = () => now() - start;
|
|
291
|
+
const aborted = await tryAbort(id, steps, elapsedMs);
|
|
292
|
+
if (aborted)
|
|
293
|
+
return aborted;
|
|
294
|
+
let wait = 0;
|
|
295
|
+
let transportFailures = 0;
|
|
296
|
+
let ohneAuskunft = 0;
|
|
297
|
+
while (elapsedMs() < resolveBudgetMs) {
|
|
298
|
+
if (wait > 0) {
|
|
299
|
+
const left = resolveBudgetMs - elapsedMs();
|
|
300
|
+
await sleep(Math.min(wait, left));
|
|
301
|
+
if (elapsedMs() >= resolveBudgetMs)
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
let status;
|
|
305
|
+
try {
|
|
306
|
+
status = await withinBudget(elapsedMs, () => client.status({ ...target, transactionId: id }));
|
|
307
|
+
transportFailures = 0;
|
|
308
|
+
letzteAntwort = status;
|
|
309
|
+
}
|
|
310
|
+
catch (e) {
|
|
311
|
+
transportFailures += 1;
|
|
312
|
+
steps.push(`Statusabfrage gescheitert (${transportFailures}): ${describe(e)}`);
|
|
313
|
+
noteUnexpected(e, id);
|
|
314
|
+
if (transportFailures >= maxTransportFailures) {
|
|
315
|
+
steps.push('Terminal antwortet nicht -- Ausgang bleibt offen');
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
wait = nextWait(wait);
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
const settled = fromResponse(status, id, steps);
|
|
322
|
+
if (settled)
|
|
323
|
+
return settled;
|
|
324
|
+
if ((0, transaction_response_js_1.isNoStatement)(status)) {
|
|
325
|
+
ohneAuskunft += 1;
|
|
326
|
+
const geklaert = ausGeschlossenerAntwort(id, steps, status, antwortMitCode, ohneAuskunft);
|
|
327
|
+
if (geklaert)
|
|
328
|
+
return geklaert;
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
ohneAuskunft = 0;
|
|
332
|
+
}
|
|
333
|
+
steps.push(statusOhneErgebnis(status) ?? 'Status: noch kein Ergebniscode');
|
|
334
|
+
wait = nextWait(wait);
|
|
335
|
+
}
|
|
336
|
+
steps.push('Ausgang bleibt offen');
|
|
337
|
+
return open(id, steps, letzteAntwort);
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Liest `9027` beim Pollen als "nicht genehmigt" -- aber NUR, wenn das
|
|
341
|
+
* Terminal die erzeugende Anfrage bereits mit einem Ergebniscode beantwortet
|
|
342
|
+
* hat, und erst ab der ZWEITEN Abfrage in Folge.
|
|
343
|
+
*
|
|
344
|
+
* **Warum das ueberhaupt geht.** [NO_STATEMENT_CODE] ist sonst eine reine
|
|
345
|
+
* Nicht-Aussage: dieselbe `9027` steht fuer einen laufenden, einen
|
|
346
|
+
* abgebrochenen und einen nie gesehenen Vorgang. Hat das Terminal aber eine
|
|
347
|
+
* Antwort MIT Code geliefert, faellt "laeuft noch" weg -- der Vorgang ist
|
|
348
|
+
* dort beendet -- und "nie gesehen" ebenso, denn zu genau dieser Kennung
|
|
349
|
+
* wurde uns gerade geantwortet. Uebrig bleibt "beendet und nicht genehmigt".
|
|
350
|
+
*
|
|
351
|
+
* **Gegenprobe** (28.08.2026, TID 3600335, jeweils nach abgeschlossenem
|
|
352
|
+
* Vorgang): genehmigt (Beleg 408811) -> Statusabfrage `0`, dreimal
|
|
353
|
+
* wiederholt; abgelehnt mit `100003` -> `9027`, zweimal; abgelehnt mit
|
|
354
|
+
* `9003` -> `9027`. Eine genehmigte Zahlung antwortet also nicht `9027`.
|
|
355
|
+
*
|
|
356
|
+
* **Warum erst ab der zweiten Abfrage.** Die erste laeuft unmittelbar nach
|
|
357
|
+
* der Antwort -- das Fenster, in dem der Datensatz am Terminal noch nicht
|
|
358
|
+
* stehen koennte. Waere er es nicht und wir lesen `9027` als "abgelehnt",
|
|
359
|
+
* entstuende die Doppelbelastung vom 24.08.2026 an einer neuen Stelle.
|
|
360
|
+
* Dieselbe Absicherung traegt bereits [fromCancelStatus].
|
|
361
|
+
*
|
|
362
|
+
* `undefined` heisst: nicht entschieden, weiter pollen.
|
|
363
|
+
*/
|
|
364
|
+
function ausGeschlossenerAntwort(id, steps, status, antwortMitCode, ohneAuskunft) {
|
|
365
|
+
if (!antwortMitCode)
|
|
366
|
+
return undefined;
|
|
367
|
+
if (ohneAuskunft < 2)
|
|
368
|
+
return undefined;
|
|
369
|
+
steps.push(`Statusabfrage zweimal ohne Auskunft (${status.responseCode}), obwohl das `
|
|
370
|
+
+ 'Terminal den Vorgang bereits beantwortet hatte -- er ist beendet und '
|
|
371
|
+
+ 'nicht genehmigt, es ist nichts belastet');
|
|
372
|
+
emit('resolved', steps[steps.length - 1], id);
|
|
373
|
+
return {
|
|
374
|
+
outcome: 'declined',
|
|
375
|
+
transactionId: id,
|
|
376
|
+
response: status,
|
|
377
|
+
steps: [...steps],
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Klaert eine offene Aufhebung -- eigene Fassung statt [resolve], weil die
|
|
382
|
+
* Kennung hier die der URSPRUENGLICHEN Zahlung ist.
|
|
383
|
+
*
|
|
384
|
+
* Zwei Unterschiede zu [resolve]:
|
|
385
|
+
*
|
|
386
|
+
* 1. Die Antwort der Statusabfrage wird ueber [fromCancelStatus]
|
|
387
|
+
* eingeordnet, NICHT ueber [fromResponse] -- der `responseCode` der
|
|
388
|
+
* Originalkennung bedeutet hier etwas anderes als bei [pay]/[refund].
|
|
389
|
+
* 2. KEIN [tryAbort]-Versuch. Der Abbruch greift nur, solange ein Vorgang
|
|
390
|
+
* noch abbrechbar ist; die Originalzahlung, deren Kennung hier vorliegt,
|
|
391
|
+
* ist laengst abgeschlossen und antwortet gemessen mit `100010`. Ein
|
|
392
|
+
* Abbruchversuch darauf waere sinnlos und koennte hoechstens fehlleiten.
|
|
393
|
+
*
|
|
394
|
+
* Budget, Backoff und Transportfehler-Deckelung sind unveraendert aus
|
|
395
|
+
* [resolve] uebernommen.
|
|
396
|
+
*/
|
|
397
|
+
async function resolveCancel(id, steps, letzteAntwort) {
|
|
398
|
+
emit('resolving', 'Ausgang offen, Klaerung laeuft', id);
|
|
399
|
+
const start = now();
|
|
400
|
+
const elapsedMs = () => now() - start;
|
|
401
|
+
let wait = 0;
|
|
402
|
+
let transportFailures = 0;
|
|
403
|
+
// Zaehlt nur BEANTWORTETE Statusabfragen -- Grundlage der Karenz fuer den
|
|
404
|
+
// `'0'`-Fall, siehe [fromCancelStatus].
|
|
405
|
+
let answeredQueries = 0;
|
|
406
|
+
while (elapsedMs() < resolveBudgetMs) {
|
|
407
|
+
if (wait > 0) {
|
|
408
|
+
const left = resolveBudgetMs - elapsedMs();
|
|
409
|
+
await sleep(Math.min(wait, left));
|
|
410
|
+
if (elapsedMs() >= resolveBudgetMs)
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
let status;
|
|
414
|
+
try {
|
|
415
|
+
status = await withinBudget(elapsedMs, () => client.status({ ...target, transactionId: id }));
|
|
416
|
+
transportFailures = 0;
|
|
417
|
+
answeredQueries += 1;
|
|
418
|
+
letzteAntwort = status;
|
|
419
|
+
}
|
|
420
|
+
catch (e) {
|
|
421
|
+
transportFailures += 1;
|
|
422
|
+
steps.push(`Statusabfrage gescheitert (${transportFailures}): ${describe(e)}`);
|
|
423
|
+
noteUnexpected(e, id);
|
|
424
|
+
if (transportFailures >= maxTransportFailures) {
|
|
425
|
+
steps.push('Terminal antwortet nicht -- Ausgang bleibt offen');
|
|
426
|
+
break;
|
|
427
|
+
}
|
|
428
|
+
wait = nextWait(wait);
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
const settled = fromCancelStatus(status, id, steps, answeredQueries === 1);
|
|
432
|
+
if (settled)
|
|
433
|
+
return settled;
|
|
434
|
+
// Der `'0'`-Karenzfall hat seinen eigenen, aussagekraeftigeren Eintrag
|
|
435
|
+
// schon in [fromCancelStatus] gesetzt.
|
|
436
|
+
if (!(0, transaction_response_js_1.isApproved)(status)) {
|
|
437
|
+
steps.push(statusOhneErgebnis(status) ?? 'Status: Aufhebung noch nicht bestaetigt');
|
|
438
|
+
}
|
|
439
|
+
wait = nextWait(wait);
|
|
440
|
+
}
|
|
441
|
+
steps.push('Ausgang bleibt offen');
|
|
442
|
+
return open(id, steps, letzteAntwort);
|
|
443
|
+
}
|
|
444
|
+
async function pay(paymentOptions) {
|
|
445
|
+
const id = paymentOptions.transactionId ?? (0, transaction_id_js_1.newHpsTransactionId)();
|
|
446
|
+
const steps = [];
|
|
447
|
+
// Das try liegt bewusst ENG um den Netzweg: was danach kommt, ist unser
|
|
448
|
+
// eigenes Auswerten und soll nicht stillschweigend als "Terminal hat
|
|
449
|
+
// nicht geantwortet" durchgehen.
|
|
450
|
+
let res;
|
|
451
|
+
try {
|
|
452
|
+
res = await client.payment({
|
|
453
|
+
...target,
|
|
454
|
+
transactionId: id,
|
|
455
|
+
amountCents: paymentOptions.amountCents,
|
|
456
|
+
tipCents: paymentOptions.tipCents,
|
|
457
|
+
reference: paymentOptions.reference,
|
|
458
|
+
currency: paymentOptions.currency,
|
|
459
|
+
language: paymentOptions.language,
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
catch (e) {
|
|
463
|
+
if (e instanceof errors_js_1.HpsPreflightError) {
|
|
464
|
+
// Beweisbar nichts gesendet -- kein Ausgang, sondern ein Aufruffehler.
|
|
465
|
+
// Wird unveraendert weitergeworfen, damit er sichtbar bleibt statt als
|
|
466
|
+
// offener Ausgang zu enden (Zwilling: Dart's `ArgumentError`-Zweig).
|
|
467
|
+
throw e;
|
|
468
|
+
}
|
|
469
|
+
const busy = fromTerminalBusy(e, id, steps);
|
|
470
|
+
if (busy)
|
|
471
|
+
return busy;
|
|
472
|
+
steps.push(`Zahlung abgebrochen: ${describe(e)}`);
|
|
473
|
+
noteUnexpected(e, id);
|
|
474
|
+
}
|
|
475
|
+
if (res) {
|
|
476
|
+
const settled = fromResponse(res, id, steps);
|
|
477
|
+
if (settled)
|
|
478
|
+
return settled;
|
|
479
|
+
steps.push(offeneAntwort(res));
|
|
480
|
+
}
|
|
481
|
+
return resolve(id, steps, res?.responseCode !== undefined, res);
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Gutschrift mit geklaertem Ausgang -- EXAKT derselbe Klaerweg wie [pay]
|
|
485
|
+
* (Abbruch eingeschlossen), siehe Klassendoku oben. [transactionId] ist die
|
|
486
|
+
* Kennung des NEUEN Vorgangs (der Gutschrift selbst), nicht die der
|
|
487
|
+
* Zahlung, auf die sie sich ueber [HpsRefundOptions.originalTransactionId]
|
|
488
|
+
* referenziert.
|
|
489
|
+
*/
|
|
490
|
+
async function refund(refundOptions) {
|
|
491
|
+
const id = refundOptions.transactionId ?? (0, transaction_id_js_1.newHpsTransactionId)();
|
|
492
|
+
const steps = [];
|
|
493
|
+
// Das try liegt bewusst ENG um den Netzweg -- siehe Begruendung in [pay].
|
|
494
|
+
let res;
|
|
495
|
+
try {
|
|
496
|
+
res = await client.refund({
|
|
497
|
+
...target,
|
|
498
|
+
transactionId: id,
|
|
499
|
+
originalTransactionId: refundOptions.originalTransactionId,
|
|
500
|
+
amountCents: refundOptions.amountCents,
|
|
501
|
+
reference: refundOptions.reference,
|
|
502
|
+
currency: refundOptions.currency,
|
|
503
|
+
language: refundOptions.language,
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
catch (e) {
|
|
507
|
+
if (e instanceof errors_js_1.HpsPreflightError) {
|
|
508
|
+
throw e;
|
|
509
|
+
}
|
|
510
|
+
const busy = fromTerminalBusy(e, id, steps);
|
|
511
|
+
if (busy)
|
|
512
|
+
return busy;
|
|
513
|
+
steps.push(`Gutschrift abgebrochen: ${describe(e)}`);
|
|
514
|
+
noteUnexpected(e, id);
|
|
515
|
+
}
|
|
516
|
+
if (res) {
|
|
517
|
+
const settled = fromResponse(res, id, steps);
|
|
518
|
+
if (settled)
|
|
519
|
+
return settled;
|
|
520
|
+
steps.push(offeneAntwort(res));
|
|
521
|
+
}
|
|
522
|
+
// Dieselbe Klaerfunktion wie [pay]: die Kennung ist die des NEUEN
|
|
523
|
+
// Vorgangs, eine Statusabfrage darauf liefert also genau dessen Ausgang,
|
|
524
|
+
// und der Abbruch ist derselbe Diskriminator wie bei einer Zahlung.
|
|
525
|
+
return resolve(id, steps, res?.responseCode !== undefined, res);
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Aufhebung (Storno/Void) einer bestehenden Zahlung mit geklaertem Ausgang.
|
|
529
|
+
*
|
|
530
|
+
* [transactionId] ist die vom TERMINAL vergebene Kennung der
|
|
531
|
+
* URSPRUENGLICHEN Zahlung -- nicht die eines neuen Vorgangs. Der direkte
|
|
532
|
+
* Antwortweg wird trotzdem ueber [fromCancelResponse] eingeordnet: die
|
|
533
|
+
* Direktantwort auf einen Aufhebungs-Request traegt einen eigenen
|
|
534
|
+
* `responseCode` fuer die Aufhebung selbst. Erst wenn dieser direkte Weg
|
|
535
|
+
* abbricht und nachgefragt werden muss, aendert sich die Frage -- siehe
|
|
536
|
+
* [resolveCancel].
|
|
537
|
+
*/
|
|
538
|
+
async function cancel(cancelOptions) {
|
|
539
|
+
const id = cancelOptions.transactionId;
|
|
540
|
+
const steps = [];
|
|
541
|
+
let res;
|
|
542
|
+
try {
|
|
543
|
+
res = await client.cancel({
|
|
544
|
+
...target,
|
|
545
|
+
transactionId: id,
|
|
546
|
+
amountCents: cancelOptions.amountCents,
|
|
547
|
+
currency: cancelOptions.currency,
|
|
548
|
+
language: cancelOptions.language,
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
catch (e) {
|
|
552
|
+
if (e instanceof errors_js_1.HpsPreflightError) {
|
|
553
|
+
throw e;
|
|
554
|
+
}
|
|
555
|
+
const busy = fromTerminalBusy(e, id, steps);
|
|
556
|
+
if (busy)
|
|
557
|
+
return busy;
|
|
558
|
+
steps.push(`Aufhebung abgebrochen: ${describe(e)}`);
|
|
559
|
+
noteUnexpected(e, id);
|
|
560
|
+
}
|
|
561
|
+
if (res) {
|
|
562
|
+
const settled = fromCancelResponse(res, id, steps);
|
|
563
|
+
if (settled)
|
|
564
|
+
return settled;
|
|
565
|
+
// Kein Sammel-Eintrag fuer 9011: [fromCancelResponse] hat dafuer
|
|
566
|
+
// bereits den zutreffenden Eintrag gesetzt.
|
|
567
|
+
if (!(0, transaction_response_js_1.isCanceled)(res))
|
|
568
|
+
steps.push(offeneAntwort(res));
|
|
569
|
+
}
|
|
570
|
+
return resolveCancel(id, steps, res);
|
|
571
|
+
}
|
|
572
|
+
return { pay, refund, cancel };
|
|
573
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { HobexReceipt } from '../../models/hobex-receipt.js';
|
|
2
|
+
import type { HpsTransactionResponse } from './transaction-response.js';
|
|
3
|
+
/**
|
|
4
|
+
* Macht aus der Terminal-Antwort den Kasseneck-Beleg -- Zwilling von
|
|
5
|
+
* `HobexReceipt.fromHps` (kasseneck_api, `lib/models/hobex_receipt.dart`).
|
|
6
|
+
*
|
|
7
|
+
* Ohne diese Bruecke endet eine gelungene HPS-Zahlung im Nichts: der
|
|
8
|
+
* Buchungsweg erwartet einen Beleg (`cardPaymentData`, siehe
|
|
9
|
+
* [hobexReceiptToCardPaymentData]), und die Antwort des Terminals ist keiner.
|
|
10
|
+
*
|
|
11
|
+
* Drei Eigenheiten, alle am 28.08.2026 am Terminal 3600335 gemessen und
|
|
12
|
+
* nicht aus der Doku uebernommen:
|
|
13
|
+
*
|
|
14
|
+
* 1. **`cvm` kommt als ZAHL** (`3`), nicht als Text. Ungewandelt scheitert
|
|
15
|
+
* [hobexReceiptNeedsSignature] (`=== '1'`) stumm an einer `1` -- und eine
|
|
16
|
+
* verlangte Unterschrift bliebe ungefragt. Deshalb ueber [text].
|
|
17
|
+
* 2. **Nicht gefuehrte Felder kommen als `null`** (gemessen: `currency`,
|
|
18
|
+
* `reference`, `source`, `state`). Sie werden zu LEEREM Text, nie zu
|
|
19
|
+
* `"null"` -- sonst stuende das Wort auf dem Beleg.
|
|
20
|
+
* 3. `cvm` wird aus [HpsTransactionResponse.raw] gelesen und nicht aus einem
|
|
21
|
+
* eigenen Feld -- genauso wie im Dart-Vorbild (`res.raw['cvm']`). Der
|
|
22
|
+
* Rohsatz ist die Quelle, damit hier nichts eigenes danebenlaeuft.
|
|
23
|
+
*
|
|
24
|
+
* Der Betrag geht ueber [euroToCents], die eine gehaertete Stelle des Pakets:
|
|
25
|
+
* die Terminal-Antwort ist eine fremde Antwort, und ein nicht-endlicher Wert
|
|
26
|
+
* ergaebe sonst lautlos `NaN`.
|
|
27
|
+
*/
|
|
28
|
+
export declare function hobexReceiptFromHps(res: HpsTransactionResponse): HobexReceipt;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.hobexReceiptFromHps = hobexReceiptFromHps;
|
|
4
|
+
const index_js_1 = require("../../enums/index.js");
|
|
5
|
+
const money_js_1 = require("../../money.js");
|
|
6
|
+
/**
|
|
7
|
+
* Macht aus der Terminal-Antwort den Kasseneck-Beleg -- Zwilling von
|
|
8
|
+
* `HobexReceipt.fromHps` (kasseneck_api, `lib/models/hobex_receipt.dart`).
|
|
9
|
+
*
|
|
10
|
+
* Ohne diese Bruecke endet eine gelungene HPS-Zahlung im Nichts: der
|
|
11
|
+
* Buchungsweg erwartet einen Beleg (`cardPaymentData`, siehe
|
|
12
|
+
* [hobexReceiptToCardPaymentData]), und die Antwort des Terminals ist keiner.
|
|
13
|
+
*
|
|
14
|
+
* Drei Eigenheiten, alle am 28.08.2026 am Terminal 3600335 gemessen und
|
|
15
|
+
* nicht aus der Doku uebernommen:
|
|
16
|
+
*
|
|
17
|
+
* 1. **`cvm` kommt als ZAHL** (`3`), nicht als Text. Ungewandelt scheitert
|
|
18
|
+
* [hobexReceiptNeedsSignature] (`=== '1'`) stumm an einer `1` -- und eine
|
|
19
|
+
* verlangte Unterschrift bliebe ungefragt. Deshalb ueber [text].
|
|
20
|
+
* 2. **Nicht gefuehrte Felder kommen als `null`** (gemessen: `currency`,
|
|
21
|
+
* `reference`, `source`, `state`). Sie werden zu LEEREM Text, nie zu
|
|
22
|
+
* `"null"` -- sonst stuende das Wort auf dem Beleg.
|
|
23
|
+
* 3. `cvm` wird aus [HpsTransactionResponse.raw] gelesen und nicht aus einem
|
|
24
|
+
* eigenen Feld -- genauso wie im Dart-Vorbild (`res.raw['cvm']`). Der
|
|
25
|
+
* Rohsatz ist die Quelle, damit hier nichts eigenes danebenlaeuft.
|
|
26
|
+
*
|
|
27
|
+
* Der Betrag geht ueber [euroToCents], die eine gehaertete Stelle des Pakets:
|
|
28
|
+
* die Terminal-Antwort ist eine fremde Antwort, und ein nicht-endlicher Wert
|
|
29
|
+
* ergaebe sonst lautlos `NaN`.
|
|
30
|
+
*/
|
|
31
|
+
function hobexReceiptFromHps(res) {
|
|
32
|
+
return {
|
|
33
|
+
transactionId: text(res.transactionId),
|
|
34
|
+
tid: text(res.tid),
|
|
35
|
+
receipt: text(res.receipt),
|
|
36
|
+
approvalCode: text(res.approvalCode),
|
|
37
|
+
reference: res.reference,
|
|
38
|
+
// Bruchteilssekunden samt Zeitzonenversatz kappen und "T" durch ein
|
|
39
|
+
// Leerzeichen ersetzen -- Wort fuer Wort wie im Dart-Vorbild.
|
|
40
|
+
transactionDate: text(res.transactionDate).split('.')[0].split('T').join(' '),
|
|
41
|
+
cardNumber: text(res.cardNumber),
|
|
42
|
+
cardExpiry: text(res.cardExpiry),
|
|
43
|
+
brand: text(res.brand),
|
|
44
|
+
cardIssuer: text(res.cardIssuer),
|
|
45
|
+
responseCode: text(res.responseCode),
|
|
46
|
+
transactionType: text(res.transactionType),
|
|
47
|
+
currency: text(res.currency),
|
|
48
|
+
amountCents: (0, money_js_1.euroToCents)(res.amount ?? 0),
|
|
49
|
+
tipCents: (0, money_js_1.euroToCents)(res.tip ?? 0),
|
|
50
|
+
cvm: text(res.raw.cvm),
|
|
51
|
+
// Der Provider ergibt sich aus dem Weg, nicht aus dem JSON: was hier
|
|
52
|
+
// ankommt, kam ueber HPS. Genau daran haengt, ob
|
|
53
|
+
// [hobexReceiptToCardPaymentData] die HPS-Zusatzfelder mitgibt.
|
|
54
|
+
creditCardProvider: index_js_1.CreditCardProvider.hobexHps,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** `null`/`undefined` werden zu leerem Text, alles andere wortgetreu. */
|
|
58
|
+
function text(wert) {
|
|
59
|
+
return wert === null || wert === undefined ? '' : String(wert);
|
|
60
|
+
}
|