@kreiseck/kasseneck-api 0.18.0 → 0.20.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 (35) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +17 -4
  3. package/dist/cjs/payments/hobex-hps/index.d.ts +1 -1
  4. package/dist/cjs/payments/hobex-hps/index.js +3 -1
  5. package/dist/cjs/payments/hobex-hps/payments.d.ts +11 -0
  6. package/dist/cjs/payments/hobex-hps/payments.js +73 -4
  7. package/dist/cjs/payments/hobex-hps/tecs-codes.d.ts +16 -0
  8. package/dist/cjs/payments/hobex-hps/tecs-codes.js +366 -0
  9. package/dist/cjs/payments/hobex-hps/transaction-response.d.ts +56 -8
  10. package/dist/cjs/payments/hobex-hps/transaction-response.js +135 -65
  11. package/dist/cjs/rechnung/texte.d.ts +5 -4
  12. package/dist/cjs/rechnung/texte.js +16 -8
  13. package/dist/cjs/rechnung/typen.d.ts +7 -0
  14. package/dist/cjs/rechnung/vertrag.d.ts +7 -4
  15. package/dist/cjs/rechnung/vertrag.js +14 -4
  16. package/dist/esm/payments/hobex-hps/index.d.ts +1 -1
  17. package/dist/esm/payments/hobex-hps/index.js +1 -1
  18. package/dist/esm/payments/hobex-hps/payments.d.ts +11 -0
  19. package/dist/esm/payments/hobex-hps/payments.js +74 -5
  20. package/dist/esm/payments/hobex-hps/tecs-codes.d.ts +16 -0
  21. package/dist/esm/payments/hobex-hps/tecs-codes.js +363 -0
  22. package/dist/esm/payments/hobex-hps/transaction-response.d.ts +56 -8
  23. package/dist/esm/payments/hobex-hps/transaction-response.js +133 -65
  24. package/dist/esm/rechnung/texte.d.ts +5 -4
  25. package/dist/esm/rechnung/texte.js +16 -8
  26. package/dist/esm/rechnung/typen.d.ts +7 -0
  27. package/dist/esm/rechnung/vertrag.d.ts +7 -4
  28. package/dist/esm/rechnung/vertrag.js +14 -4
  29. package/fixtures/hobex-hps-codes.json +4075 -52
  30. package/fixtures/kasse-texte.json +1 -1
  31. package/fixtures/oberflaeche.json +1 -1
  32. package/fixtures/rechnung-api-beispiele/zahlung-fehler-vor-ort.json +15 -0
  33. package/fixtures/rechnung-api.schema.json +7 -1
  34. package/fixtures/rechnung-texte.json +11 -9
  35. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,64 @@ Was vor 0.7.0 geschah, steht in der Commit-Historie (`git log`); ab hier wird
4
4
  es hier geführt. Ein Eintrag nennt die Änderung **und ihren Grund** —
5
5
  nur der Grund überlebt den nächsten Umbau.
6
6
 
