@kreiseck/kasseneck-api 0.9.3 → 0.10.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.
Files changed (30) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/dist/cjs/kasse/settings.d.ts +10 -2
  3. package/dist/cjs/kasse/settings.js +10 -2
  4. package/dist/cjs/payments/hobex-hps/index.d.ts +2 -2
  5. package/dist/cjs/payments/hobex-hps/index.js +36 -1
  6. package/dist/cjs/payments/hobex-hps/outcome.d.ts +20 -1
  7. package/dist/cjs/payments/hobex-hps/outcome.js +11 -0
  8. package/dist/cjs/payments/hobex-hps/payments.d.ts +19 -0
  9. package/dist/cjs/payments/hobex-hps/payments.js +160 -33
  10. package/dist/cjs/payments/hobex-hps/transaction-response.d.ts +180 -31
  11. package/dist/cjs/payments/hobex-hps/transaction-response.js +308 -88
  12. package/dist/cjs/payments/index.d.ts +1 -1
  13. package/dist/cjs/payments/index.js +8 -1
  14. package/dist/esm/kasse/settings.d.ts +10 -2
  15. package/dist/esm/kasse/settings.js +10 -2
  16. package/dist/esm/payments/hobex-hps/index.d.ts +2 -2
  17. package/dist/esm/payments/hobex-hps/index.js +2 -2
  18. package/dist/esm/payments/hobex-hps/outcome.d.ts +20 -1
  19. package/dist/esm/payments/hobex-hps/outcome.js +10 -0
  20. package/dist/esm/payments/hobex-hps/payments.d.ts +19 -0
  21. package/dist/esm/payments/hobex-hps/payments.js +161 -34
  22. package/dist/esm/payments/hobex-hps/transaction-response.d.ts +180 -31
  23. package/dist/esm/payments/hobex-hps/transaction-response.js +302 -87
  24. package/dist/esm/payments/index.d.ts +1 -1
  25. package/dist/esm/payments/index.js +1 -1
  26. package/fixtures/hobex-hps-codes.json +365 -15
  27. package/fixtures/kasse-settings-standard.json +1 -1
  28. package/fixtures/kasse-texte.json +1 -1
  29. package/fixtures/oberflaeche.json +2 -1
  30. package/package.json +1 -1
@@ -19,8 +19,8 @@
19
19
  */
20
20
  export { createHpsConnectClient, } from './connect-client.js';
21
21
  export { HpsClarifyTimeoutError, HpsConnectException, HpsConnectTerminalError, HpsConnectTransportError, HpsPreflightError, HpsTransactionIdError, PREFLIGHT_CONNECT_CODES, } from './errors.js';
22
- export { mayRetrySafely, } from './outcome.js';
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, APPROVED_CODE, CARD_NOT_PRESENT_CODE, HPS_MEASURED_CODES, 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, TERMINAL_BUSY_HTTP_STATUS, TRANSACTION_CANCELED_CODE, isApproved, isCanceled, isConclusive, 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, TERMINAL_BUSY_HTTP_STATUS, TRANSACTION_CANCELED_CODE, isApproved, isCanceled, isConclusive, isConclusiveAsStatus, isInProgress, isNoStatement, isNotAbortable, isTechnicalError, isUnknownCode, parseHpsTransactionResponse, } from './transaction-response.js';
26
26
  export { hobexReceiptFromHps } from './receipt.js';
@@ -1,4 +1,4 @@
1
- import type { HpsTransactionResponse } from './transaction-response.js';
1
+ import { type HpsCodeReason, type HpsTransactionResponse } from './transaction-response.js';
2
2
  /**
3
3
  * Ausgang eines Kartenzahlvorgangs — die einzige Frage, die ein Aufrufer
4
4
  * wirklich hat: darf ich es nochmal versuchen?
@@ -35,6 +35,18 @@ export interface HpsPaymentResult {
35
35
  * Entscheidung getragen. Bei schluessigem Ausgang nicht gesetzt.
36
36
  */
