@kreiseck/kasseneck-api 0.17.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.
Files changed (50) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +26 -0
  3. package/dist/cjs/client/aufrufe.d.ts +1 -1
  4. package/dist/cjs/client/aufrufe.js +1 -0
  5. package/dist/cjs/payments/hobex-hps/index.d.ts +1 -1
  6. package/dist/cjs/payments/hobex-hps/index.js +3 -1
  7. package/dist/cjs/payments/hobex-hps/payments.d.ts +11 -0
  8. package/dist/cjs/payments/hobex-hps/payments.js +73 -4
  9. package/dist/cjs/payments/hobex-hps/tecs-codes.d.ts +16 -0
  10. package/dist/cjs/payments/hobex-hps/tecs-codes.js +366 -0
  11. package/dist/cjs/payments/hobex-hps/transaction-response.d.ts +56 -8
  12. package/dist/cjs/payments/hobex-hps/transaction-response.js +135 -65
  13. package/dist/cjs/rechnung/api.d.ts +3 -1
  14. package/dist/cjs/rechnung/api.js +1 -0
  15. package/dist/cjs/rechnung/endpunkte.d.ts +11 -1
  16. package/dist/cjs/rechnung/endpunkte.js +22 -0
  17. package/dist/cjs/rechnung/index.d.ts +3 -3
  18. package/dist/cjs/rechnung/index.js +4 -1
  19. package/dist/cjs/rechnung/typen.d.ts +48 -1
  20. package/dist/cjs/rechnung/vertrag.d.ts +24 -2
  21. package/dist/cjs/rechnung/vertrag.js +43 -1
  22. package/dist/esm/client/aufrufe.d.ts +1 -1
  23. package/dist/esm/client/aufrufe.js +1 -0
  24. package/dist/esm/payments/hobex-hps/index.d.ts +1 -1
  25. package/dist/esm/payments/hobex-hps/index.js +1 -1
  26. package/dist/esm/payments/hobex-hps/payments.d.ts +11 -0
  27. package/dist/esm/payments/hobex-hps/payments.js +74 -5
  28. package/dist/esm/payments/hobex-hps/tecs-codes.d.ts +16 -0
  29. package/dist/esm/payments/hobex-hps/tecs-codes.js +363 -0
  30. package/dist/esm/payments/hobex-hps/transaction-response.d.ts +56 -8
  31. package/dist/esm/payments/hobex-hps/transaction-response.js +133 -65
  32. package/dist/esm/rechnung/api.d.ts +3 -1
  33. package/dist/esm/rechnung/api.js +2 -1
  34. package/dist/esm/rechnung/endpunkte.d.ts +11 -1
  35. package/dist/esm/rechnung/endpunkte.js +21 -0
  36. package/dist/esm/rechnung/index.d.ts +3 -3
  37. package/dist/esm/rechnung/index.js +2 -2
  38. package/dist/esm/rechnung/typen.d.ts +48 -1
  39. package/dist/esm/rechnung/vertrag.d.ts +24 -2
  40. package/dist/esm/rechnung/vertrag.js +42 -0
  41. package/fixtures/hobex-hps-codes.json +4075 -52
  42. package/fixtures/kasse-texte.json +1 -1
  43. package/fixtures/oberflaeche.json +16 -3
  44. package/fixtures/rechnung-api-beispiele/issue-bezahlt.json +32 -0
  45. package/fixtures/rechnung-api-beispiele/issue-fehler-zahlungsart.json +26 -0
  46. package/fixtures/rechnung-api-beispiele/zahlung-fehler-ohne-schluessel.json +14 -0
  47. package/fixtures/rechnung-api-beispiele/zahlung-nachtrag.json +14 -0
  48. package/fixtures/rechnung-api.schema.json +82 -2
  49. package/fixtures/rechnung-texte.json +1 -1
  50. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,69 @@ 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.19.0
