@kreiseck/kasseneck-api 0.18.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -26,10 +26,21 @@
26
26
  * bis dahin unbenannter Code ([TECHNICAL_ERROR_CODE], `9900`) war darueber
27
27
  * schluessig und haette eine Zahlung, unter der tatsaechlich Geld geflossen
28
28
  * sein kann, als `declined` gemeldet.
29
+ *
30
+ * Seit 16.09.2026 kommt die TECS-Liste dazu (`tecs-codes.ts`): das HPS
31
+ * reicht die Codes der TECS-Plattform durch, auf der hobex autorisiert. Neu
32
+ * sind dabei [HpsCode.sendReversal] (Storno nachschicken, siehe
33
+ * `payments.ts`) und die Schreibweise: TECS fuehrt `0055`, das Terminal
34
+ * sendet `55` -- [normalizeHpsCode] macht beides zu einem Code.
35
+ *
36
+ * **Einen neuen Code aufnehmen:** Eintrag hier oder in `tecs-codes.ts`,
37
+ * derselbe im Dart-Zwilling, `npm run fixtures:hobex-hps-codes`; ein neuer
38
+ * Grund braucht in jeder Kasse eine Uebersetzung.
29
39
  */
30
40
  Object.defineProperty(exports, "__esModule", { value: true });
31
41
  exports.NOT_FOUND_HTTP_STATUS = exports.TERMINAL_BUSY_HTTP_STATUS = exports.INTERNAL_ERROR_CODE = exports.TERMINAL_BUSY_CODE = exports.HOST_TIMEOUT_REVERSED_CODE = exports.TIP_SELECTION_FAILED_CODE = exports.UNSUPPORTED_USER_DATA_CODE = exports.CHIP_DATA_MISMATCH_CODE = exports.REFUND_DISABLED_CODE = exports.COMPLETION_FAILED_CODE = exports.INVALID_MESSAGE_TYPE_CODE = exports.TERMINAL_BLOCKED_CODE = exports.PASSWORD_NOT_ENTERED_CODE = exports.REFUND_PASSWORD_INVALID_CODE = exports.SCEP_ENROLLMENT_FAILED_CODE = exports.CARD_NOT_SUPPORTED_CODE = exports.CARD_DECLINED_CODE = exports.CARD_INFO_NOT_ENTERED_CODE = exports.DIAGNOSIS_FAILED_CODE = exports.MAX_RETRIES_EXCEEDED_CODE = exports.NOT_FOUND_CODE = exports.INVALID_TX_TYPE_CODE = exports.INVALID_TID_DOCUMENTED_CODE = exports.HOST_STEP_FAILED_CODE = exports.HOST_COMMUNICATION_FAILED_CODE = exports.APP_SELECT_FAILED_CODE = exports.CARD_READ_FAILED_CODE = exports.BAD_REQUEST_CODE = exports.WRONG_PIN_CODE = exports.INVALID_TID_CODE = exports.AMOUNT_OUT_OF_RANGE_CODE = exports.INVALID_AMOUNT_CODE = exports.NOT_ABORTABLE_CODE = exports.CARD_NOT_PRESENT_CODE = exports.ABORTED_CODE = exports.TECHNICAL_ERROR_CODE = exports.NO_STATEMENT_CODE = exports.TRANSACTION_CANCELED_CODE = exports.INVALID_TRANSACTION_CODE = exports.APPROVED_CODE = exports.HPS_MEASURED_CODES = exports.HPS_CODES = exports.HPS_REASON_HINTS = void 0;
32
42
  exports.isHostUncertainReason = isHostUncertainReason;
43
+ exports.normalizeHpsCode = normalizeHpsCode;
33
44
  exports.hpsCodeInfo = hpsCodeInfo;
34
45
  exports.hpsCodeReason = hpsCodeReason;
35
46
  exports.parseHpsTransactionResponse = parseHpsTransactionResponse;
@@ -42,66 +53,75 @@ exports.isCanceled = isCanceled;
42
53
  exports.isConclusive = isConclusive;
43
54
  exports.isConclusiveAsStatus = isConclusiveAsStatus;
44
55
  exports.isHostUncertain = isHostUncertain;
56
+ exports.needsReversal = needsReversal;
45
57
  exports.isUnknownCode = isUnknownCode;
58
+ const tecs_codes_js_1 = require("./tecs-codes.js");
46
59
  /** Der Satz fuer den Bediener je Grund, deutsch. */
