@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
@@ -19,7 +19,7 @@
19
19
  * Nummer, Summen — ist bewusst **nicht** setzbar.
20
20
  */
21
21
  Object.defineProperty(exports, "__esModule", { value: true });
22
- exports.RECHNUNG_MINDESTENS_EINS = exports.RECHNUNG_GENAU_EINS = exports.RECHNUNG_ANFRAGEN = exports.POSITION_FELDER = exports.KUNDE_FELDER = exports.INVOICE_SETUP_REQUIREMENTS = exports.RECHNUNG_EINHEITEN_CODES = exports.INVOICE_UNITS = exports.INVOICE_LANGUAGES = exports.EINVOICE_FORMATS = exports.DOC_TYPES = exports.INVOICE_LIST_STATUS = exports.CUSTOMER_TYPES = exports.VAT_RATES = exports.PRICE_MODES = exports.TAX_SCHEMES = exports.CREDIT_NOTE_REASONS = exports.INVOICE_ERROR_CODES = exports.RECHNUNG_AUFRUFE = exports.RECHNUNG_VERTRAG_VERSION = void 0;
22
+ exports.RECHNUNG_MINDESTENS_EINS = exports.RECHNUNG_GENAU_EINS = exports.RECHNUNG_ANFRAGEN = exports.PAYMENT_FELDER = exports.POSITION_FELDER = exports.KUNDE_FELDER = exports.INVOICE_SETUP_REQUIREMENTS = exports.RECHNUNG_EINHEITEN_CODES = exports.INVOICE_UNITS = exports.INVOICE_NOTICE_CODES = exports.INVOICE_PAYMENT_METHODS = exports.INVOICE_LANGUAGES = exports.EINVOICE_FORMATS = exports.DOC_TYPES = exports.INVOICE_LIST_STATUS = exports.CUSTOMER_TYPES = exports.VAT_RATES = exports.PRICE_MODES = exports.TAX_SCHEMES = exports.CREDIT_NOTE_REASONS = exports.INVOICE_ERROR_CODES = exports.RECHNUNG_AUFRUFE = exports.RECHNUNG_VERTRAG_VERSION = void 0;
23
23
  exports.RECHNUNG_VERTRAG_VERSION = 1;
