@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.
Files changed (122) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +87 -15
  3. package/dist/cjs/client/aufrufe.d.ts +1 -1
  4. package/dist/cjs/client/aufrufe.js +1 -0
  5. package/dist/cjs/client/errors.d.ts +8 -4
  6. package/dist/cjs/client/errors.js +12 -9
  7. package/dist/cjs/client/receipts.d.ts +22 -0
  8. package/dist/cjs/client/receipts.js +16 -0
  9. package/dist/cjs/client/transport.js +5 -4
  10. package/dist/cjs/index.d.ts +1 -1
  11. package/dist/cjs/index.js +4 -2
  12. package/dist/cjs/models/cancellation.d.ts +28 -0
  13. package/dist/cjs/models/cancellation.js +31 -1
  14. package/dist/cjs/models/hobex-receipt.js +9 -2
  15. package/dist/cjs/models/index.d.ts +1 -1
  16. package/dist/cjs/models/index.js +3 -1
  17. package/dist/cjs/models/receipt-summary.d.ts +2 -0
  18. package/dist/cjs/models/receipt-summary.js +4 -1
  19. package/dist/cjs/models/receipt.js +8 -1
  20. package/dist/cjs/models/voucher.js +18 -2
  21. package/dist/cjs/payments/hobex-hps/connect-client.d.ts +104 -0
  22. package/dist/cjs/payments/hobex-hps/connect-client.js +159 -0
  23. package/dist/cjs/payments/hobex-hps/errors.d.ts +122 -0
  24. package/dist/cjs/payments/hobex-hps/errors.js +158 -0
  25. package/dist/cjs/payments/hobex-hps/events.d.ts +22 -0
  26. package/dist/cjs/payments/hobex-hps/events.js +2 -0
  27. package/dist/cjs/payments/hobex-hps/index.d.ts +27 -0
  28. package/dist/cjs/payments/hobex-hps/index.js +67 -0
  29. package/dist/cjs/payments/hobex-hps/outcome.d.ts +46 -0
  30. package/dist/cjs/payments/hobex-hps/outcome.js +7 -0
  31. package/dist/cjs/payments/hobex-hps/payments.d.ts +149 -0
  32. package/dist/cjs/payments/hobex-hps/payments.js +573 -0
  33. package/dist/cjs/payments/hobex-hps/receipt.d.ts +28 -0
  34. package/dist/cjs/payments/hobex-hps/receipt.js +60 -0
  35. package/dist/cjs/payments/hobex-hps/transaction-id.d.ts +65 -0
  36. package/dist/cjs/payments/hobex-hps/transaction-id.js +91 -0
  37. package/dist/cjs/payments/hobex-hps/transaction-response.d.ts +175 -0
  38. package/dist/cjs/payments/hobex-hps/transaction-response.js +266 -0
  39. package/dist/cjs/payments/hobex.js +9 -6
  40. package/dist/cjs/payments/index.d.ts +26 -14
  41. package/dist/cjs/payments/index.js +59 -15
  42. package/dist/cjs/printing/index.d.ts +1 -1
  43. package/dist/cjs/printing/index.js +2 -1
  44. package/dist/cjs/printing/webusb.d.ts +37 -7
  45. package/dist/cjs/printing/webusb.js +76 -18
  46. package/dist/cjs/receipt/epos.d.ts +18 -0
  47. package/dist/cjs/receipt/epos.js +78 -6
  48. package/dist/cjs/receipt/index.d.ts +1 -1
  49. package/dist/cjs/receipt/index.js +2 -1
  50. package/dist/cjs/receipt/layout.js +19 -0
  51. package/dist/cjs/register/index.d.ts +1 -1
  52. package/dist/cjs/register/index.js +2 -1
  53. package/dist/cjs/register/pairing.d.ts +59 -0
  54. package/dist/cjs/register/pairing.js +51 -2
  55. package/dist/esm/client/aufrufe.d.ts +1 -1
  56. package/dist/esm/client/aufrufe.js +1 -0
  57. package/dist/esm/client/errors.d.ts +8 -4
  58. package/dist/esm/client/errors.js +12 -9
  59. package/dist/esm/client/receipts.d.ts +22 -0
  60. package/dist/esm/client/receipts.js +16 -0
  61. package/dist/esm/client/transport.js +5 -4
  62. package/dist/esm/index.d.ts +1 -1
  63. package/dist/esm/index.js +1 -1
  64. package/dist/esm/models/cancellation.d.ts +28 -0
  65. package/dist/esm/models/cancellation.js +29 -0
  66. package/dist/esm/models/hobex-receipt.js +10 -3
  67. package/dist/esm/models/index.d.ts +1 -1
  68. package/dist/esm/models/index.js +1 -1
  69. package/dist/esm/models/receipt-summary.d.ts +2 -0
  70. package/dist/esm/models/receipt-summary.js +4 -1
  71. package/dist/esm/models/receipt.js +8 -1
  72. package/dist/esm/models/voucher.js +18 -2
  73. package/dist/esm/payments/hobex-hps/connect-client.d.ts +104 -0
  74. package/dist/esm/payments/hobex-hps/connect-client.js +156 -0
  75. package/dist/esm/payments/hobex-hps/errors.d.ts +122 -0
  76. package/dist/esm/payments/hobex-hps/errors.js +149 -0
  77. package/dist/esm/payments/hobex-hps/events.d.ts +22 -0
  78. package/dist/esm/payments/hobex-hps/events.js +1 -0
  79. package/dist/esm/payments/hobex-hps/index.d.ts +27 -0
  80. package/dist/esm/payments/hobex-hps/index.js +26 -0
  81. package/dist/esm/payments/hobex-hps/outcome.d.ts +46 -0
  82. package/dist/esm/payments/hobex-hps/outcome.js +4 -0
  83. package/dist/esm/payments/hobex-hps/payments.d.ts +149 -0
  84. package/dist/esm/payments/hobex-hps/payments.js +570 -0
  85. package/dist/esm/payments/hobex-hps/receipt.d.ts +28 -0
  86. package/dist/esm/payments/hobex-hps/receipt.js +57 -0
  87. package/dist/esm/payments/hobex-hps/transaction-id.d.ts +65 -0
  88. package/dist/esm/payments/hobex-hps/transaction-id.js +86 -0
  89. package/dist/esm/payments/hobex-hps/transaction-response.d.ts +175 -0
  90. package/dist/esm/payments/hobex-hps/transaction-response.js +254 -0
  91. package/dist/esm/payments/hobex.js +9 -6
  92. package/dist/esm/payments/index.d.ts +26 -14
  93. package/dist/esm/payments/index.js +26 -14
  94. package/dist/esm/printing/index.d.ts +1 -1
  95. package/dist/esm/printing/index.js +1 -1
  96. package/dist/esm/printing/webusb.d.ts +37 -7
  97. package/dist/esm/printing/webusb.js +74 -17
  98. package/dist/esm/receipt/epos.d.ts +18 -0
  99. package/dist/esm/receipt/epos.js +76 -6
  100. package/dist/esm/receipt/index.d.ts +1 -1
  101. package/dist/esm/receipt/index.js +1 -1
  102. package/dist/esm/receipt/layout.js +19 -0
  103. package/dist/esm/register/index.d.ts +1 -1
  104. package/dist/esm/register/index.js +1 -1
  105. package/dist/esm/register/pairing.d.ts +59 -0
  106. package/dist/esm/register/pairing.js +50 -2
  107. package/fixtures/belege/storno-rabatt.json +3 -2
  108. package/fixtures/belege/storno-teil.json +2 -1
  109. package/fixtures/belege/storno-voll.json +2 -1
  110. package/fixtures/erwartet/storno-rabatt.grid32.txt +1 -0
  111. package/fixtures/erwartet/storno-rabatt.grid48.txt +1 -0
  112. package/fixtures/erwartet/storno-rabatt.lines.json +6 -0
  113. package/fixtures/erwartet/storno-teil.grid32.txt +1 -0
  114. package/fixtures/erwartet/storno-teil.grid48.txt +1 -0
  115. package/fixtures/erwartet/storno-teil.lines.json +6 -0
  116. package/fixtures/erwartet/storno-voll.grid32.txt +1 -0
  117. package/fixtures/erwartet/storno-voll.grid48.txt +1 -0
  118. package/fixtures/erwartet/storno-voll.lines.json +6 -0
  119. package/fixtures/hobex-hps-codes.json +83 -0
  120. package/fixtures/manifest.json +12 -12
  121. package/fixtures/oberflaeche.json +2 -1
  122. 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
+ }