37
37
  readonly lastResponse?: HpsTransactionResponse;
38
+ /**
39
+ * Worauf die Kasse reagiert -- der Grund hinter dem Ausgang, mit dem Satz
40
+ * fuer den Bediener in `HPS_REASON_HINTS`.
41
+ *
42
+ * Gesetzt an der Stelle, die den Ausgang entschieden hat, nicht aus
43
+ * [response] abgeleitet: ein bestaetigter Abbruch antwortet `'0'` und ist
44
+ * trotzdem `'aborted'`, und wenn die Zwei-9027-Regel eine Zahlung als
45
+ * abgelehnt klaert, zaehlt der Code der ZAHLUNG, nicht das `9027` danach.
46
+ * Fehlt, wenn kein Code etwas erklaert (die Leitung riss ab, bevor das
47
+ * Terminal etwas sagte) und bei einer Aufhebung, die nicht gegriffen hat.
48
+ */
49
+ readonly reason?: HpsCodeReason;
38
50
  /**
39
51
  * Verlauf der Klaerung, in Reihenfolge — der Nachweis, der im
40
52
  * Belastungsstreit gelesen wird. Behauptet nie eine Ursache, die nicht
@@ -44,3 +56,10 @@ export interface HpsPaymentResult {
44
56
  }
45
57
  /** Nur bei `'declined'` steht fest, dass nichts belastet wurde. */
46
58
  export declare function mayRetrySafely(result: Pick<HpsPaymentResult, 'outcome'>): boolean;
59
+ /**
60
+ * `true`, wenn der Ausgang offen ist, weil das Terminal eine Stoerung beim
61
+ * oder nach dem hobex-Host meldete (`effect: 'hostUncertain'`). Wichtig fuer
62
+ * jede SPAETERE Nachfrage: antwortet die Statusabfrage dann `9027`, heisst das
63
+ * hier NICHT "nichts belastet".
64
+ */
65
+ export declare function isHostUncertainResult(result: Pick<HpsPaymentResult, 'outcome' | 'reason'>): boolean;
@@ -1,4 +1,14 @@
1
+ import { isHostUncertainReason } from './transaction-response.js';
1
2
  /** Nur bei `'declined'` steht fest, dass nichts belastet wurde. */
2
3
  export function mayRetrySafely(result) {
3
4
  return result.outcome === 'declined';
4
5
  }
6
+ /**
7
+ * `true`, wenn der Ausgang offen ist, weil das Terminal eine Stoerung beim
8
+ * oder nach dem hobex-Host meldete (`effect: 'hostUncertain'`). Wichtig fuer
9
+ * jede SPAETERE Nachfrage: antwortet die Statusabfrage dann `9027`, heisst das
10
+ * hier NICHT "nichts belastet".
11
+ */
12
+ export function isHostUncertainResult(result) {
13
+ return result.outcome === 'unresolved' && isHostUncertainReason(result.reason);
14
+ }
@@ -89,6 +89,25 @@ import type { HpsPaymentResult } from './outcome.js';
89
89
  * Kein [tryAbort]-Versuch bei [cancel]: die Originalzahlung ist laengst
90
90
  * abgeschlossen und antwortet gemessen mit `100010` -- ein Abbruch darauf
91
91
  * waere sinnlos.