47
60
  exports.HPS_REASON_HINTS = {
48
61
  approved: 'Vom Terminal genehmigt.',
62
+ approvedWithCondition: 'Das Terminal meldet eine Genehmigung mit Vorbehalt (etwa nur über einen Teilbetrag). Ob und in welcher Höhe belastet wurde, bitte am Terminalbeleg prüfen — nicht erneut kassieren, bevor es geklärt ist.',
49
63
  aborted: 'Der Vorgang wurde abgebrochen. Es wurde kein Geld bewegt.',
50
- noCard: 'Es wurde keine Karte vorgehalten. Es wurde kein Geld bewegt — '
51
- + 'bitte erneut versuchen.',
52
- cardReadFailed: 'Die Karte konnte nicht gelesen werden. Es wurde kein Geld bewegt — '
53
- + 'bitte erneut versuchen, notfalls die Karte stecken statt auflegen.',
54
- cardDeclined: 'Die Karte wurde vom Terminal abgelehnt. Es wurde kein Geld bewegt '
55
- + '— bitte eine andere Karte oder Zahlungsart verwenden.',
56
- wrongPin: 'Die PIN war falsch. Es wurde kein Geld bewegt — bitte erneut '
57
- + 'versuchen.',
58
- amountInvalid: 'Das Terminal nimmt diesen Betrag nicht an. Es wurde kein Geld '
59
- + 'bewegt.',
60
- tipNotSelected: 'Das Trinkgeld wurde nicht rechtzeitig gewählt. Es wurde kein Geld '
61
- + 'bewegt — bitte erneut versuchen.',
62
- terminalBusy: 'Das Terminal ist noch mit einem anderen Vorgang beschäftigt. Es '
63
- + 'wurde kein Geld bewegt — kurz warten und erneut versuchen.',
64
- terminalBlocked: 'Das Terminal ist gesperrt. Es wurde kein Geld bewegt — bitte hobex '
65
- + 'kontaktieren.',
66
- terminalSetup: 'Das Terminal ist nicht richtig eingerichtet. Es wurde kein Geld '
67
- + 'bewegt — bitte die Terminal-ID in den Einstellungen prüfen, sonst '
68
- + 'hobex kontaktieren.',
69
- terminalFault: 'Das Terminal meldet eine Störung. Es wurde kein Geld bewegt — '
70
- + 'bitte das Terminal neu starten und erneut versuchen.',
71
- requestRejected: 'Das Terminal hat die Anfrage abgewiesen. Es wurde kein Geld bewegt '
72
- + '— tritt das wieder auf, bitte den Support kontaktieren.',
73
- invalidTransaction: 'Das Terminal kennt die ursprüngliche Zahlung nicht. Es wurde kein '
74
- + 'Geld bewegt.',
75
- refundPassword: 'Das Passwort für die Gutschrift war falsch oder wurde nicht '
76
- + 'eingegeben. Es wurde nichts ausgezahlt.',
77
- refundDisabled: 'Gutschriften sind an diesem Terminal abgeschaltet. Es wurde nichts '
78
- + 'ausgezahlt — bitte hobex kontaktieren.',
79
- hostTimeoutReversed: 'hobex hat nicht rechtzeitig geantwortet, das Terminal hat den '
80
- + 'Vorgang selbst storniert. Es wird kein Geld bewegt — bitte erneut '
81
- + 'versuchen.',
82
- hostFault: 'Die Verbindung zwischen Terminal und hobex ist gestört. Ob die '
83
- + 'Karte belastet wurde, weiß das Terminal nicht — bitte nicht erneut '
84
- + 'kassieren, bevor es geklärt ist.',
85
- internalError: 'Das Terminal meldet einen internen Fehler. Ob die Karte belastet '
86
- + 'wurde, ist unklar — bitte nicht erneut kassieren, bevor es geklärt '
87
- + 'ist.',
64
+ noCard: 'Es wurde keine Karte vorgehalten. Es wurde kein Geld bewegt — bitte erneut versuchen.',
65
+ cardReadFailed: 'Die Karte konnte nicht gelesen werden. Es wurde kein Geld bewegt — bitte erneut versuchen, notfalls die Karte stecken statt auflegen.',
66
+ cardDeclined: 'Die Karte wurde vom Terminal abgelehnt. Es wurde kein Geld bewegt — bitte eine andere Karte oder Zahlungsart verwenden.',
67
+ issuerDeclined: 'Die Zahlung wurde von der Bank abgelehnt. Es wurde kein Geld bewegt — bitte eine andere Karte oder Zahlungsart verwenden.',
68
+ cardBlocked: 'Die Karte ist gesperrt. Es wurde kein Geld bewegt — bitte eine andere Karte oder Zahlungsart verwenden.',
69
+ cardExpired: 'Die Karte ist abgelaufen. Es wurde kein Geld bewegt — bitte eine andere Karte oder Zahlungsart verwenden.',
70
+ insufficientFunds: 'Das Konto ist nicht gedeckt oder das Kartenlimit ist erreicht. Es wurde kein Geld bewegt — bitte eine andere Karte oder Zahlungsart verwenden.',
71
+ wrongPin: 'Die PIN war falsch. Es wurde kein Geld bewegt — bitte erneut versuchen.',
72
+ pinTriesExceeded: 'Die PIN wurde zu oft falsch eingegeben. Es wurde kein Geld bewegt — bitte eine andere Karte oder Zahlungsart verwenden.',
73
+ pinRequired: 'Die Bank verlangt die PIN. Es wurde kein Geld bewegt — bitte erneut versuchen, die Karte stecken und die PIN eingeben.',
74
+ amountInvalid: 'Das Terminal nimmt diesen Betrag nicht an. Es wurde kein Geld bewegt.',
75
+ tipNotSelected: 'Das Trinkgeld wurde nicht rechtzeitig gewählt. Es wurde kein Geld bewegt — bitte erneut versuchen.',
76
+ terminalBusy: 'Das Terminal ist noch mit einem anderen Vorgang beschäftigt. Es wurde kein Geld bewegt — kurz warten und erneut versuchen.',
77
+ terminalBlocked: 'Das Terminal ist gesperrt. Es wurde kein Geld bewegt — bitte hobex kontaktieren.',
78
+ terminalSetup: 'Das Terminal ist nicht richtig eingerichtet. Es wurde kein Geld bewegt — bitte die Terminal-ID in den Einstellungen prüfen, sonst hobex kontaktieren.',
79
+ acquirerSetup: 'hobex nimmt dieses Terminal oder diesen Händler so nicht an. Es wurde kein Geld bewegt — bitte hobex kontaktieren.',
80
+ terminalFault: 'Das Terminal meldet eine Störung. Es wurde kein Geld bewegt — bitte das Terminal neu starten und erneut versuchen.',
81
+ requestRejected: 'Das Terminal hat die Anfrage abgewiesen. Es wurde kein Geld bewegt — tritt das wieder auf, bitte den Support kontaktieren.',
82
+ hostRejected: 'hobex hat die Zahlung abgewiesen. Es wurde kein Geld bewegt — bitte erneut versuchen; tritt das wieder auf, hobex kontaktieren.',
83
+ hostUnavailable: 'Die Bank oder hobex ist gerade nicht erreichbar. Es wurde kein Geld bewegt — bitte später erneut versuchen oder eine andere Zahlungsart verwenden.',
84
+ invalidTransaction: 'Das Terminal kennt die ursprüngliche Zahlung nicht. Es wurde kein Geld bewegt.',
85
+ refundPassword: 'Das Passwort für die Gutschrift war falsch oder wurde nicht eingegeben. Es wurde nichts ausgezahlt.',
86
+ refundDisabled: 'Gutschriften sind an diesem Terminal abgeschaltet. Es wurde nichts ausgezahlt — bitte hobex kontaktieren.',
87
+ refundRejected: 'Die Gutschrift wurde abgewiesen (Betrag zu hoch, bereits erstattet oder zu viele Versuche). Es wurde nichts ausgezahlt.',
88
+ hostTimeoutReversed: 'hobex hat nicht rechtzeitig geantwortet, das Terminal hat den Vorgang selbst storniert. Es wird kein Geld bewegt — bitte erneut versuchen.',
89
+ reversedByHost: 'Die Zahlung wurde wegen einer Störung automatisch storniert. Es wurde kein Geld bewegt — bitte erneut versuchen.',
90
+ voidedAfterHostFault: 'hobex hat nicht sauber geantwortet, die Zahlung wurde deshalb sicherheitshalber storniert. Es wurde kein Geld bewegt — bitte erneut versuchen.',
91
+ hostFault: 'Die Verbindung zwischen Terminal und hobex ist gestört. Ob die Karte belastet wurde, weiß das Terminal nicht — bitte nicht erneut kassieren, bevor es geklärt ist.',
92
+ hostTimeout: 'hobex hat nicht rechtzeitig geantwortet. Ob die Karte belastet wurde, weiß das Terminal nicht — bitte nicht erneut kassieren, bevor es geklärt ist.',
93
+ internalError: 'Das Terminal meldet einen internen Fehler. Ob die Karte belastet wurde, ist unklar — bitte nicht erneut kassieren, bevor es geklärt ist.',
88
94
  canceled: 'Die Zahlung ist aufgehoben.',
89
- notAbortable: 'Der Vorgang ist bereits abgeschlossen und lässt sich nicht mehr '
90
- + 'abbrechen.',
95
+ cancelDenied: 'Die Zahlung lässt sich nicht mehr aufheben und bleibt belastet — bitte stattdessen eine Gutschrift ausführen.',
96
+ originalDeclined: 'Die ursprüngliche Zahlung war abgelehnt; es gibt nichts aufzuheben.',
97
+ notAbortable: 'Der Vorgang ist bereits abgeschlossen und lässt sich nicht mehr abbrechen.',
91
98
  noStatement: 'Das Terminal hat zu diesem Vorgang keine Auskunft.',
92
- technicalError: 'Das Terminal meldet einen technischen Fehler; über den Vorgang '
93
- + 'sagt das nichts.',
99
+ technicalError: 'Das Terminal meldet einen technischen Fehler; über den Vorgang sagt das nichts.',
94
100
  unknown: 'Das Terminal nennt einen Code, dessen Bedeutung nicht bekannt ist.',
95
101
  };