8
+
9
+ ### hobex HPS: TECS-Antwortcodes eingeordnet, Storno nach ausbleibender Host-Antwort
10
+
11
+ **Anlass:** hobex hat am 16.09.2026 bestätigt, dass das HPS die Codes der
12
+ TECS-Plattform durchreicht, und deren Liste geschickt. Zu `9908` (im Betrieb am
13
+ 11.09.2026 gesehen, bisher unbekannt und damit offen): „ein Timeout wie jeder
14
+ andere. Richtigerweise sollte in dem Fall ein Storno nachgeschickt werden.“
15
+
16
+ - `HPS_CODES` führt jetzt 365 Codes; die TECS-Liste steht in
17
+ `src/payments/hobex-hps/tecs-codes.ts`, eine Zeile je Code.
18
+ - `sendReversal` je Code und `needsReversal()`: bei ausbleibender oder
19
+ unbrauchbarer Host-Antwort schickt `pay` genau einmal ein Storno
20
+ (`/v1/terminal/cancel`) nach. Quittiert → `declined` mit
21
+ `'voidedAfterHostFault'`; sonst Klärung wie bisher, ein späteres `9011`
22
+ zählt ebenfalls als storniert. Gutschriften bekommen kein Storno (TECS:
23
+ `9031`).
24
+ - `tecsTitle` je Code: der Titel in der TECS-Liste.
25
+ - `normalizeHpsCode()`: `0055` und `55` sind derselbe Code; `responseCode`
26
+ kommt normalisiert an (`0000` → `0`), `raw` bleibt unverändert. Familien mit
27
+ Platzhalter (`81xx`) werden gefunden.
28
+ - 16 neue Gründe in `HpsCodeReason` und `HPS_REASON_HINTS`, darunter
29
+ `issuerDeclined`, `insufficientFunds`, `cardExpired`, `hostTimeout`,
30
+ `approvedWithCondition`. **Kassen mit eigener Übersetzung brauchen dafür
31
+ Texte.**
32
+ - Eine Genehmigung, die nicht `0` ist (`8`, `10`, `11`, `16`, `32`), bleibt
33
+ offen und wird nie zur Ablehnung.
34
+ - `9900` ist jetzt `hostUncertain` (`internalError`) statt `noStatement`:
35
+ gemessen kam er nach verarbeiteter Karte, und die Zwei-9027-Regel hätte
36
+ daraus „nichts belastet“ gemacht.
37
+ - `fixtures/hobex-hps-codes.json` führt je Code zusätzlich `sendReversal` und
38
+ `tecsTitle`.
39
+
40
+ ## 0.18.0
41
+
42
+ ### Rechnungs-API: bezahlte Rechnung und Zahlungen nachtragen
43
+
44
+ **Anlass:** Die API wird vor allem von Online-Shops benutzt, die **vor** der
45
+ Rechnung kassieren. Bisher entstand immer eine offene Rechnung — mit Giro-QR
46
+ und Fälligkeit auf dem Blatt, obwohl das Geld längst da war.
47
+
48
+ - `issueInvoice` nimmt `payment` (`method`, optional `amountCents`, `paidAt`,
49
+ `reference`). Die Zahlung entsteht in **derselben** Transaktion wie das
50
+ Festschreiben; ohne `amountCents` gilt der volle Bruttobetrag.
51
+ - Neuer Aufruf `recordInvoicePayment` für später eintreffende Zahlungen, mit
52
+ **pflichtigem** `idempotencyKey` — ohne ihn bucht ein Wiederholungslauf nach
53
+ einem Zeitlimit eine zweite Zahlung.
54
+ - `INVOICE_PAYMENT_METHODS` (`transfer`, `card`, `online`, `cash`);
55
+ `Invoice.paidCents` und `Invoice.openCents`.
56
+ - Neue Fehlercodes `not_payable` (storniert oder Gutschrift) und
57
+ `payment_exceeds_invoice` (mit `remainingCents`).
58
+ - Neu: **Hinweise** an einer erfolgreichen Antwort (`INVOICE_NOTICE_CODES`,
59
+ `data.notice`). Bisheriger Fall: `cash_receipt_required`. Grund: eine
60
+ Barzahlung ist ein Barumsatz und braucht einen Beleg (§ 132a BAO), bei
61
+ Registrierkassenpflicht über die Registrierkasse — der Bezahlt-Vermerk ist
62
+ Buchhaltung und ersetzt ihn nicht. Die Rechnung selbst ist bei jeder
63
+ Zahlungsart erlaubt (§ 11 UStG), deshalb wird `cash` gebucht und nicht
64
+ abgewiesen.
65
+
66
+ `reference` wird gespeichert, aber nicht gedruckt: die Rechnung wird sieben
67
+ Jahre aufbewahrt und vervielfältigt, und dem Empfänger nützt die Zahlungs-ID
68
+ des Shops nichts.
69
+
7
70
  ## 0.17.0
8
71
 
9
72
  ### Rechnungs-API: Sprache (de/en) und Marke je Rechnung
package/README.md CHANGED
@@ -524,6 +524,32 @@ bzw. `pcs` —, und die E-Rechnung trägt den UN/ECE-Code aus
524
524
  `RECHNUNG_EINHEITEN_CODES` (`C62`, `HUR`, …). Freier Text wie `"Std"` ist
525
525
  `validation` mit Feld `items[0].unit`.
526
526
 
527
+ **Schon bezahlt?** Wer online kassiert und danach die Rechnung stellt, gibt die
528
+ Zahlung gleich mit: sie entsteht in derselben Transaktion wie das Festschreiben,
529
+ und das PDF trägt dann keinen Zahlungskasten und keinen Giro-QR.
530
+
531
+ ```ts
532
+ const { invoice } = await rechnungen.issueInvoice({
533
+ idempotencyKey: `bestellung-${bestellnummer}`,
534
+ customerId: kunde.id,
535
+ taxScheme: 'normal', priceMode: 'net', serviceStart: '2026-09-16',
536
+ items: [{ description: 'Beratung', quantity: 2, unitPriceCents: 5000, vatRate: 20, unit: 'hour' }],
537
+ payment: { method: 'card', reference: zahlung.id }, // ohne amountCents: voll bezahlt
538
+ }); // invoice.openCents === 0
539
+
540
+ // Trifft das Geld erst später ein (Überweisung, Teilzahlung):
541
+ await rechnungen.recordInvoicePayment({
542
+ idempotencyKey: `zahlung-${zahlung.id}`, // Pflicht: sonst bucht eine Wiederholung zweimal
543
+ invoiceId: invoice.id, method: 'transfer', amountCents: 12000, paidAt: '2026-09-20',
544
+ });
545
+ ```
546
+
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.
552
+
527
553
  **Nach einem Zeitlimit mit demselben `idempotencyKey` wiederholen**, nie mit