92
+ *
93
+ * ## Stoerung beim Host: die Klaerung darf nichts schliessen (11.09.2026)
94
+ *
95
+ * Die Antwortcodeliste von hobex benennt Codes, bei denen der Host beteiligt
96
+ * war und das Terminal NICHT selbst storniert (`effect: 'hostUncertain'`,
97
+ * etwa `100007`). Fuer sie gilt zweierlei:
98
+ *
99
+ * 1. Die Zwei-9027-Regel ([ausGeschlossenerAntwort]) greift nicht. Sie
100
+ * schliesst aus "das Terminal hat geantwortet und nichts gespeichert" auf
101
+ * "nichts belastet" -- das stimmt fuer einen Vorgang, der am Terminal
102
+ * endete, aber nicht fuer einen, dessen Ausgang beim Host liegt. Dasselbe
103
+ * gilt sinngemaess fuer [cancel]: ein unveraendertes `'0'` auf die
104
+ * Originalzahlung beweist dann nicht, dass die Aufhebung beim Host nicht
105
+ * ankam.
106
+ * 2. Die Klaerung wartet trotzdem nicht das ganze Budget ab. Sagt die
107
+ * Statusabfrage zweimal in Folge nichts Neues (`9027` oder erneut ein
108
+ * solcher Code), endet sie sofort als `unresolved` -- das Terminal wird es
109
+ * auch in 90 Sekunden nicht wissen. Meldet sie dagegen `'0'`, ist die
110
+ * Zahlung genehmigt, ganz normal.
92
111
  */