7
+ ## 0.20.0
8
+
9
+ ### Rechnung: richtige Steuerhinweise, Barumsatz auch mit Karte
10
+
11
+ **Anlass:** drei Befunde aus einer Durchsicht an den Primärquellen.
12
+
13
+ - **Reverse Charge ist keine Steuerbefreiung.** Der Aufdruck hiess
14
+ „Steuerfreie Leistung – Reverse Charge" und behauptete damit eine Befreiung,
15
+ die es nicht gibt: der Umsatz bleibt steuerpflichtig, nur die Steuer schuldet
16
+ der Empfänger. Das Gesetz trennt beides (§ 11 Abs. 1 Z 3 lit. e = Hinweis auf
17
+ eine Befreiung, § 11 Abs. 1a = Hinweis auf die Steuerschuldnerschaft); Art. 226
18
+ Nr. 11a MwSt-RL nennt den Begriff wörtlich. Jetzt:
19
+ „Steuerschuldnerschaft des Leistungsempfängers."
20
+ - **Ig. Lieferung** bekommt denselben Kasten wie Reverse Charge, mit **beiden**
21
+ UID-Nummern — Art. 11 Abs. 2 UStG verlangt sie auf der Rechnung. Der
22
+ Textschlüssel `steuer.reverseCharge.uid` heisst darum jetzt `steuer.uidZeile`,
23
+ neu sind `steuer.igLieferung.titel` und `steuer.igLieferung.text`
24
+ (`steuer.igLieferung` entfällt).
25
+ - **`onSite` an der Zahlung.** Ein Barumsatz ist nicht nur Bargeld: als
26
+ Barzahlung gilt auch die Karte **vor Ort** (§ 131b Abs. 1 Z 3 UStG), nicht
27
+ aber dieselbe Karte im Internet. Weil `card`/`online` beides sein können,
28
+ sagt es das Fremdsystem jetzt selbst — der Hinweis `cash_receipt_required`
29
+ kommt bei `cash` immer und sonst nur mit `onSite: true`. `transfer` mit
30
+ `onSite` wird abgewiesen.
31
+
32
+ ## 0.19.0
33
+
34
+ ### hobex HPS: TECS-Antwortcodes eingeordnet, Storno nach ausbleibender Host-Antwort
35
+
36
+ **Anlass:** hobex hat am 16.09.2026 bestätigt, dass das HPS die Codes der
37
+ TECS-Plattform durchreicht, und deren Liste geschickt. Zu `9908` (im Betrieb am
38
+ 11.09.2026 gesehen, bisher unbekannt und damit offen): „ein Timeout wie jeder
39
+ andere. Richtigerweise sollte in dem Fall ein Storno nachgeschickt werden.“
40
+
41
+ - `HPS_CODES` führt jetzt 365 Codes; die TECS-Liste steht in
42
+ `src/payments/hobex-hps/tecs-codes.ts`, eine Zeile je Code.
43
+ - `sendReversal` je Code und `needsReversal()`: bei ausbleibender oder
44
+ unbrauchbarer Host-Antwort schickt `pay` genau einmal ein Storno
45
+ (`/v1/terminal/cancel`) nach. Quittiert → `declined` mit
46
+ `'voidedAfterHostFault'`; sonst Klärung wie bisher, ein späteres `9011`
47
+ zählt ebenfalls als storniert. Gutschriften bekommen kein Storno (TECS:
48
+ `9031`).
49
+ - `tecsTitle` je Code: der Titel in der TECS-Liste.
50
+ - `normalizeHpsCode()`: `0055` und `55` sind derselbe Code; `responseCode`
51
+ kommt normalisiert an (`0000` → `0`), `raw` bleibt unverändert. Familien mit
52
+ Platzhalter (`81xx`) werden gefunden.
53
+ - 16 neue Gründe in `HpsCodeReason` und `HPS_REASON_HINTS`, darunter
54
+ `issuerDeclined`, `insufficientFunds`, `cardExpired`, `hostTimeout`,
55
+ `approvedWithCondition`. **Kassen mit eigener Übersetzung brauchen dafür
56
+ Texte.**
57
+ - Eine Genehmigung, die nicht `0` ist (`8`, `10`, `11`, `16`, `32`), bleibt
58
+ offen und wird nie zur Ablehnung.
59
+ - `9900` ist jetzt `hostUncertain` (`internalError`) statt `noStatement`:
60
+ gemessen kam er nach verarbeiteter Karte, und die Zwei-9027-Regel hätte
61
+ daraus „nichts belastet“ gemacht.
62
+ - `fixtures/hobex-hps-codes.json` führt je Code zusätzlich `sendReversal` und
63
+ `tecsTitle`.
64
+
7
65
  ## 0.18.0
8
66
 
9
67
  ### Rechnungs-API: bezahlte Rechnung und Zahlungen nachtragen
