@kreiseck/kasseneck-api 0.6.46 → 0.6.49

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