96
102
  /**
97
- * Die beiden Gruende hinter einem Code mit `effect: 'hostUncertain'`: ob Geld
98
- * geflossen ist, weiss das Terminal nicht.
103
+ * Die Gruende hinter einem Code mit `effect: 'hostUncertain'`: ob (und wie
104
+ * viel) Geld geflossen ist, weiss das Terminal nicht.
99
105
  */
100
106
  function isHostUncertainReason(reason) {
101
- return reason === 'hostFault' || reason === 'internalError';
107
+ return reason === 'approvedWithCondition'
108
+ || reason === 'hostFault'
109
+ || reason === 'hostTimeout'
110
+ || reason === 'internalError';
102
111
  }
103
- function code(c, title, meaning, effect, reason, source, rejectsRequest = false) {
104
- return { code: c, title, meaning, conclusive: effect === 'conclusive', effect, reason, source, rejectsRequest };
112
+ function code(c, title, meaning, effect, reason, source, rejectsRequest = false, extra = {}) {
113
+ return {
114
+ code: c,
115
+ title,
116
+ meaning,
117
+ conclusive: effect === 'conclusive',
118
+ effect,
119
+ reason,
120
+ source,
121
+ rejectsRequest,
122
+ sendReversal: extra.sendReversal ?? false,
123
+ tecsTitle: extra.tecsTitle ?? null,
124
+ };
105
125
  }
106
126
  /**
107
127
  * Die vollstaendige Codetabelle — Vertrag mit dem Dart-Zwilling
@@ -126,19 +146,20 @@ function code(c, title, meaning, effect, reason, source, rejectsRequest = false)
126
146
  * dokumentierten aufsteigend -- identisch mit dem Dart-Zwilling.
127
147
  */
128
148
  exports.HPS_CODES = [
129
- code('0', 'Authorized', 'genehmigt', 'conclusive', 'approved', 'measuredAndDocumented'),
149
+ code('0', 'Authorized', 'genehmigt', 'conclusive', 'approved', 'measuredAndDocumented', false, { tecsTitle: 'Approved Transaction / OK' }),
130
150
  code('9002', 'Invalid Transaction', 'ungueltiger Vorgang -- das Terminal hat den Vorgang selbst als '
131
- + 'unzulaessig verworfen, bevor irgendetwas in Bewegung kam', 'conclusive', 'invalidTransaction', 'measured', true),
151
+ + 'unzulaessig verworfen, bevor irgendetwas in Bewegung kam', 'conclusive', 'invalidTransaction', 'measuredAndDocumented', true, { tecsTitle: 'Invalid Transaction' }),
132
152
  code('9011', 'Transaction Canceled', 'aufgehoben ("Transaction Canceled") -- der Vorgang unter dieser '
133
- + 'Kennung wurde storniert', 'conclusive', 'canceled', 'measured'),
153
+ + 'Kennung wurde storniert', 'conclusive', 'canceled', 'measuredAndDocumented', false, { tecsTitle: 'Transaction cancelled' }),
134
154
  code('9027', 'Original Tx not found', 'keine Aussage -- steht gleichermassen fuer "nie gesehen", "laeuft '
135
- + 'gerade", "Karte nicht aufgelegt" und "abgebrochen"', 'noStatement', 'noStatement', 'measured'),
155
+ + 'gerade", "Karte nicht aufgelegt" und "abgebrochen"', 'noStatement', 'noStatement', 'measuredAndDocumented', false, { tecsTitle: 'Original Transaction not found' }),
136
156
  code('9900', 'Technical Error Database', '"Technical Error Database" -- gemessen im Zusammenhang mit einer '
137
- + 'nicht rein numerischen Kennung; keine Aussage ueber den Vorgang '
138
- + 'selbst', 'noStatement', 'technicalError', 'measured'),
157
+ + 'nicht rein numerischen Kennung, NACHDEM die Karte verarbeitet war; '
158
+ + 'laut TECS ein Datenbankfehler im Backend -- ob belastet wurde, ist '
159
+ + 'offen', 'hostUncertain', 'internalError', 'measuredAndDocumented', false, { tecsTitle: 'Technical Error: Database (General)' }),
139
160
  code('9003', 'Invalid Amount', '"Invalid Amount" -- der Betrag wird abgewiesen, BEVOR eine Karte '
140
161
  + 'verlangt wird (28.08.2026: 99999,99 EUR, Antwort nach 15,7 s ohne '
141
- + 'Kartenaufforderung); nichts belastet', 'conclusive', 'amountInvalid', 'measured'),
162
+ + 'Kartenaufforderung); nichts belastet', 'conclusive', 'amountInvalid', 'measuredAndDocumented', false, { tecsTitle: 'Invalid Amount' }),
142
163
  code('100002', 'Aborted', 'abgebrochen ("Aborted") -- ueber die Kasse oder am Terminal; '
143
164
  + 'nichts belastet', 'conclusive', 'aborted', 'measuredAndDocumented'),
144
165
  code('100003', 'Card not present', 'Karte nicht aufgelegt ("Card not present") -- innerhalb der Frist '
@@ -153,7 +174,7 @@ exports.HPS_CODES = [
153
174
  + 'gemessene Host-Ablehnung ueberhaupt (02.09.2026 im Betrieb, TID '
154
175
  + '3556988, HPS 1.11.4, Firmware 2.3.9): die Zahlung antwortete '
155
176
  + 'direkt damit, die Statusabfrage danach elfmal in Folge ebenso -- '
156
- + 'eine Host-Ablehnung bleibt am Terminal abrufbar; nichts belastet', 'conclusive', 'wrongPin', 'measured'),
177
+ + 'eine Host-Ablehnung bleibt am Terminal abrufbar; nichts belastet', 'conclusive', 'wrongPin', 'measuredAndDocumented', false, { tecsTitle: 'Incorrect PIN' }),
157
178
  // ---- ab hier: Antwortcodeliste von hobex, erhalten 11.09.2026 ----