528
554
  einem neuen: dann kommt die schon ausgestellte Rechnung zurück
529
555
  (`replayed: true`). Derselbe Schlüssel mit anderen Daten ergibt
@@ -13,7 +13,7 @@
13
13
  * umhuellt, muss ihn weiterhin absetzen koennen.
14
14
  */
15
15
  import type { TransportBodyFields } from './transport.js';
16
- export declare const AUFRUFE: readonly ["activateCashregister", "cancelInvoice", "cancelReceipt", "createCreditNote", "createCustomer", "createCustomerCashregister", "createPartnerCustomer", "checkPartnerCustomerEmail", "createPartnerWebhook", "createPaymentLinkStripe", "createPrintJob", "createReceipt", "deletePartnerWebhook", "downloadDailyReport", "downloadReport", "endRegisterSession", "financeWebService", "generateFullReceiptId", "getCustomer", "getCustomerCredentials", "getCustomerSignatureStatus", "getFirstReceiptDate", "getInvoice", "getInvoicePdf", "getInvoiceSetupStatus", "getInvoiceXml", "getKasseSettings", "getPartnerCustomer", "getPartnerInfo", "getPrintJob", "getReceipt", "hobexPayApi", "hobexRefundApi", "issueInvoice", "listCustomerCashregisters", "listInvoices", "listBrands", "listMyArticleGroups", "listMyArticles", "listMyCashregisters", "listMyPrinters", "listMyReceipts", "listMyTipRecipients", "listPartnerCustomers", "listPartnerWebhookDeliveries", "listPartnerWebhooks", "listRegisterUsersForDevice", "listRegisterSessionsForDevice", "pairRegisterDevice", "registerPinLogin", "registerUserLogin", "renewRegisterSession", "requestCustomerSignature", "searchCustomers", "sendPartnerCustomerFonLink", "sendPartnerWebhookTest", "sendReceiptEmail", "setMyKasseSettings", "setMyRegisterDeviceSettings", "stripeCaptureIntent", "unpairRegisterDevice", "rotatePartnerWebhookSecret", "updateCustomer", "updatePartnerWebhook"];
16
+ export declare const AUFRUFE: readonly ["activateCashregister", "cancelInvoice", "cancelReceipt", "createCreditNote", "createCustomer", "createCustomerCashregister", "createPartnerCustomer", "checkPartnerCustomerEmail", "createPartnerWebhook", "createPaymentLinkStripe", "createPrintJob", "createReceipt", "deletePartnerWebhook", "downloadDailyReport", "downloadReport", "endRegisterSession", "financeWebService", "generateFullReceiptId", "getCustomer", "getCustomerCredentials", "getCustomerSignatureStatus", "getFirstReceiptDate", "getInvoice", "getInvoicePdf", "getInvoiceSetupStatus", "getInvoiceXml", "getKasseSettings", "getPartnerCustomer", "getPartnerInfo", "getPrintJob", "getReceipt", "hobexPayApi", "hobexRefundApi", "issueInvoice", "listCustomerCashregisters", "listInvoices", "listBrands", "listMyArticleGroups", "listMyArticles", "listMyCashregisters", "listMyPrinters", "listMyReceipts", "listMyTipRecipients", "listPartnerCustomers", "listPartnerWebhookDeliveries", "listPartnerWebhooks", "listRegisterUsersForDevice", "listRegisterSessionsForDevice", "pairRegisterDevice", "recordInvoicePayment", "registerPinLogin", "registerUserLogin", "renewRegisterSession", "requestCustomerSignature", "searchCustomers", "sendPartnerCustomerFonLink", "sendPartnerWebhookTest", "sendReceiptEmail", "setMyKasseSettings", "setMyRegisterDeviceSettings", "stripeCaptureIntent", "unpairRegisterDevice", "rotatePartnerWebhookSecret", "updateCustomer", "updatePartnerWebhook"];
17
17
  export type Aufruf = typeof AUFRUFE[number];
18
18
  /** Wie [KasseneckTransport], nur mit bekanntem Aufrufnamen. Nicht exportiert nach aussen. */
19
19
  export type InternerTransport = <T = unknown>(functionName: Aufruf, params?: Record<string, unknown>, extraBodyFields?: TransportBodyFields, secretParams?: readonly string[]) => Promise<T>;
@@ -51,6 +51,7 @@ exports.AUFRUFE = [
51
51
  'listRegisterUsersForDevice',
52
52
  'listRegisterSessionsForDevice',
53
53
  'pairRegisterDevice',
54
+ 'recordInvoicePayment',
54
55
  'registerPinLogin',
55
56
  'registerUserLogin',
56
57
  'renewRegisterSession',
@@ -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[];