24
24
  exports.RECHNUNG_AUFRUFE = [
25
25
  'createCustomer',
@@ -35,6 +35,7 @@ exports.RECHNUNG_AUFRUFE = [
35
35
  'getInvoiceXml',
36
36
  'getInvoiceSetupStatus',
37
37
  'listBrands',
38
+ 'recordInvoicePayment',
38
39
  ];
39
40
  /**
40
41
  * Stabile Fehlercodes. Das Fremdsystem verzweigt am Code, der Text darf sich
@@ -60,6 +61,8 @@ exports.INVOICE_ERROR_CODES = [
60
61
  'invoice_setup_incomplete',
61
62
  'language_not_allowed',
62
63
  'brand_not_found',
64
+ 'not_payable',
65
+ 'payment_exceeds_invoice',
63
66
  ];
64
67
  /** Gruende einer Gutschrift. Der Server druckt den deutschen Anzeigetext. */
65
68
  exports.CREDIT_NOTE_REASONS = [
@@ -83,6 +86,18 @@ exports.EINVOICE_FORMATS = ['ubl', 'cii'];
83
86
  * oesterreichisch formatiert.
84
87
  */
85
88
  exports.INVOICE_LANGUAGES = ['de', 'en'];
89
+ /**
90
+ * Wie eine Rechnung bezahlt wurde. Der Vermerk ist Buchhaltung, kein Beleg:
91
+ * `cash` wird gebucht, die Antwort traegt dann zusaetzlich den Hinweis
92
+ * `cash_receipt_required` — eine Barzahlung ist ein Barumsatz und braucht
93
+ * einen Beleg (§ 132a BAO), bei Registrierkassenpflicht ueber die Kasse.
94
+ */
95
+ exports.INVOICE_PAYMENT_METHODS = ['transfer', 'card', 'online', 'cash'];
96
+ /**
97
+ * Hinweise, die eine erfolgreiche Antwort zusaetzlich tragen kann (`data.notice`).
98
+ * Sie sind keine Fehler: der Aufruf hat gewirkt, es gibt nur etwas zu wissen.
99
+ */
100
+ exports.INVOICE_NOTICE_CODES = ['cash_receipt_required'];
86
101
  /**
87
102
  * Einheiten einer Position. Die API nimmt nur diese Schluessel an; gedruckt
88
103
  * wird das Kuerzel in der Sprache der Rechnung (`RECHNUNG_TEXTE`,
@@ -264,6 +279,19 @@ exports.POSITION_FELDER = Object.freeze({
264
279
  discountPct: { typ: 'number', pflicht: false, min: 0, max: 100, nachkomma: 2 },
265
280
  });
266
281
  const positionen = { typ: 'list', pflicht: true, min: 1, max: 500, eintrag: { typ: 'object', pflicht: true, felder: exports.POSITION_FELDER } };
282
+ /**
283
+ * Eine Zahlung zu einer Rechnung. Ohne `amountCents` gilt der volle
284
+ * Bruttobetrag, ohne `paidAt` der heutige Wiener Tag. `reference` ist die
285
+ * Zahlungskennung des Fremdsystems (z. B. `pi_3Q...`) — sie wird gespeichert,
286
+ * aber **nicht gedruckt**: die Rechnung wird aufbewahrt und vervielfaeltigt,
287
+ * und dem Empfaenger nuetzt sie nichts.
288
+ */
289
+ exports.PAYMENT_FELDER = Object.freeze({
290
+ method: { typ: 'enum', pflicht: true, werte: exports.INVOICE_PAYMENT_METHODS },
291
+ amountCents: { typ: 'integer', pflicht: false, min: 1, max: 100_000_000 },
292
+ paidAt: datum(),
293
+ reference: text(100),
294
+ });
267
295
  exports.RECHNUNG_ANFRAGEN = Object.freeze({
268
296
  createCustomer: {
269
297
  customer: { typ: 'object', pflicht: true, felder: exports.KUNDE_FELDER },
@@ -305,6 +333,12 @@ exports.RECHNUNG_ANFRAGEN = Object.freeze({
305
333
  language: { typ: 'enum', pflicht: false, werte: exports.INVOICE_LANGUAGES },
306
334
  /** Marke (Kennung aus `listBrands`); sonst die Standardmarke. */
307
335
  brandId: id,
336
+ /**
337
+ * Schon bezahlt: die Zahlung entsteht in **derselben** Transaktion wie das
338
+ * Festschreiben. Sonst gaebe es einen Moment, in dem die Rechnung offen ist
339
+ * und ein sofort geholtes PDF Zahlungsinformationen traegt.
340
+ */
341
+ payment: { typ: 'object', pflicht: false, felder: exports.PAYMENT_FELDER },
308
342
  },
309
343
  cancelInvoice: {
310
344
  idempotencyKey: idempotencyKey(true),
@@ -343,6 +377,14 @@ exports.RECHNUNG_ANFRAGEN = Object.freeze({
343
377
  },
344
378
  getInvoiceSetupStatus: {},
345
379
  listBrands: {},
380
+ recordInvoicePayment: {
381
+ idempotencyKey: idempotencyKey(true),
382
+ invoiceId: idPflicht,
383
+ method: { typ: 'enum', pflicht: true, werte: exports.INVOICE_PAYMENT_METHODS },
384
+ amountCents: { typ: 'integer', pflicht: false, min: 1, max: 100_000_000 },
385
+ paidAt: datum(),
386
+ reference: text(100),
387
+ },
346
388
  });
347
389
  /** Genau eines dieser Felder muss gesetzt sein (je Aufruf, je Gruppe). */
348
390
  exports.RECHNUNG_GENAU_EINS = Object.freeze({
@@ -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>;
@@ -48,6 +48,7 @@ export const AUFRUFE = [
48
48
  'listRegisterUsersForDevice',
49
49
  'listRegisterSessionsForDevice',
50
50
  'pairRegisterDevice',
51
+ 'recordInvoicePayment',
51
52
  'registerPinLogin',
52
53
  'registerUserLogin',
53
54
  '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';
@@ -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[];