158
179
  code('100001', 'Bad Request', 'fehlerhafte Anfrage der Kasse ("Bad Request") -- Abweisung vor dem '
159
180
  + 'Kartenfluss; nichts belastet', 'conclusive', 'requestRejected', 'documented', true),
@@ -165,10 +186,10 @@ exports.HPS_CODES = [
165
186
  + 'Betrieb danach dauerhaft 9027; nichts belastet', 'conclusive', 'cardReadFailed', 'documented'),
166
187
  code('100006', 'Communication with TecsXml failed', 'keine Verbindung zum hobex-Host ("Communication with TecsXml '
167
188
  + 'failed") -- das Terminal storniert NICHT selbst; ob beim Host '
168
- + 'etwas angekommen ist, weiss das Terminal nicht', 'hostUncertain', 'hostFault', 'documented'),
189
+ + 'etwas angekommen ist, weiss das Terminal nicht', 'hostUncertain', 'hostFault', 'documented', false, { sendReversal: true }),
169
190
  code('100007', 'Processing of TecsXml step failed', 'Schritt beim hobex-Host gescheitert ("Processing of TecsXml step '
170
191
  + 'failed") -- das Terminal storniert NICHT selbst; ob beim Host '
171
- + 'belastet wurde, weiss das Terminal nicht', 'hostUncertain', 'hostFault', 'documented'),
192
+ + 'belastet wurde, weiss das Terminal nicht', 'hostUncertain', 'hostFault', 'documented', false, { sendReversal: true }),
172
193
  code('100008', 'Invalid TID', 'Terminal-Kennung passt nicht ("Invalid TID") -- die TID der '
173
194
  + 'Anfrage ist nicht die eingerichtete; Abweisung vor dem '
174
195
  + 'Kartenfluss, nichts belastet (am Geraet gemessen wurde dafuer '
@@ -201,18 +222,18 @@ exports.HPS_CODES = [
201
222
  + 'IN_OPERATION; nichts belastet', 'conclusive', 'terminalBlocked', 'documented', true),
202
223
  code('100023', 'Invalid message type', 'ungueltige Antwort des hobex-Hosts ("Invalid message type") -- '
203
224
  + 'Nachricht und Antwortcode in den UserData ungueltig; ob beim Host '
204
- + 'belastet wurde, weiss das Terminal nicht', 'hostUncertain', 'hostFault', 'documented'),
225
+ + 'belastet wurde, weiss das Terminal nicht', 'hostUncertain', 'hostFault', 'documented', false, { sendReversal: true }),
205
226
  code('100024', 'Transaction completion has failed', 'Abschluss des Online-Vorgangs gescheitert ("Transaction completion '
206
227
  + 'has failed") -- NACH der Anfrage beim Host; ob belastet bleibt, '
207
- + 'weiss das Terminal nicht', 'hostUncertain', 'hostFault', 'documented'),
228
+ + 'weiss das Terminal nicht', 'hostUncertain', 'hostFault', 'documented', false, { sendReversal: true }),
208
229
  code('100025', 'Refund transactions are disabled', 'Gutschriften abgeschaltet ("Refund transactions are disabled") -- '
209
230
  + 'in der Geraeteeinstellung; nichts ausgezahlt', 'conclusive', 'refundDisabled', 'documented'),
210
231
  code('100026', 'Transaction was declined.', 'Host-Antwort passt nicht zur Karte ("Transaction was declined.") '
211
232
  + '-- Chip-Daten fuer eine Karte ohne Chip; das Terminal lehnt ab, ob '
212
- + 'der Host zuvor belastet hat, ist offen', 'hostUncertain', 'hostFault', 'documented'),
233
+ + 'der Host zuvor belastet hat, ist offen', 'hostUncertain', 'hostFault', 'documented', false, { sendReversal: true }),
213
234
  code('100027', 'Unsupported UserData in TecsXml Response', 'unbekannte Daten in der Antwort des hobex-Hosts ("Unsupported '
214
235
  + 'UserData in TecsXml Response") -- ob beim Host belastet wurde, '
215
- + 'weiss das Terminal nicht', 'hostUncertain', 'hostFault', 'documented'),
236
+ + 'weiss das Terminal nicht', 'hostUncertain', 'hostFault', 'documented', false, { sendReversal: true }),
216
237
  code('100028', 'Tip selection process has failed.', 'Trinkgeld nicht gewaehlt ("Tip selection process has failed.") -- '
217
238
  + 'nicht innerhalb der Frist; nichts belastet', 'conclusive', 'tipNotSelected', 'documented'),