93
112
  export interface HpsPaymentsOptions {
94
113
  /** Wie lange insgesamt geklaert wird, bevor der Ausgang offen bleibt. Vorgabe 90 s. */
@@ -1,6 +1,6 @@
1
1
  import { HpsClarifyTimeoutError, HpsConnectException, HpsConnectTerminalError, HpsPreflightError, } from './errors.js';
2
2
  import { newHpsTransactionId } from './transaction-id.js';
3
- import { isApproved, isCanceled, isConclusive, isNoStatement, isNotAbortable, isTechnicalError, isUnknownCode, NOT_ABORTABLE_CODE, TECHNICAL_ERROR_CODE, TRANSACTION_CANCELED_CODE, } from './transaction-response.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';
4
4
  /** Teiler, mit dem das Abbruchbudget aus [HpsPaymentsOptions.resolveBudgetMs] entsteht. */
5
5
  const ABORT_BUDGET_DIVISOR = 6;
6
6
  export function createHpsPayments(client, target, options = {}) {
@@ -56,7 +56,34 @@ export function createHpsPayments(client, target, options = {}) {
56
56
  return null;
57
57
  steps.push('Terminal beschaeftigt (HTTP 409) -- die Anfrage wurde nicht angenommen, es ist nichts geschehen');
58
58
  emit('resolved', steps[steps.length - 1], id);
59
- return { outcome: 'declined', transactionId: id, steps: [...steps] };
59
+ return { outcome: 'declined', transactionId: id, reason: 'terminalBusy', steps: [...steps] };
60
+ }
61
+ /**
62
+ * Benennt einen Code mit `effect: 'hostUncertain'` fuer den Nachweis: was
63
+ * das Terminal meldet, mit Code und hobex-Titel. Die Folgerung setzt der
64
+ * Aufrufer dazu.
65
+ */
66
+ function stoerung(res) {
67
+ const info = hpsCodeInfo(res.responseCode);
68
+ const was = info.reason === 'internalError'
69
+ ? '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}")`;
72
+ }
73
+ /**
74
+ * Der Grund eines offenen Ausgangs. [antwort] ist die Antwort auf die
75
+ * ERZEUGENDE Anfrage (oder die zuerst gemeldete Stoerung), [letzte] die
76
+ * zuletzt gelesene Statusabfrage. Eine Stoerung beim Host geht vor; sonst
77
+ * erklaert der Code der erzeugenden Anfrage mehr als ein `9027` danach.
78
+ */
79
+ function offenerGrund(antwort, letzte) {
80
+ if (antwort && isHostUncertain(antwort))
81
+ return hpsCodeReason(antwort.responseCode);
82
+ if (letzte && isHostUncertain(letzte))
83
+ return hpsCodeReason(letzte.responseCode);
84
+ if (antwort?.responseCode !== undefined)
85
+ return hpsCodeReason(antwort.responseCode);
86
+ return hpsCodeReason(letzte?.responseCode);
60
87
  }
61
88
  /** Verlaufseintrag fuer eine direkte Antwort, die den Ausgang NICHT festschreibt. */
62
89
  function offeneAntwort(res) {
@@ -66,6 +93,12 @@ export function createHpsPayments(client, target, options = {}) {
66
93
  if (isTechnicalError(res)) {
67
94
  return `Antwort mit technischem Fehler (${TECHNICAL_ERROR_CODE}) -- keine Aussage ueber den Vorgang, Ausgang wird geklaert`;
68
95
  }
96
+ if (isHostUncertain(res)) {
97
+ return `${stoerung(res)} -- ob belastet wurde, weiss das Terminal nicht, Ausgang wird geklaert`;
98
+ }
99
+ if (res.responseCode === NOT_FOUND_CODE) {
100
+ return `Terminal kennt den Vorgang nicht (${res.responseCode} "Not Found") -- keine Aussage, Ausgang wird geklaert`;
101
+ }
69
102
  if (isUnknownCode(res)) {
70
103
  return `Terminal nennt einen unbekannten Code (${res.responseCode})${klartext(res)} -- Ausgang wird geklaert`;
71
104
  }
@@ -79,6 +112,22 @@ export function createHpsPayments(client, target, options = {}) {
79
112
  if (isTechnicalError(status)) {
80
113
  return `Status: technischer Fehler (${TECHNICAL_ERROR_CODE}) -- keine Aussage ueber den Vorgang`;
81
114
  }
115
+ if (isHostUncertain(status)) {
116
+ return `Status: ${stoerung(status)} -- keine Aussage`;
117
+ }
118
+ // Bewusst NICHT "keine Auskunft": das Wort steht fuer das gemessene 9027,
119
+ // und Aufrufer lesen es als solches (sastre, OpenCardPaymentService).
120
+ if (status.responseCode === NOT_FOUND_CODE) {
121
+ return `Status: Vorgang nicht gefunden (${status.responseCode}) -- keine Aussage`;
122
+ }
123
+ const info = hpsCodeInfo(status.responseCode);
124
+ if (info?.rejectsRequest) {
125
+ return `Status: Abfrage abgewiesen (${info.code} "${info.title}") -- keine Aussage ueber den Vorgang`;
126
+ }
127
+ if (info?.conclusive && !isApproved(status)) {
128
+ // Nur erreichbar nach einer Stoerung beim Host, siehe [resolve].
129
+ return `Status: abgelehnt (${info.code} "${info.title}") -- nach der Stoerung beim hobex-Host entscheidet nur eine Genehmigung`;
130
+ }
82
131
  if (isUnknownCode(status)) {
83
132
  return `Status: unbekannter Code (${status.responseCode})${klartext(status)} -- keine Aussage`;
84
133
  }
@@ -95,14 +144,27 @@ export function createHpsPayments(client, target, options = {}) {
95
144
  const text = res.responseText?.trim();
96
145
  return text ? ` "${text}"` : '';
97
146
  }
98
- /** Ordnet eine Terminal-Antwort ein. `null`, wenn sie nichts entscheidet. */
99
- function fromResponse(res, id, steps) {
147
+ /**
148
+ * Ordnet eine Terminal-Antwort ein. `null`, wenn sie nichts entscheidet.
149
+ *
150
+ * [aufhebung]: die Antwort gehoert zu [cancel]. Ein `'0'` heisst dort
151
+ * "aufgehoben", und der Grund ist `'canceled'` statt `'approved'`.
152
+ */
153
+ function fromResponse(res, id, steps, aufhebung = false) {
100
154
  if (!isConclusive(res))
101
155
  return null;
102
156
  const approved = res.responseCode === '0';
103
- steps.push(approved ? 'Terminal: genehmigt' : `Terminal: abgelehnt (${res.responseCode})`);
157
+ steps.push(approved
158
+ ? 'Terminal: genehmigt'
159
+ : `Terminal: abgelehnt (${res.responseCode} "${hpsCodeInfo(res.responseCode).title}")`);
104
160
  emit('resolved', steps[steps.length - 1], id);
105
- return { outcome: approved ? 'approved' : 'declined', transactionId: id, response: res, steps: [...steps] };
161
+ return {
162
+ outcome: approved ? 'approved' : 'declined',
163
+ transactionId: id,
164
+ response: res,
165
+ reason: approved && aufhebung ? 'canceled' : hpsCodeReason(res.responseCode),
166
+ steps: [...steps],
167
+ };
106
168
  }
107
169
  /**
108
170
  * Ordnet die DIREKTE Antwort auf einen Aufhebungs-Request ein. Fast dasselbe
@@ -128,7 +190,7 @@ export function createHpsPayments(client, target, options = {}) {
128
190
  steps.push(`Aufhebung mit ${TRANSACTION_CANCELED_CODE} beantwortet -- mehrdeutig, der Zustand der Originalzahlung wird abgefragt`);
129
191
  return null;
130
192
  }
131
- return fromResponse(res, id, steps);
193
+ return fromResponse(res, id, steps, true);
132
194
  }
133
195
  /**
134
196
  * Ordnet die Statusabfrage einer OFFENEN AUFHEBUNG ein. `null`, wenn sie
@@ -183,7 +245,7 @@ export function createHpsPayments(client, target, options = {}) {
183
245
  if (voided) {
184
246
  steps.push(`Terminal: Aufhebung bestaetigt (${status.responseCode ?? status.state})`);
185
247
  emit('resolved', steps[steps.length - 1], id);
186
- return { outcome: 'approved', transactionId: id, response: status, steps: [...steps] };
248
+ return { outcome: 'approved', transactionId: id, response: status, reason: 'canceled', steps: [...steps] };
187
249
  }
188
250
  if (isApproved(status)) {
189
251
  if (firstQuery) {
@@ -199,14 +261,15 @@ export function createHpsPayments(client, target, options = {}) {
199
261
  /**
200
262
  * [letzteAntwort] ist die letzte Antwort, die das Terminal in dieser
201
263
  * Klaerung gab -- als `lastResponse` fuer Anzeige und Katalog,
202
- * ausdruecklich NICHT als `response`.
264
+ * ausdruecklich NICHT als `response`. [grund] siehe [offenerGrund].
203
265
  */
204
- function open(id, steps, letzteAntwort) {
266
+ function open(id, steps, letzteAntwort, grund) {
205
267
  emit('resolved', steps[steps.length - 1], id);
206
268
  return {
207
269
  outcome: 'unresolved',
208
270
  transactionId: id,
209
271
  ...(letzteAntwort ? { lastResponse: letzteAntwort } : {}),
272
+ ...(grund ? { reason: grund } : {}),
210
273
  steps: [...steps],
211
274
  };
212
275
  }
@@ -271,26 +334,46 @@ export function createHpsPayments(client, target, options = {}) {
271
334
  }
272
335
  steps.push('Abbruch bestaetigt -- der Vorgang war noch abbrechbar, es ist nichts belastet');
273
336
  emit('resolved', steps[steps.length - 1], id);
274
- return { outcome: 'declined', transactionId: id, response: res, steps: [...steps] };
337
+ return { outcome: 'declined', transactionId: id, response: res, reason: 'aborted', steps: [...steps] };
275
338
  }
276
- /** Klaert einen offenen Ausgang: erst abbrechen, dann abfragen -- bis das Terminal etwas sagt oder das Budget aufgebraucht ist. */
277
339
  /**
278
- * `antwortMitCode` heisst: das Terminal hat auf die ERZEUGENDE Anfrage eine
279
- * Antwort MIT Ergebniscode geliefert, deren Bedeutung wir nur nicht kennen.
280
- * Dann ist der Vorgang am Geraet abgeschlossen -- und erst dadurch bekommt
281
- * [NO_STATEMENT_CODE] (`9027`) beim Pollen einen Aussagewert, den es sonst
282
- * nicht hat. Siehe [ausGeschlossenerAntwort].
340
+ * Klaert einen offenen Ausgang: erst abbrechen, dann abfragen -- bis das
341
+ * Terminal etwas sagt oder das Budget aufgebraucht ist.
342
+ *
343
+ * [antwort] ist die Antwort auf die ERZEUGENDE Anfrage, sofern eine kam.
344
+ * Traegt sie einen Ergebniscode, dessen Bedeutung wir nur nicht kennen, ist
345
+ * der Vorgang am Geraet abgeschlossen -- und erst dadurch bekommt
346
+ * `9027` beim Pollen einen Aussagewert, den es sonst nicht hat. Siehe
347
+ * [ausGeschlossenerAntwort]. Meldet sie eine Stoerung beim Host
348
+ * ([isHostUncertain]), bekommt `9027` diesen Aussagewert gerade NICHT --
349
+ * siehe Klassendoku, "Stoerung beim Host".
283
350
  */
284
- async function resolve(id, steps, antwortMitCode = false, letzteAntwort) {
351
+ async function resolve(id, steps, antwort) {
285
352
  emit('resolving', 'Ausgang offen, Klaerung laeuft', id);
286
353
  const start = now();
287
354
  const elapsedMs = () => now() - start;
288
- const aborted = await tryAbort(id, steps, elapsedMs);
289
- if (aborted)
290
- return aborted;
355
+ const antwortMitCode = antwort?.responseCode !== undefined;
356
+ // Einmal gemeldet, bleibt die Stoerung stehen -- auch wenn erst die
357
+ // Statusabfrage sie nennt und danach 9027 kommt.
358
+ let stoerungsAntwort = antwort && isHostUncertain(antwort) ? antwort : undefined;
359
+ let letzteAntwort = antwort;
360
+ if (stoerungsAntwort === undefined) {
361
+ const aborted = await tryAbort(id, steps, elapsedMs);
362
+ if (aborted)
363
+ return aborted;
364
+ }
365
+ else {
366
+ // Der Vorgang ist am Terminal schon beendet -- mit einer Stoerung beim
367
+ // Host. Ein quittierter Abbruch bewiese nur, dass am Terminal nichts mehr
368
+ // laeuft, nicht, dass der Host nichts belastet hat.
369
+ steps.push('Kein Abbruchversuch -- das Terminal hat den Vorgang mit einer Stoerung beim hobex-Host beendet');
370
+ }
291
371
  let wait = 0;
292
372
  let transportFailures = 0;
293
373
  let ohneAuskunft = 0;
374
+ // Beantwortete Statusabfragen in Folge, die zu einem Vorgang mit
375
+ // Host-Stoerung nichts Neues sagen.
376
+ let stoerungOhneNeues = 0;
294
377
  while (elapsedMs() < resolveBudgetMs) {
295
378
  if (wait > 0) {
296
379
  const left = resolveBudgetMs - elapsedMs();
@@ -315,23 +398,45 @@ export function createHpsPayments(client, target, options = {}) {
315
398
  wait = nextWait(wait);
316
399
  continue;
317
400
  }
318
- const settled = fromResponse(status, id, steps);
319
- if (settled)
320
- return settled;
401
+ if (isHostUncertain(status))
402
+ stoerungsAntwort ??= status;
403
+ const hostUngewiss = stoerungsAntwort !== undefined;
404
+ // Auf die Statusabfrage entscheidet nur ein Code, der den gesuchten
405
+ // Vorgang beschreibt -- nicht einer, der diese Abfrage abweist
406
+ // ([isConclusiveAsStatus]). Nach einer Stoerung beim Host entscheidet nur
407
+ // noch eine Genehmigung: jede andere Aussage des Terminals betrifft seinen
408
+ // Speicher, nicht den des Hosts.
409
+ if (hostUngewiss ? isApproved(status) : isConclusiveAsStatus(status)) {
410
+ const settled = fromResponse(status, id, steps);
411
+ if (settled)
412
+ return settled;
413
+ }
321
414
  if (isNoStatement(status)) {
322
415
  ohneAuskunft += 1;
323
- const geklaert = ausGeschlossenerAntwort(id, steps, status, antwortMitCode, ohneAuskunft);
324
- if (geklaert)
325
- return geklaert;
416
+ if (!hostUngewiss) {
417
+ const geklaert = ausGeschlossenerAntwort(id, steps, status, antwortMitCode ? antwort : undefined, ohneAuskunft);
418
+ if (geklaert)
419
+ return geklaert;
420
+ }
326
421
  }
327
422
  else {
328
423
  ohneAuskunft = 0;
329
424
  }
425
+ // Nach einer Stoerung ist jede Antwort ausser '0' (die oben schon
426
+ // entschieden hat) "nichts Neues".
427
+ stoerungOhneNeues = hostUngewiss ? stoerungOhneNeues + 1 : 0;
330
428
  steps.push(statusOhneErgebnis(status) ?? 'Status: noch kein Ergebniscode');
429
+ if (stoerungOhneNeues >= 2) {
430
+ // Siehe Klassendoku, "Stoerung beim Host": das Terminal weiss es
431
+ // nicht, und es wird es auch nicht wissen, wenn wir weiterfragen.
432
+ steps.push('Statusabfrage zweimal ohne Neues nach einer Stoerung beim hobex-Host -- '
433
+ + 'das Terminal kann nicht sagen, ob belastet wurde, Ausgang bleibt offen');
434
+ return open(id, steps, letzteAntwort, offenerGrund(stoerungsAntwort ?? antwort, letzteAntwort));
435
+ }
331
436
  wait = nextWait(wait);
332
437
  }
333
438
  steps.push('Ausgang bleibt offen');
334
- return open(id, steps, letzteAntwort);
439
+ return open(id, steps, letzteAntwort, offenerGrund(stoerungsAntwort ?? antwort, letzteAntwort));
335
440
  }
336
441
  /**
337
442
  * Liest `9027` beim Pollen als "nicht genehmigt" -- aber NUR, wenn das
@@ -356,10 +461,15 @@ export function createHpsPayments(client, target, options = {}) {
356
461
  * entstuende die Doppelbelastung vom 24.08.2026 an einer neuen Stelle.
357
462
  * Dieselbe Absicherung traegt bereits [fromCancelStatus].
358
463
  *
464
+ * [antwort] ist die Antwort MIT Code auf die erzeugende Anfrage -- oder
465
+ * `undefined`, wenn keine kam; dann greift die Regel nicht. Ihr Code ergibt
466
+ * den `reason`: er erklaert den Ausgang, das `9027` danach nicht. Der
467
+ * Aufrufer ruft diese Regel NICHT fuer eine Stoerung beim Host.
468
+ *
359
469
  * `undefined` heisst: nicht entschieden, weiter pollen.
360
470
  */
361
- function ausGeschlossenerAntwort(id, steps, status, antwortMitCode, ohneAuskunft) {
362
- if (!antwortMitCode)
471
+ function ausGeschlossenerAntwort(id, steps, status, antwort, ohneAuskunft) {
472
+ if (!antwort)
363
473
  return undefined;
364
474
  if (ohneAuskunft < 2)
365
475
  return undefined;
@@ -371,6 +481,7 @@ export function createHpsPayments(client, target, options = {}) {
371
481
  outcome: 'declined',
372
482
  transactionId: id,
373
483
  response: status,
484
+ reason: hpsCodeReason(antwort.responseCode),
374
485
  steps: [...steps],
375
486
  };
376
487
  }
@@ -390,11 +501,17 @@ export function createHpsPayments(client, target, options = {}) {
390
501
  *
391
502
  * Budget, Backoff und Transportfehler-Deckelung sind unveraendert aus
392
503
  * [resolve] uebernommen.
504
+ *
505
+ * [antwort] ist die direkte Antwort auf den Aufhebungs-Request, sofern eine
506
+ * kam. Meldete sie eine Stoerung beim Host, entscheidet ein unveraendertes
507
+ * `'0'` nichts -- siehe Klassendoku, "Stoerung beim Host".
393
508
  */
394
- async function resolveCancel(id, steps, letzteAntwort) {
509
+ async function resolveCancel(id, steps, antwort) {
395
510
  emit('resolving', 'Ausgang offen, Klaerung laeuft', id);
396
511
  const start = now();
397
512
  const elapsedMs = () => now() - start;
513
+ const hostUngewiss = antwort !== undefined && isHostUncertain(antwort);
514
+ let letzteAntwort = antwort;
398
515
  let wait = 0;
399
516
  let transportFailures = 0;
400
517
  // Zaehlt nur BEANTWORTETE Statusabfragen -- Grundlage der Karenz fuer den
@@ -425,6 +542,16 @@ export function createHpsPayments(client, target, options = {}) {
425
542
  wait = nextWait(wait);
426
543
  continue;
427
544
  }
545
+ if (hostUngewiss && isApproved(status) && answeredQueries >= 2) {
546
+ // Siehe Klassendoku, "Stoerung beim Host": das unveraenderte '0'
547
+ // spiegelt nur den Speicher des Terminals. "Hat nicht gegriffen"
548
+ // fuehrte nach dem Tagesabschluss zu einer Rueckerstattung, die der
549
+ // Kunde doppelt bekaeme, falls der Host die Aufhebung doch verbucht hat.
550
+ steps.push('Terminal: Originalzahlung steht unveraendert (0), aber die Aufhebung endete mit einer '
551
+ + 'Stoerung beim hobex-Host -- ob sie dort gewirkt hat, kann das Terminal nicht sagen, '
552
+ + 'Ausgang bleibt offen');
553
+ return open(id, steps, letzteAntwort, offenerGrund(antwort, letzteAntwort));
554
+ }
428
555
  const settled = fromCancelStatus(status, id, steps, answeredQueries === 1);
429
556
  if (settled)
430
557
  return settled;
@@ -436,7 +563,7 @@ export function createHpsPayments(client, target, options = {}) {
436
563
  wait = nextWait(wait);
437
564
  }
438
565
  steps.push('Ausgang bleibt offen');
439
- return open(id, steps, letzteAntwort);
566
+ return open(id, steps, letzteAntwort, offenerGrund(antwort, letzteAntwort));
440
567
  }
441
568
  async function pay(paymentOptions) {
442
569
  const id = paymentOptions.transactionId ?? newHpsTransactionId();
@@ -475,7 +602,7 @@ export function createHpsPayments(client, target, options = {}) {
475
602
  return settled;
476
603
  steps.push(offeneAntwort(res));
477
604
  }
478
- return resolve(id, steps, res?.responseCode !== undefined, res);
605
+ return resolve(id, steps, res);
479
606
  }
480
607
  /**
481
608
  * Gutschrift mit geklaertem Ausgang -- EXAKT derselbe Klaerweg wie [pay]
@@ -519,7 +646,7 @@ export function createHpsPayments(client, target, options = {}) {
519
646
  // Dieselbe Klaerfunktion wie [pay]: die Kennung ist die des NEUEN
520
647
  // Vorgangs, eine Statusabfrage darauf liefert also genau dessen Ausgang,
521
648
  // und der Abbruch ist derselbe Diskriminator wie bei einer Zahlung.
522
- return resolve(id, steps, res?.responseCode !== undefined, res);
649
+ return resolve(id, steps, res);
523
650
  }
524
651
  /**
525
652
  * Aufhebung (Storno/Void) einer bestehenden Zahlung mit geklaertem Ausgang.