package/README.md CHANGED
@@ -545,10 +545,23 @@ await rechnungen.recordInvoicePayment({
545
545
  ```
546
546
 
547
547
  `reference` wird gespeichert, aber **nicht gedruckt**; Kartendaten gehören
548
- ohnehin nicht auf eine Rechnung. Bei `method: 'cash'` wird die Zahlung gebucht
549
- und die Antwort trägt zusätzlich `notice.code = 'cash_receipt_required'`: eine
550
- Barzahlung ist ein Barumsatz und braucht einen Beleg (§ 132a BAO), bei
551
- Registrierkassenpflicht über die Kasse — der Vermerk hier ersetzt ihn nicht.
548
+ ohnehin nicht auf eine Rechnung.
549
+
550
+ **Barumsatz ist nicht nur Bargeld.** Als Barzahlung gilt auch die Karte **vor
551
+ Ort** an der Kasse (§ 131b Abs. 1 Z 3 UStG) — dieselbe Karte im Internet
552
+ dagegen nicht. Weil `card` und `online` beides sein können, sagt es das
553
+ Fremdsystem selbst:
554
+
555
+ ```ts
556
+ payment: { method: 'card', onSite: true } // Terminal an der Kasse
557
+ payment: { method: 'card' } // Kartenzahlung im Shop
558
+ ```
559
+
560
+ Bei `cash` (immer) und bei `onSite: true` trägt die Antwort
561
+ `notice.code = 'cash_receipt_required'`: ein Barumsatz braucht einen Beleg
562
+ (§ 132a BAO), bei Registrierkassenpflicht über die Registrierkasse — der
563
+ Vermerk an der Rechnung ersetzt ihn nicht. `transfer` mit `onSite` ist ein
564
+ Feldfehler, eine Überweisung erfolgt nicht vor Ort.
552
565
 
553
566
  **Nach einem Zeitlimit mit demselben `idempotencyKey` wiederholen**, nie mit
554
567
  einem neuen: dann kommt die schon ausgestellte Rechnung zurück
@@ -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';
@@ -20,7 +20,7 @@
20
20
  */
21
21
  Object.defineProperty(exports, "__esModule", { value: true });
22
22
  exports.isHostUncertain = exports.hpsCodeReason = exports.hpsCodeInfo = exports.UNSUPPORTED_USER_DATA_CODE = exports.TIP_SELECTION_FAILED_CODE = exports.TERMINAL_BUSY_CODE = exports.TERMINAL_BLOCKED_CODE = exports.SCEP_ENROLLMENT_FAILED_CODE = exports.REFUND_PASSWORD_INVALID_CODE = exports.REFUND_DISABLED_CODE = exports.PASSWORD_NOT_ENTERED_CODE = exports.NOT_FOUND_CODE = exports.MAX_RETRIES_EXCEEDED_CODE = exports.INVALID_TX_TYPE_CODE = exports.INVALID_TID_DOCUMENTED_CODE = exports.INVALID_MESSAGE_TYPE_CODE = exports.INTERNAL_ERROR_CODE = exports.HPS_REASON_HINTS = exports.HPS_MEASURED_CODES = exports.HPS_CODES = exports.HOST_TIMEOUT_REVERSED_CODE = exports.HOST_STEP_FAILED_CODE = exports.HOST_COMMUNICATION_FAILED_CODE = exports.DIAGNOSIS_FAILED_CODE = exports.COMPLETION_FAILED_CODE = exports.CHIP_DATA_MISMATCH_CODE = exports.CARD_READ_FAILED_CODE = exports.CARD_NOT_SUPPORTED_CODE = exports.CARD_NOT_PRESENT_CODE = exports.CARD_INFO_NOT_ENTERED_CODE = exports.CARD_DECLINED_CODE = exports.BAD_REQUEST_CODE = exports.APPROVED_CODE = exports.APP_SELECT_FAILED_CODE = exports.ABORTED_CODE = exports.newHpsTransactionId = exports.isValidHpsTransactionId = exports.createHpsTransactionIdGenerator = exports.MAX_TRANSACTION_ID_LENGTH = exports.createHpsPayments = exports.mayRetrySafely = exports.isHostUncertainResult = exports.PREFLIGHT_CONNECT_CODES = exports.HpsTransactionIdError = exports.HpsPreflightError = exports.HpsConnectTransportError = exports.HpsConnectTerminalError = exports.HpsConnectException = exports.HpsClarifyTimeoutError = exports.createHpsConnectClient = void 0;
23
- exports.hobexReceiptFromHps = exports.parseHpsTransactionResponse = exports.isUnknownCode = exports.isTechnicalError = exports.isNotAbortable = exports.isNoStatement = exports.isInProgress = exports.isConclusiveAsStatus = exports.isConclusive = exports.isCanceled = exports.isApproved = exports.TRANSACTION_CANCELED_CODE = exports.TERMINAL_BUSY_HTTP_STATUS = exports.NOT_FOUND_HTTP_STATUS = exports.TECHNICAL_ERROR_CODE = exports.NO_STATEMENT_CODE = exports.WRONG_PIN_CODE = exports.INVALID_TID_CODE = exports.AMOUNT_OUT_OF_RANGE_CODE = exports.INVALID_AMOUNT_CODE = exports.NOT_ABORTABLE_CODE = exports.INVALID_TRANSACTION_CODE = exports.isHostUncertainReason = void 0;
23
+ exports.hobexReceiptFromHps = exports.parseHpsTransactionResponse = exports.isUnknownCode = exports.isTechnicalError = exports.normalizeHpsCode = exports.needsReversal = exports.isNotAbortable = exports.isNoStatement = exports.isInProgress = exports.isConclusiveAsStatus = exports.isConclusive = exports.isCanceled = exports.isApproved = exports.TRANSACTION_CANCELED_CODE = exports.TERMINAL_BUSY_HTTP_STATUS = exports.NOT_FOUND_HTTP_STATUS = exports.TECHNICAL_ERROR_CODE = exports.NO_STATEMENT_CODE = exports.WRONG_PIN_CODE = exports.INVALID_TID_CODE = exports.AMOUNT_OUT_OF_RANGE_CODE = exports.INVALID_AMOUNT_CODE = exports.NOT_ABORTABLE_CODE = exports.INVALID_TRANSACTION_CODE = exports.isHostUncertainReason = void 0;
24
24
  var connect_client_js_1 = require("./connect-client.js");
25
25
  Object.defineProperty(exports, "createHpsConnectClient", { enumerable: true, get: function () { return connect_client_js_1.createHpsConnectClient; } });
26
26
  var errors_js_1 = require("./errors.js");
@@ -96,6 +96,8 @@ Object.defineProperty(exports, "isConclusiveAsStatus", { enumerable: true, get:
96
96
  Object.defineProperty(exports, "isInProgress", { enumerable: true, get: function () { return transaction_response_js_1.isInProgress; } });
97
97
  Object.defineProperty(exports, "isNoStatement", { enumerable: true, get: function () { return transaction_response_js_1.isNoStatement; } });
98
98
  Object.defineProperty(exports, "isNotAbortable", { enumerable: true, get: function () { return transaction_response_js_1.isNotAbortable; } });
99
+ Object.defineProperty(exports, "needsReversal", { enumerable: true, get: function () { return transaction_response_js_1.needsReversal; } });
100
+ Object.defineProperty(exports, "normalizeHpsCode", { enumerable: true, get: function () { return transaction_response_js_1.normalizeHpsCode; } });
99
101
  Object.defineProperty(exports, "isTechnicalError", { enumerable: true, get: function () { return transaction_response_js_1.isTechnicalError; } });
100
102
  Object.defineProperty(exports, "isUnknownCode", { enumerable: true, get: function () { return transaction_response_js_1.isUnknownCode; } });
101
103
  Object.defineProperty(exports, "parseHpsTransactionResponse", { enumerable: true, get: function () { return transaction_response_js_1.parseHpsTransactionResponse; } });
@@ -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. */
@@ -6,6 +6,11 @@ const transaction_id_js_1 = require("./transaction-id.js");
6
6
  const transaction_response_js_1 = require("./transaction-response.js");
7
7
  /** Teiler, mit dem das Abbruchbudget aus [HpsPaymentsOptions.resolveBudgetMs] entsteht. */
8
8
  const ABORT_BUDGET_DIVISOR = 6;
9
+ /**
10
+ * Teiler fuer das Budget des nachgeschickten Stornos: ein Drittel, bei der
11
+ * Vorgabe 30 s. Ein Storno geht ueber den Host und darf Sekunden brauchen.
12
+ */
13
+ const REVERSAL_BUDGET_DIVISOR = 3;
9
14
  function createHpsPayments(client, target, options = {}) {
10
15
  const resolveBudgetMs = options.resolveBudgetMs ?? 90_000;
11
16
  const maxBackoffMs = options.maxBackoffMs ?? 10_000;
@@ -14,6 +19,7 @@ function createHpsPayments(client, target, options = {}) {
14
19
  const now = options.now ?? Date.now;
15
20
  const observer = options.observer;
16
21
  const abortBudgetMs = Math.trunc(resolveBudgetMs / ABORT_BUDGET_DIVISOR);
22
+ const reversalBudgetMs = Math.trunc(resolveBudgetMs / REVERSAL_BUDGET_DIVISOR);
17
23
  function emit(kind, message, transactionId) {
18
24
  if (!observer)
19
25
  return;
@@ -70,8 +76,12 @@ function createHpsPayments(client, target, options = {}) {
70
76
  const info = (0, transaction_response_js_1.hpsCodeInfo)(res.responseCode);
71
77
  const was = info.reason === 'internalError'
72
78
  ? 'interner Fehler des Terminals'
73
- : 'Stoerung zwischen Terminal und hobex-Host, das Terminal storniert nicht selbst';
74
- return `Terminal meldet ${was} (${info.code} "${info.title}")`;
79
+ : info.reason === 'approvedWithCondition'
80
+ ? 'Genehmigung mit Vorbehalt'
81
+ : info.reason === 'hostTimeout'
82
+ ? 'keine rechtzeitige Antwort des hobex-Hosts, das Terminal storniert nicht selbst'
83
+ : 'Stoerung zwischen Terminal und hobex-Host, das Terminal storniert nicht selbst';
84
+ return `Terminal meldet ${was} (${res.responseCode} "${info.title}")`;
75
85
  }
76
86
  /**
77
87
  * Der Grund eines offenen Ausgangs. [antwort] ist die Antwort auf die
@@ -344,6 +354,37 @@ function createHpsPayments(client, target, options = {}) {
344
354
  emit('resolved', steps[steps.length - 1], id);
345
355
  return { outcome: 'declined', transactionId: id, response: res, reason: 'aborted', steps: [...steps] };
346
356
  }
357
+ /**
358
+ * Schickt zu einer Zahlung, deren Host-Antwort ausblieb ([anlass] traegt
359
+ * `sendReversal`), GENAU EINMAL ein Storno nach. Ergebnis nur, wenn das
360
+ * Terminal es mit `'0'` quittiert; sonst `null`. Ohne [amountCents]
361
+ * (Gutschrift) wird nichts gesendet.
362
+ */
363
+ async function sendReversal(id, steps, elapsedMs, amountCents, anlass) {
364
+ if (amountCents === undefined) {
365
+ steps.push('Kein Storno nachgeschickt -- die Aufhebung einer Gutschrift laesst TECS nicht zu');
366
+ return null;
367
+ }
368
+ emit('resolving', `Storno wird nachgeschickt (${anlass.responseCode})`, id);
369
+ let res;
370
+ try {
371
+ res = await withinBudget(elapsedMs, () => client.cancel({ ...target, transactionId: id, amountCents }), reversalBudgetMs);
372
+ }
373
+ catch (e) {
374
+ steps.push(`Storno nachgeschickt, aber nicht bestaetigt (${describe(e)}) -- ob es wirkte, ist offen, Ausgang wird abgefragt`);
375
+ noteUnexpected(e, id);
376
+ return null;
377
+ }
378
+ if (!(0, transaction_response_js_1.isApproved)(res)) {
379
+ const titel = (0, transaction_response_js_1.hpsCodeInfo)(res.responseCode)?.title;
380
+ steps.push(`Storno nachgeschickt, nicht bestaetigt (${res.responseCode ?? 'ohne Ergebniscode'}`
381
+ + `${titel === undefined ? '' : ` "${titel}"`}) -- Ausgang wird abgefragt`);
382
+ return null;
383
+ }
384
+ steps.push('Storno nachgeschickt und bestaetigt -- die Zahlung ist aufgehoben, es ist nichts belastet');
385
+ emit('resolved', steps[steps.length - 1], id);
386
+ return { outcome: 'declined', transactionId: id, response: res, reason: 'voidedAfterHostFault', steps: [...steps] };
387
+ }
347
388
  /**
348
389
  * Klaert einen offenen Ausgang: erst abbrechen, dann abfragen -- bis das
349
390
  * Terminal etwas sagt oder das Budget aufgebraucht ist.
@@ -356,7 +397,7 @@ function createHpsPayments(client, target, options = {}) {
356
397
  * ([isHostUncertain]), bekommt `9027` diesen Aussagewert gerade NICHT --
357
398
  * siehe Klassendoku, "Stoerung beim Host".
358
399
  */
359
- async function resolve(id, steps, antwort) {
400
+ async function resolve(id, steps, antwort, stornoCents) {
360
401
  emit('resolving', 'Ausgang offen, Klaerung laeuft', id);
361
402
  const start = now();
362
403
  const elapsedMs = () => now() - start;
@@ -365,6 +406,9 @@ function createHpsPayments(client, target, options = {}) {
365
406
  // Statusabfrage sie nennt und danach 9027 kommt.
366
407
  let stoerungsAntwort = antwort && (0, transaction_response_js_1.isHostUncertain)(antwort) ? antwort : undefined;
367
408
  let letzteAntwort = antwort;
409
+ // Das Storno wird hoechstens einmal erwogen; gesendet ist es nur mit Betrag.
410
+ let stornoErwogen = false;
411
+ let stornoGesendet = false;
368
412
  if (stoerungsAntwort === undefined) {
369
413
  const aborted = await tryAbort(id, steps, elapsedMs);
370
414
  if (aborted)
@@ -375,6 +419,13 @@ function createHpsPayments(client, target, options = {}) {
375
419
  // Host. Ein quittierter Abbruch bewiese nur, dass am Terminal nichts mehr
376
420
  // laeuft, nicht, dass der Host nichts belastet hat.
377
421
  steps.push('Kein Abbruchversuch -- das Terminal hat den Vorgang mit einer Stoerung beim hobex-Host beendet');
422
+ if ((0, transaction_response_js_1.needsReversal)(stoerungsAntwort)) {
423
+ stornoErwogen = true;
424
+ stornoGesendet = stornoCents !== undefined;
425
+ const storniert = await sendReversal(id, steps, elapsedMs, stornoCents, stoerungsAntwort);
426
+ if (storniert)
427
+ return storniert;
428
+ }
378
429
  }
379
430
  let wait = 0;
380
431
  let transportFailures = 0;
@@ -409,6 +460,24 @@ function createHpsPayments(client, target, options = {}) {
409
460
  if ((0, transaction_response_js_1.isHostUncertain)(status))
410
461
  stoerungsAntwort ??= status;
411
462
  const hostUngewiss = stoerungsAntwort !== undefined;
463
+ if (stornoGesendet && (0, transaction_response_js_1.isCanceled)(status)) {
464
+ steps.push(`Status: Zahlung aufgehoben (${transaction_response_js_1.TRANSACTION_CANCELED_CODE}) -- das nachgeschickte Storno hat gegriffen, `
465
+ + 'es ist nichts belastet');
466
+ emit('resolved', steps[steps.length - 1], id);
467
+ return { outcome: 'declined', transactionId: id, response: status, reason: 'voidedAfterHostFault', steps: [...steps] };
468
+ }
469
+ if (!stornoErwogen && (0, transaction_response_js_1.needsReversal)(status)) {
470
+ // Die Zahlung selbst kam ohne Antwort zurueck, erst die Statusabfrage
471
+ // nennt die ausgebliebene Host-Antwort -- dasselbe Storno wie oben.
472
+ steps.push(statusOhneErgebnis(status));
473
+ stornoErwogen = true;
474
+ stornoGesendet = stornoCents !== undefined;
475
+ const storniert = await sendReversal(id, steps, elapsedMs, stornoCents, status);
476
+ if (storniert)
477
+ return storniert;
478
+ wait = nextWait(wait);
479
+ continue;
480
+ }
412
481
  // Auf die Statusabfrage entscheidet nur ein Code, der den gesuchten
413
482
  // Vorgang beschreibt -- nicht einer, der diese Abfrage abweist
414
483
  // ([isConclusiveAsStatus]). Nach einer Stoerung beim Host entscheidet nur
@@ -610,7 +679,7 @@ function createHpsPayments(client, target, options = {}) {
610
679
  return settled;
611
680
  steps.push(offeneAntwort(res));
612
681
  }
613
- return resolve(id, steps, res);
682
+ return resolve(id, steps, res, paymentOptions.amountCents);
614
683
  }
615
684
  /**
616
685
  * 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[];