218
239
  code('100029', 'Communication with TecsXml timeout', 'Zeitueberschreitung zum hobex-Host ("Communication with TecsXml '
@@ -224,6 +245,7 @@ exports.HPS_CODES = [
224
245
  code('100999', 'Internal Error', 'interner Fehler des Terminals ("Internal Error") -- Sammelcode '
225
246
  + 'fuer jeden sonst nicht benannten Fehler, an jeder Stelle des '
226
247
  + 'Ablaufs moeglich; ob belastet wurde, ist offen', 'hostUncertain', 'internalError', 'documented'),
248
+ ...tecs_codes_js_1.TECS_CODES,
227
249
  ];
228
250
  /**
229
251
  * @deprecated Seit 0.10.0 [HPS_CODES] -- die Tabelle fuehrt nicht mehr nur
@@ -345,9 +367,38 @@ exports.TERMINAL_BUSY_HTTP_STATUS = 409;
345
367
  */
346
368
  exports.NOT_FOUND_HTTP_STATUS = 404;
347
369
  const CODE_BY_ID = new Map(exports.HPS_CODES.map((c) => [c.code, c]));
348
- /** Der Eintrag zu [code] in [HPS_CODES], oder `undefined`, wenn seine Bedeutung nicht feststeht. */
370
+ const PLATZHALTER = 'xx';
371
+ /** Eintraege, die eine ganze Familie abdecken (`81xx`), nach ihrem Praefix. */
372
+ const CODE_BY_PREFIX = new Map(exports.HPS_CODES.filter((c) => c.code.endsWith(PLATZHALTER)).map((c) => [c.code.slice(0, -PLATZHALTER.length), c]));
373
+ /**
374
+ * Die Schreibweise, unter der [code] in [HPS_CODES] steht: ein rein
375
+ * numerischer Code ohne fuehrende Nullen (`0055` -> `55`, `0000` -> `0`),
376
+ * jeder andere unveraendert. Ohne diese Angleichung waere ein `0000` kein
377
+ * `0` -- und damit eine ABLEHNUNG einer genehmigten Zahlung.
378
+ * [parseHpsTransactionResponse] gleicht deshalb schon beim Einlesen an.
379
+ */
380
+ function normalizeHpsCode(code) {
381
+ const c = code.trim();
382
+ if (!/^\d+$/.test(c))
383
+ return c;
384
+ const ohneNullen = c.replace(/^0+/, '');
385
+ return ohneNullen === '' ? '0' : ohneNullen;
386
+ }
387
+ /**
388
+ * Der Eintrag zu [code] in [HPS_CODES], oder `undefined`, wenn seine
389
+ * Bedeutung nicht feststeht. Findet beide Schreibweisen
390
+ * ([normalizeHpsCode]) und die Familien mit Platzhalter (`8105` -> `81xx`).
391
+ */
349
392
  function hpsCodeInfo(code) {
350
- return code === undefined ? undefined : CODE_BY_ID.get(code);
393
+ if (code === undefined)
394
+ return undefined;
395
+ const c = normalizeHpsCode(code);
396
+ const genau = CODE_BY_ID.get(c);
397
+ if (genau)
398
+ return genau;
399
+ if (c.length === 4 && /^\d+$/.test(c))
400
+ return CODE_BY_PREFIX.get(c.slice(0, 2));
401
+ return undefined;
351
402
  }
352
403
  /**
353
404
  * Der Grund zu [code] -- `'unknown'` fuer einen Code ausserhalb der Tabelle,
@@ -391,7 +442,7 @@ function parseHpsTransactionResponse(raw) {
391
442
  // responseCode kommt teils als Zahl, teils als Zeichenkette — siehe
392
443
  // Zwilling. Ein leerer String traegt keine Aussage und wird wie ein
393
444
  // fehlendes Feld behandelt.
394
- responseCode: nonEmpty(stringify(r['responseCode'])),
445
+ responseCode: normalizedCode(stringify(r['responseCode'])),
395
446
  responseText: asString(r['responseText']),
396
447
  state: asString(r['state']),
397
448
  raw: r,
@@ -421,6 +472,14 @@ function stringify(v) {
421
472
  function nonEmpty(v) {
422
473
  return v === undefined || v === '' ? undefined : v;
423
474
  }
475
+ /**
476
+ * Der Ergebniscode in der Schreibweise der Tabelle ([normalizeHpsCode]):
477
+ * `0000` wird `0`. Der Rumpf in `raw` bleibt unveraendert.
478
+ */
479
+ function normalizedCode(v) {
480
+ const c = nonEmpty(v?.trim());
481
+ return c === undefined ? undefined : normalizeHpsCode(c);
482
+ }
424
483
  /** `true`, wenn der Vorgang genehmigt ist. */
425
484
  function isApproved(res) {
426
485
  return res.responseCode === exports.APPROVED_CODE;
@@ -443,7 +502,10 @@ function isNotAbortable(res) {
443
502
  function isNoStatement(res) {
444
503
  return res.responseCode === exports.NO_STATEMENT_CODE;
445
504
  }
446
- /** `true`, wenn das Terminal einen technischen Fehler meldet (9900). */
505
+ /**
506
+ * `true`, wenn das Terminal einen technischen Fehler meldet (9900). Seit der
507
+ * TECS-Liste zugleich [isHostUncertain]: ob belastet wurde, ist offen.
508
+ */
447
509
  function isTechnicalError(res) {
448
510
  return res.responseCode === exports.TECHNICAL_ERROR_CODE;
449
511
  }
@@ -487,6 +549,14 @@ function isConclusiveAsStatus(res) {
487
549
  function isHostUncertain(res) {
488
550
  return hpsCodeInfo(res.responseCode)?.effect === 'hostUncertain';
489
551
  }
552
+ /**
553
+ * `true`, wenn zu diesem Code ein Storno nachzuschicken ist
554
+ * ([HpsCode.sendReversal]) -- der Host hat nicht oder nicht brauchbar
555
+ * geantwortet, etwa `9908`.
556
+ */
557
+ function needsReversal(res) {
558
+ return hpsCodeInfo(res.responseCode)?.sendReversal ?? false;
559
+ }
490
560
  /**
491
561
  * `true`, wenn ein Ergebniscode VORHANDEN ist, dessen Bedeutung aber nicht
492
562
  * feststeht -- er fehlt in [HPS_CODES]. Ein Code, den dieses Modell schlicht
@@ -23,5 +23,5 @@ export { type HpsPaymentEvent, type HpsPaymentEventKind, type HpsPaymentObserver
23
23
  export { type CardPaymentOutcome, type HpsPaymentResult, isHostUncertainResult, mayRetrySafely, } from './outcome.js';
24
24
  export { type HpsCancelOptions, type HpsPaymentOptions, type HpsPayments, type HpsPaymentsOptions, type HpsRefundOptions, createHpsPayments, } from './payments.js';
25
25
  export { MAX_TRANSACTION_ID_LENGTH, createHpsTransactionIdGenerator, isValidHpsTransactionId, newHpsTransactionId, type HpsTransactionIdGeneratorOptions, } from './transaction-id.js';
26
- export { ABORTED_CODE, APP_SELECT_FAILED_CODE, APPROVED_CODE, BAD_REQUEST_CODE, CARD_DECLINED_CODE, CARD_INFO_NOT_ENTERED_CODE, CARD_NOT_PRESENT_CODE, CARD_NOT_SUPPORTED_CODE, CARD_READ_FAILED_CODE, CHIP_DATA_MISMATCH_CODE, COMPLETION_FAILED_CODE, DIAGNOSIS_FAILED_CODE, HOST_COMMUNICATION_FAILED_CODE, HOST_STEP_FAILED_CODE, HOST_TIMEOUT_REVERSED_CODE, HPS_CODES, HPS_MEASURED_CODES, HPS_REASON_HINTS, INTERNAL_ERROR_CODE, INVALID_MESSAGE_TYPE_CODE, INVALID_TID_DOCUMENTED_CODE, INVALID_TX_TYPE_CODE, MAX_RETRIES_EXCEEDED_CODE, NOT_FOUND_CODE, PASSWORD_NOT_ENTERED_CODE, REFUND_DISABLED_CODE, REFUND_PASSWORD_INVALID_CODE, SCEP_ENROLLMENT_FAILED_CODE, TERMINAL_BLOCKED_CODE, TERMINAL_BUSY_CODE, TIP_SELECTION_FAILED_CODE, UNSUPPORTED_USER_DATA_CODE, hpsCodeInfo, hpsCodeReason, isHostUncertain, isHostUncertainReason, INVALID_TRANSACTION_CODE, NOT_ABORTABLE_CODE, INVALID_AMOUNT_CODE, AMOUNT_OUT_OF_RANGE_CODE, INVALID_TID_CODE, WRONG_PIN_CODE, NO_STATEMENT_CODE, TECHNICAL_ERROR_CODE, NOT_FOUND_HTTP_STATUS, TERMINAL_BUSY_HTTP_STATUS, TRANSACTION_CANCELED_CODE, isApproved, isCanceled, isConclusive, isConclusiveAsStatus, isInProgress, isNoStatement, isNotAbortable, isTechnicalError, isUnknownCode, parseHpsTransactionResponse, type HpsCode, type HpsCodeEffect, type HpsCodeReason, type HpsCodeSource, type HpsMeasuredCode, type HpsTransactionResponse, } from './transaction-response.js';
26
+ export { ABORTED_CODE, APP_SELECT_FAILED_CODE, APPROVED_CODE, BAD_REQUEST_CODE, CARD_DECLINED_CODE, CARD_INFO_NOT_ENTERED_CODE, CARD_NOT_PRESENT_CODE, CARD_NOT_SUPPORTED_CODE, CARD_READ_FAILED_CODE, CHIP_DATA_MISMATCH_CODE, COMPLETION_FAILED_CODE, DIAGNOSIS_FAILED_CODE, HOST_COMMUNICATION_FAILED_CODE, HOST_STEP_FAILED_CODE, HOST_TIMEOUT_REVERSED_CODE, HPS_CODES, HPS_MEASURED_CODES, HPS_REASON_HINTS, INTERNAL_ERROR_CODE, INVALID_MESSAGE_TYPE_CODE, INVALID_TID_DOCUMENTED_CODE, INVALID_TX_TYPE_CODE, MAX_RETRIES_EXCEEDED_CODE, NOT_FOUND_CODE, PASSWORD_NOT_ENTERED_CODE, REFUND_DISABLED_CODE, REFUND_PASSWORD_INVALID_CODE, SCEP_ENROLLMENT_FAILED_CODE, TERMINAL_BLOCKED_CODE, TERMINAL_BUSY_CODE, TIP_SELECTION_FAILED_CODE, UNSUPPORTED_USER_DATA_CODE, hpsCodeInfo, hpsCodeReason, isHostUncertain, isHostUncertainReason, INVALID_TRANSACTION_CODE, NOT_ABORTABLE_CODE, INVALID_AMOUNT_CODE, AMOUNT_OUT_OF_RANGE_CODE, INVALID_TID_CODE, WRONG_PIN_CODE, NO_STATEMENT_CODE, TECHNICAL_ERROR_CODE, NOT_FOUND_HTTP_STATUS, TERMINAL_BUSY_HTTP_STATUS, TRANSACTION_CANCELED_CODE, isApproved, isCanceled, isConclusive, isConclusiveAsStatus, isInProgress, isNoStatement, isNotAbortable, needsReversal, normalizeHpsCode, isTechnicalError, isUnknownCode, parseHpsTransactionResponse, type HpsCode, type HpsCodeEffect, type HpsCodeReason, type HpsCodeSource, type HpsMeasuredCode, type HpsTransactionResponse, } from './transaction-response.js';
27
27
  export { hobexReceiptFromHps } from './receipt.js';
@@ -22,5 +22,5 @@ export { HpsClarifyTimeoutError, HpsConnectException, HpsConnectTerminalError, H
22
22
  export { isHostUncertainResult, mayRetrySafely, } from './outcome.js';
23
23
  export { createHpsPayments, } from './payments.js';
24
24
  export { MAX_TRANSACTION_ID_LENGTH, createHpsTransactionIdGenerator, isValidHpsTransactionId, newHpsTransactionId, } from './transaction-id.js';
25
- export { ABORTED_CODE, APP_SELECT_FAILED_CODE, APPROVED_CODE, BAD_REQUEST_CODE, CARD_DECLINED_CODE, CARD_INFO_NOT_ENTERED_CODE, CARD_NOT_PRESENT_CODE, CARD_NOT_SUPPORTED_CODE, CARD_READ_FAILED_CODE, CHIP_DATA_MISMATCH_CODE, COMPLETION_FAILED_CODE, DIAGNOSIS_FAILED_CODE, HOST_COMMUNICATION_FAILED_CODE, HOST_STEP_FAILED_CODE, HOST_TIMEOUT_REVERSED_CODE, HPS_CODES, HPS_MEASURED_CODES, HPS_REASON_HINTS, INTERNAL_ERROR_CODE, INVALID_MESSAGE_TYPE_CODE, INVALID_TID_DOCUMENTED_CODE, INVALID_TX_TYPE_CODE, MAX_RETRIES_EXCEEDED_CODE, NOT_FOUND_CODE, PASSWORD_NOT_ENTERED_CODE, REFUND_DISABLED_CODE, REFUND_PASSWORD_INVALID_CODE, SCEP_ENROLLMENT_FAILED_CODE, TERMINAL_BLOCKED_CODE, TERMINAL_BUSY_CODE, TIP_SELECTION_FAILED_CODE, UNSUPPORTED_USER_DATA_CODE, hpsCodeInfo, hpsCodeReason, isHostUncertain, isHostUncertainReason, INVALID_TRANSACTION_CODE, NOT_ABORTABLE_CODE, INVALID_AMOUNT_CODE, AMOUNT_OUT_OF_RANGE_CODE, INVALID_TID_CODE, WRONG_PIN_CODE, NO_STATEMENT_CODE, TECHNICAL_ERROR_CODE, NOT_FOUND_HTTP_STATUS, TERMINAL_BUSY_HTTP_STATUS, TRANSACTION_CANCELED_CODE, isApproved, isCanceled, isConclusive, isConclusiveAsStatus, isInProgress, isNoStatement, isNotAbortable, isTechnicalError, isUnknownCode, parseHpsTransactionResponse, } from './transaction-response.js';
25
+ export { ABORTED_CODE, APP_SELECT_FAILED_CODE, APPROVED_CODE, BAD_REQUEST_CODE, CARD_DECLINED_CODE, CARD_INFO_NOT_ENTERED_CODE, CARD_NOT_PRESENT_CODE, CARD_NOT_SUPPORTED_CODE, CARD_READ_FAILED_CODE, CHIP_DATA_MISMATCH_CODE, COMPLETION_FAILED_CODE, DIAGNOSIS_FAILED_CODE, HOST_COMMUNICATION_FAILED_CODE, HOST_STEP_FAILED_CODE, HOST_TIMEOUT_REVERSED_CODE, HPS_CODES, HPS_MEASURED_CODES, HPS_REASON_HINTS, INTERNAL_ERROR_CODE, INVALID_MESSAGE_TYPE_CODE, INVALID_TID_DOCUMENTED_CODE, INVALID_TX_TYPE_CODE, MAX_RETRIES_EXCEEDED_CODE, NOT_FOUND_CODE, PASSWORD_NOT_ENTERED_CODE, REFUND_DISABLED_CODE, REFUND_PASSWORD_INVALID_CODE, SCEP_ENROLLMENT_FAILED_CODE, TERMINAL_BLOCKED_CODE, TERMINAL_BUSY_CODE, TIP_SELECTION_FAILED_CODE, UNSUPPORTED_USER_DATA_CODE, hpsCodeInfo, hpsCodeReason, isHostUncertain, isHostUncertainReason, INVALID_TRANSACTION_CODE, NOT_ABORTABLE_CODE, INVALID_AMOUNT_CODE, AMOUNT_OUT_OF_RANGE_CODE, INVALID_TID_CODE, WRONG_PIN_CODE, NO_STATEMENT_CODE, TECHNICAL_ERROR_CODE, NOT_FOUND_HTTP_STATUS, TERMINAL_BUSY_HTTP_STATUS, TRANSACTION_CANCELED_CODE, isApproved, isCanceled, isConclusive, isConclusiveAsStatus, isInProgress, isNoStatement, isNotAbortable, needsReversal, normalizeHpsCode, isTechnicalError, isUnknownCode, parseHpsTransactionResponse, } from './transaction-response.js';
26
26
  export { hobexReceiptFromHps } from './receipt.js';
@@ -108,6 +108,17 @@ import type { HpsPaymentResult } from './outcome.js';
108
108
  * solcher Code), endet sie sofort als `unresolved` -- das Terminal wird es
109
109
  * auch in 90 Sekunden nicht wissen. Meldet sie dagegen `'0'`, ist die
110
110
  * Zahlung genehmigt, ganz normal.
111
+ *
112
+ * ## Storno nach ausbleibender Host-Antwort (16.09.2026)
113
+ *
114
+ * Fuer einen Teil dieser Codes -- der Host hat nicht oder nicht brauchbar
115
+ * geantwortet, etwa `9908` -- gibt hobex vor, ein Storno nachzuschicken
116
+ * (`sendReversal`). [pay] tut das genau einmal, mit Kennung und Betrag der
117
+ * Zahlung, bevor die Statusabfrage laeuft: quittiert mit `'0'` -> `declined`
118
+ * mit `'voidedAfterHostFault'`; sonst weiter wie oben, und ein `9011` in der
119
+ * Statusabfrage heisst dann ebenfalls `declined`. Am Geraet ungemessen, siehe
120
+ * Dart-Zwilling. [refund] schickt kein Storno nach (TECS: `9031`), [cancel]
121
+ * ist selbst eines.
111
122
  */
112
123
  export interface HpsPaymentsOptions {
113
124
  /** Wie lange insgesamt geklaert wird, bevor der Ausgang offen bleibt. Vorgabe 90 s. */
@@ -1,8 +1,13 @@
1
1
  import { HpsClarifyTimeoutError, HpsConnectException, HpsConnectTerminalError, HpsPreflightError, } from './errors.js';
2
2
  import { newHpsTransactionId } from './transaction-id.js';
3
- import { hpsCodeInfo, hpsCodeReason, isApproved, isCanceled, isConclusive, isConclusiveAsStatus, isHostUncertain, isNoStatement, isNotAbortable, isTechnicalError, isUnknownCode, NOT_ABORTABLE_CODE, NOT_FOUND_CODE, TECHNICAL_ERROR_CODE, TRANSACTION_CANCELED_CODE, } from './transaction-response.js';
3
+ import { hpsCodeInfo, hpsCodeReason, isApproved, isCanceled, isConclusive, isConclusiveAsStatus, isHostUncertain, isNoStatement, isNotAbortable, isTechnicalError, isUnknownCode, needsReversal, NOT_ABORTABLE_CODE, NOT_FOUND_CODE, TECHNICAL_ERROR_CODE, TRANSACTION_CANCELED_CODE, } from './transaction-response.js';
4
4
  /** Teiler, mit dem das Abbruchbudget aus [HpsPaymentsOptions.resolveBudgetMs] entsteht. */
5
5
  const ABORT_BUDGET_DIVISOR = 6;
6
+ /**
7
+ * Teiler fuer das Budget des nachgeschickten Stornos: ein Drittel, bei der
8
+ * Vorgabe 30 s. Ein Storno geht ueber den Host und darf Sekunden brauchen.
9
+ */
10
+ const REVERSAL_BUDGET_DIVISOR = 3;
6
11
  export function createHpsPayments(client, target, options = {}) {
7
12
  const resolveBudgetMs = options.resolveBudgetMs ?? 90_000;
8
13
  const maxBackoffMs = options.maxBackoffMs ?? 10_000;
@@ -11,6 +16,7 @@ export function createHpsPayments(client, target, options = {}) {
11
16
  const now = options.now ?? Date.now;
12
17
  const observer = options.observer;
13
18
  const abortBudgetMs = Math.trunc(resolveBudgetMs / ABORT_BUDGET_DIVISOR);
19
+ const reversalBudgetMs = Math.trunc(resolveBudgetMs / REVERSAL_BUDGET_DIVISOR);
14
20
  function emit(kind, message, transactionId) {
15
21
  if (!observer)
16
22
  return;
@@ -67,8 +73,12 @@ export function createHpsPayments(client, target, options = {}) {
67
73
  const info = hpsCodeInfo(res.responseCode);
68
74
  const was = info.reason === 'internalError'
69
75
  ? 'interner Fehler des Terminals'
70
- : 'Stoerung zwischen Terminal und hobex-Host, das Terminal storniert nicht selbst';
71
- return `Terminal meldet ${was} (${info.code} "${info.title}")`;
76
+ : info.reason === 'approvedWithCondition'
77
+ ? 'Genehmigung mit Vorbehalt'
78
+ : info.reason === 'hostTimeout'
79
+ ? 'keine rechtzeitige Antwort des hobex-Hosts, das Terminal storniert nicht selbst'
80
+ : 'Stoerung zwischen Terminal und hobex-Host, das Terminal storniert nicht selbst';
81
+ return `Terminal meldet ${was} (${res.responseCode} "${info.title}")`;
72
82
  }
73
83
  /**
74
84
  * Der Grund eines offenen Ausgangs. [antwort] ist die Antwort auf die
@@ -341,6 +351,37 @@ export function createHpsPayments(client, target, options = {}) {
341
351
  emit('resolved', steps[steps.length - 1], id);
342
352
  return { outcome: 'declined', transactionId: id, response: res, reason: 'aborted', steps: [...steps] };
343
353
  }
354
+ /**
355
+ * Schickt zu einer Zahlung, deren Host-Antwort ausblieb ([anlass] traegt
356
+ * `sendReversal`), GENAU EINMAL ein Storno nach. Ergebnis nur, wenn das
357
+ * Terminal es mit `'0'` quittiert; sonst `null`. Ohne [amountCents]
358
+ * (Gutschrift) wird nichts gesendet.
359
+ */
360
+ async function sendReversal(id, steps, elapsedMs, amountCents, anlass) {
361
+ if (amountCents === undefined) {
362
+ steps.push('Kein Storno nachgeschickt -- die Aufhebung einer Gutschrift laesst TECS nicht zu');
363
+ return null;
364
+ }
365
+ emit('resolving', `Storno wird nachgeschickt (${anlass.responseCode})`, id);
366
+ let res;
367
+ try {
368
+ res = await withinBudget(elapsedMs, () => client.cancel({ ...target, transactionId: id, amountCents }), reversalBudgetMs);
369
+ }
370
+ catch (e) {
371
+ steps.push(`Storno nachgeschickt, aber nicht bestaetigt (${describe(e)}) -- ob es wirkte, ist offen, Ausgang wird abgefragt`);
372
+ noteUnexpected(e, id);
373
+ return null;
374
+ }
375
+ if (!isApproved(res)) {
376
+ const titel = hpsCodeInfo(res.responseCode)?.title;
377
+ steps.push(`Storno nachgeschickt, nicht bestaetigt (${res.responseCode ?? 'ohne Ergebniscode'}`
378
+ + `${titel === undefined ? '' : ` "${titel}"`}) -- Ausgang wird abgefragt`);
379
+ return null;
380
+ }
381
+ steps.push('Storno nachgeschickt und bestaetigt -- die Zahlung ist aufgehoben, es ist nichts belastet');
382
+ emit('resolved', steps[steps.length - 1], id);
383
+ return { outcome: 'declined', transactionId: id, response: res, reason: 'voidedAfterHostFault', steps: [...steps] };
384
+ }
344
385
  /**
345
386
  * Klaert einen offenen Ausgang: erst abbrechen, dann abfragen -- bis das
346
387
  * Terminal etwas sagt oder das Budget aufgebraucht ist.
@@ -353,7 +394,7 @@ export function createHpsPayments(client, target, options = {}) {
353
394
  * ([isHostUncertain]), bekommt `9027` diesen Aussagewert gerade NICHT --
354
395
  * siehe Klassendoku, "Stoerung beim Host".
355
396
  */
356
- async function resolve(id, steps, antwort) {
397
+ async function resolve(id, steps, antwort, stornoCents) {
357
398
  emit('resolving', 'Ausgang offen, Klaerung laeuft', id);
358
399
  const start = now();
359
400
  const elapsedMs = () => now() - start;
@@ -362,6 +403,9 @@ export function createHpsPayments(client, target, options = {}) {
362
403
  // Statusabfrage sie nennt und danach 9027 kommt.
363
404
  let stoerungsAntwort = antwort && isHostUncertain(antwort) ? antwort : undefined;
364
405
  let letzteAntwort = antwort;
406
+ // Das Storno wird hoechstens einmal erwogen; gesendet ist es nur mit Betrag.
407
+ let stornoErwogen = false;
408
+ let stornoGesendet = false;
365
409
  if (stoerungsAntwort === undefined) {
366
410
  const aborted = await tryAbort(id, steps, elapsedMs);
367
411
  if (aborted)
@@ -372,6 +416,13 @@ export function createHpsPayments(client, target, options = {}) {
372
416
  // Host. Ein quittierter Abbruch bewiese nur, dass am Terminal nichts mehr
373
417
  // laeuft, nicht, dass der Host nichts belastet hat.
374
418
  steps.push('Kein Abbruchversuch -- das Terminal hat den Vorgang mit einer Stoerung beim hobex-Host beendet');
419
+ if (needsReversal(stoerungsAntwort)) {
420
+ stornoErwogen = true;
421
+ stornoGesendet = stornoCents !== undefined;
422
+ const storniert = await sendReversal(id, steps, elapsedMs, stornoCents, stoerungsAntwort);
423
+ if (storniert)
424
+ return storniert;
425
+ }
375
426
  }
376
427
  let wait = 0;
377
428
  let transportFailures = 0;
@@ -406,6 +457,24 @@ export function createHpsPayments(client, target, options = {}) {
406
457
  if (isHostUncertain(status))
407
458
  stoerungsAntwort ??= status;
408
459
  const hostUngewiss = stoerungsAntwort !== undefined;
460
+ if (stornoGesendet && isCanceled(status)) {
461
+ steps.push(`Status: Zahlung aufgehoben (${TRANSACTION_CANCELED_CODE}) -- das nachgeschickte Storno hat gegriffen, `
462
+ + 'es ist nichts belastet');
463
+ emit('resolved', steps[steps.length - 1], id);
464
+ return { outcome: 'declined', transactionId: id, response: status, reason: 'voidedAfterHostFault', steps: [...steps] };
465
+ }
466
+ if (!stornoErwogen && needsReversal(status)) {
467
+ // Die Zahlung selbst kam ohne Antwort zurueck, erst die Statusabfrage
468
+ // nennt die ausgebliebene Host-Antwort -- dasselbe Storno wie oben.
469
+ steps.push(statusOhneErgebnis(status));
470
+ stornoErwogen = true;
471
+ stornoGesendet = stornoCents !== undefined;
472
+ const storniert = await sendReversal(id, steps, elapsedMs, stornoCents, status);
473
+ if (storniert)
474
+ return storniert;
475
+ wait = nextWait(wait);
476
+ continue;
477
+ }
409
478
  // Auf die Statusabfrage entscheidet nur ein Code, der den gesuchten
410
479
  // Vorgang beschreibt -- nicht einer, der diese Abfrage abweist
411
480
  // ([isConclusiveAsStatus]). Nach einer Stoerung beim Host entscheidet nur
@@ -607,7 +676,7 @@ export function createHpsPayments(client, target, options = {}) {
607
676
  return settled;
608
677
  steps.push(offeneAntwort(res));
609
678
  }
610
- return resolve(id, steps, res);
679
+ return resolve(id, steps, res, paymentOptions.amountCents);
611
680
  }
612
681
  /**
613
682
  * Gutschrift mit geklaertem Ausgang -- EXAKT derselbe Klaerweg wie [pay]
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Die Antwortcodeliste von TECS, erhalten ueber hobex am 16.09.2026.
3
+ *
4
+ * **Zwilling:** `kasseneck_api/lib/src/hobex_hps/tecs_codes.dart` -- dort
5
+ * steht die Einordnung ausfuehrlich. Kurz: eine Antwort des Hosts ohne
6
+ * Genehmigung lehnt ab; eine Genehmigung, die nicht `0` ist, bleibt offen;
7
+ * keine oder keine brauchbare Host-Antwort bleibt offen und bekommt ein
8
+ * Storno; Codes anderer TECS-Produkte sagen nichts ueber die Zahlung.
9
+ *
10
+ * TECS fuehrt die Host-Antworten vierstellig (`0055`), das Terminal sendet
11
+ * sie ohne Nullen (`55`). Die Tabelle fuehrt die Form des Terminals.
12
+ * Gemessene Codes (`0`, `9002`, `9003`, `9011`, `9027`, `9900`, `55`)
13
+ * stehen in `transaction-response.ts`, hier nur die uebrigen.
14
+ */
15
+ import type { HpsCode } from './transaction-response.js';
16
+ export declare const TECS_CODES: readonly HpsCode[];