@kreiseck/kasseneck-api 1.0.0-rc.1 → 1.0.0-rc.2

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.
package/CHANGELOG.md CHANGED
@@ -104,13 +104,28 @@ an upgrade as long as `/v1` is served.
104
104
  means the operation may have happened: `dialect_mismatch`,
105
105
  `receipt_outcome_unknown`, `cancellation_outcome_unknown`,
106
106
  `response_translation_failed` (unless `details.handled === false`),
107
- `response_unreadable` (a signing call reported success but the response
108
- lacks the receipt, the reference or the remaining quantities), and on
109
- `createReceipt`, `cancelReceipt` and `financeWebService` a network error,
110
- timeout or HTTP 5xx after sending as well as HTTP 200 with the `/v3` marker
111
- but an empty, non-JSON or status-less body. Never retry those; read the
112
- result back. Reason: a retried receipt is a second signed receipt in the
113
- chain.
107
+ `response_unreadable` (a signing or money call reported success but the
108
+ response lacks the receipt, the reference, the remaining quantities, the
109
+ Hobex receipt or the captured payment intent), and on `createReceipt`,
110
+ `cancelReceipt`, `financeWebService`, `hobexPayApi`, `hobexRefundApi` and
111
+ `stripeCaptureIntent` a network error, timeout or HTTP 5xx after sending as
112
+ well as HTTP 200 with the `/v3` marker but an empty, non-JSON (also HTML) or
113
+ status-less body. Never retry those; read the result back. Reason: a
114
+ retried receipt is a second signed receipt in the chain.
115
+ - **Money calls have an unknown outcome too (1.0.0-rc.2).** `hobexPay`,
116
+ `hobexRefund` and `stripeCaptureIntent` joined the calls above. In rc.1 a
117
+ timeout, network error or HTTP 5xx on them was `'rejected'`, and a success
118
+ reply without a readable Hobex receipt or payment intent was a
119
+ `KasseneckValidationError` (`scope: 'response'`); now both are `'unknown'`
120
+ (the latter as `KasseneckApiError` `response_unreadable`), and HTML with the
121
+ `/v3` marker is `not-json` with `'unknown'` instead of `route_missing`. An
122
+ error envelope throws a `KasseneckApiError` with its `code`; `hobexRefund`
123
+ never returns `false`. **Callers must never retry these calls, and must not
124
+ pass a `fetch` that retries by itself or wrap the package's requests in a
125
+ retrying layer** (proxy, service worker, HTTP client with a retry policy).
126
+ Reason: an app that read `'rejected'` could charge or refund a card twice;
127
+ a silent resend below the package does the same without any error. Same
128
+ change as in the Dart twin `kasseneck_api` (no `RetryClient`).
114
129
  - **Strict validation before sending.** The 0.x payment fields
115
130
  (`paymentMethod`, `paymentMethodFromServer`, `creditCardProvider`,
116
131
  `cardPaymentId`, `cardPaymentData`) throw, also from plain JavaScript.
package/README.md CHANGED
@@ -427,9 +427,10 @@ silently. For a layout option of 0.x the message names its English successor.
427
427
  ### `outcome: 'unknown'`: never retry, look it up
428
428
 
429
429
  `KasseneckApiError`, `KasseneckHttpError` and `KasseneckNetworkError` carry
430
- `outcome`. `'rejected'` means no signing operation is left open: no receipt was
431
- signed and nothing went to FinanzOnline, so retrying the same request does not
432
- create a second receipt (whether it helps depends on the code). `'unknown'` means the operation **may have been carried out**, for
430
+ `outcome`. `'rejected'` means no operation with an effect is left open: no
431
+ receipt was signed, nothing went to FinanzOnline and no money moved, so
432
+ retrying the same request does not create a second receipt or charge (whether
433
+ it helps depends on the code). `'unknown'` means the operation **may have been carried out**, for
433
434
  `createReceipt` a signed receipt in the chain. Then never send it again: read
434
435
  the result back (`getReceipt`, `listMyReceipts`, the original of a
435
436
  cancellation) and continue from there. `isOutcomeUnknown(error)` covers all
@@ -437,18 +438,35 @@ three classes. The outcome is unknown for:
437
438
 
438
439
  - `dialect_mismatch`, `receipt_outcome_unknown`, `cancellation_outcome_unknown`;
439
440
  - `response_translation_failed`, unless `details.handled === false`;
440
- - `response_unreadable`: a signing call reported success, but the response
441
- lacks what the call promises (no receipt, no reference, no remaining
442
- quantities);
443
- - on the signing calls `createReceipt`, `cancelReceipt` and
444
- `financeWebService`: a network error or timeout after sending began, HTTP
441
+ - `response_unreadable`: a signing or money-moving call reported success,
442
+ but the response lacks what the call promises (no receipt, no reference, no
443
+ remaining quantities, no Hobex receipt, no captured payment intent);
444
+ - on the calls with an effect: the signing calls `createReceipt`,
445
+ `cancelReceipt` and `financeWebService`, and the money calls `hobexPay`
446
+ (`hobexPayApi`, charges a card), `hobexRefund` (`hobexRefundApi`) and
447
+ `stripeCaptureIntent`: a network error or timeout after sending began, HTTP
445
448
  5xx, and HTTP 200 with the `Kasseneck-Api-Version: v3` marker but an empty,
446
449
  non-JSON (also `text/html`) or status-less body (`KasseneckHttpError`,
447
450
  `reason` `empty-body`, `not-json` or `missing-status`). HTML without the
448
451
  marker stays `route_missing` with `'rejected'`: no function saw the call.
449
452
 
450
- `outcome` only covers signing. After a network error on `issueInvoice` an
451
- invoice may still have been issued; retry it with the same `idempotencyKey`.
453
+ On the money calls an unknown outcome means the card may have been charged,
454
+ the refund may have gone through, the payment may have been captured. A
455
+ rejection of `hobexRefund` throws a `KasseneckApiError` with its `code`; it
456
+ never returns `false`, because a `false` on an unclear outcome invites a
457
+ second refund.
458
+
459
+ **Never retry these six calls, and never put a retrying layer under the
460
+ package.** A `fetch` passed in `options.fetch` (or a proxy or service worker in
461
+ front of it) must not resend a request by itself after a network error, a
462
+ timeout or a 5xx: a silent second send is a second signed receipt, a second
463
+ charge or a second refund, and the package cannot see it. Look the result up
464
+ instead (`getReceipt`, `listMyReceipts`, the Hobex transaction by its
465
+ `transactionId`, the Stripe session).
466
+
467
+ `outcome` only covers signing and the money calls. After a network error on
468
+ `issueInvoice` an invoice may still have been issued; retry it with the same
469
+ `idempotencyKey`.
452
470
 
453
471
  ```ts
454
472
  import { isOutcomeUnknown, paymentsExpectedCents } from '@kreiseck/kasseneck-api';
@@ -792,6 +810,11 @@ process:
792
810
  - **Hobex cloud** (`hobexPay`, `hobexRefund`): a terminal registered with
793
811
  Hobex, controlled over the network through the backend. Amounts are passed in
794
812
  cents; the package converts to euros for Hobex.
813
+
814
+ `hobexPay`, `hobexRefund` and `stripeCaptureIntent` move money. They are never
815
+ retried, neither by the app nor by a retrying `fetch` under the package: on
816
+ `isOutcomeUnknown(error)` look the payment up (see
817
+ [`outcome: 'unknown'`](#outcome-unknown-never-retry-look-it-up)).
795
818
  - **Hobex HPS via Kasseneck Connect**, described below.
796
819
 
797
820
  ### Hobex HPS via Kasseneck Connect
@@ -20,7 +20,7 @@
20
20
  * Single-Page-App hat geantwortet, der Aufruf kam nie an),
21
21
  * `dialect_mismatch` (Antwort ohne Kennzeichen `Kasseneck-Api-Version: v3`:
22
22
  * ein Rand ohne `/v3` hat geantwortet, **Ausgang unklar**) und
23
- * `response_unreadable` (ein signierender Aufruf meldete Erfolg, die
23
+ * `response_unreadable` (ein Aufruf mit Wirkung meldete Erfolg, die
24
24
  * Antwort traegt aber nicht, was er zusagt: **Ausgang unklar**).
25
25
  * **Entscheidend ist `outcome`:** `'rejected'` heisst abgelehnt, nichts
26
26
  * geschehen, Wiederholen hilft nicht. `'unknown'` heisst: der Vorgang kann
@@ -29,14 +29,16 @@
29
29
  * - `KasseneckHttpError` — die Antwort war **keine** verwertbare Huelle:
30
30
  * HTTP 500/404 ohne Huelle, leerer Rumpf oder Text statt JSON. Beim
31
31
  * Bericht-Download gelten dieselben Gruende fuer alles, was kein PDF ist.
32
- * `reason` trennt die Faelle maschinenlesbar. Auf einem signierenden
33
- * Aufruf hat HTTP 5xx `outcome: 'unknown'`, ebenso HTTP 200 mit
34
- * Kennzeichen, aber leerem oder unlesbarem Rumpf.
32
+ * `reason` trennt die Faelle maschinenlesbar. Auf einem Aufruf mit
33
+ * Wirkung (signierend oder geldbewegend) hat HTTP 5xx
34
+ * `outcome: 'unknown'`, ebenso HTTP 200 mit Kennzeichen, aber leerem oder
35
+ * unlesbarem Rumpf.
35
36
  * - `KasseneckNetworkError` — die Antwort kam gar nicht: Netz weg, DNS,
36
37
  * abgebrochene Verbindung oder Zeitueberschreitung (`timedOut`). Auch hier
37
38
  * gilt `outcome`: war die Anfrage schon unterwegs und ist der Aufruf einer
38
- * der signierenden (`createReceipt`, `cancelReceipt`, `financeWebService`),
39
- * ist er `'unknown'`.
39
+ * mit Wirkung (signierend: `createReceipt`, `cancelReceipt`,
40
+ * `financeWebService`; geldbewegend: `hobexPayApi`, `hobexRefundApi`,
41
+ * `stripeCaptureIntent`), ist er `'unknown'`.
40
42
  * - `KasseneckAuthError` — es kam nicht einmal zur Anfrage, weil die Anmeldung
41
43
  * scheiterte (fehlende Zugangsdaten, oder der Token-/Sitzungsgeber warf).
42
44
  * In der Browser-Kasse mit ihrer 90-Sekunden-Sitzung ist das Alltag, kein
@@ -111,7 +113,7 @@ export type ErrorOutcome = 'unknown' | 'rejected';
111
113
  /**
112
114
  * Codes, die das Paket selbst vergibt, nicht der Server: `route_missing`
113
115
  * (HTML statt Backend, der Aufruf kam nie an) und `response_unreadable`
114
- * (ein signierender Aufruf meldete Erfolg, die Antwort ist aber unlesbar;
116
+ * (ein Aufruf mit Wirkung meldete Erfolg, die Antwort ist aber unlesbar;
115
117
  * Ausgang unklar). `dialect_mismatch` vergibt das Paket ebenfalls, der Code
116
118
  * gehoert aber schon zum Rand des Servers (`errorCodes.edge`).
117
119
  */
@@ -142,7 +144,7 @@ export declare class KasseneckApiError extends Error {
142
144
  /**
143
145
  * `'unknown'` bei `dialect_mismatch`, `receipt_outcome_unknown`,
144
146
  * `cancellation_outcome_unknown`, `response_unreadable` (Erfolg gemeldet,
145
- * Antwort eines signierenden Aufrufs aber unlesbar) und
147
+ * Antwort eines Aufrufs mit Wirkung aber unlesbar) und
146
148
  * `response_translation_failed` (ausser mit `details.handled === false`);
147
149
  * sonst `'rejected'`. Bei `'unknown'` nie wiederholen, sondern nachlesen.
148
150
  */
@@ -162,8 +164,9 @@ export declare class KasseneckHttpError extends Error {
162
164
  /** Maschinenlesbarer Grund — trennt den Rewrite-Fall vom 500er ohne Textparsen. */
163
165
  readonly reason: HttpFailureReason;
164
166
  /**
165
- * `'unknown'` auf einem signierenden Aufruf (`createReceipt`,
166
- * `cancelReceipt`, `financeWebService`) bei HTTP 5xx und bei HTTP 200 mit
167
+ * `'unknown'` auf einem Aufruf mit Wirkung (`createReceipt`,
168
+ * `cancelReceipt`, `financeWebService`, `hobexPayApi`, `hobexRefundApi`,
169
+ * `stripeCaptureIntent`) bei HTTP 5xx und bei HTTP 200 mit
167
170
  * Kennzeichen, aber unlesbarem Rumpf (`empty-body`, `not-json` auch bei
168
171
  * `text/html`, `missing-status`): der Handler kann gelaufen sein, nie wiederholen,
169
172
  * sondern nachlesen. Sonst `'rejected'` (auch 4xx).
@@ -249,3 +252,16 @@ export declare function isKasseneckHttpError(error: unknown): error is Kasseneck
249
252
  export declare function isKasseneckNetworkError(error: unknown): error is KasseneckNetworkError;
250
253
  export declare function isKasseneckAuthError(error: unknown): error is KasseneckAuthError;
251
254
  export declare function isKasseneckValidationError(error: unknown): error is KasseneckValidationError;
255
+ /**
256
+ * Liest die Erfolgsantwort eines Aufrufs **mit Wirkung** (signierend oder
257
+ * geldbewegend). Scheitert das Lesen (fehlender Beleg, fehlender Bezug,
258
+ * unbrauchbares Feld oder ein Laufzeitfehler beim Umwandeln), hat der Server
259
+ * trotzdem Erfolg gemeldet: der Beleg ist signiert und im DEP, die Karte
260
+ * belastet bzw. der Einzug gelaufen. Das darf nie als gewoehnlicher Fehler
261
+ * enden, sonst kassiert die Kasse ein zweites Mal. Darum wird daraus
262
+ * `KasseneckApiError` mit Code `response_unreadable` und `outcome: 'unknown'`.
263
+ * Der Grund stammt vom Paket; aus der Antwort selbst wird nichts uebernommen.
264
+ *
265
+ * Paketintern (receipts.ts, payments/); nicht Teil der Paketoberflaeche.
266
+ */
267
+ export declare function signiertGelesen<T>(functionName: string, lesen: () => T): T;
@@ -21,7 +21,7 @@
21
21
  * Single-Page-App hat geantwortet, der Aufruf kam nie an),
22
22
  * `dialect_mismatch` (Antwort ohne Kennzeichen `Kasseneck-Api-Version: v3`:
23
23
  * ein Rand ohne `/v3` hat geantwortet, **Ausgang unklar**) und
24
- * `response_unreadable` (ein signierender Aufruf meldete Erfolg, die
24
+ * `response_unreadable` (ein Aufruf mit Wirkung meldete Erfolg, die
25
25
  * Antwort traegt aber nicht, was er zusagt: **Ausgang unklar**).
26
26
  * **Entscheidend ist `outcome`:** `'rejected'` heisst abgelehnt, nichts
27
27
  * geschehen, Wiederholen hilft nicht. `'unknown'` heisst: der Vorgang kann
@@ -30,14 +30,16 @@
30
30
  * - `KasseneckHttpError` — die Antwort war **keine** verwertbare Huelle:
31
31
  * HTTP 500/404 ohne Huelle, leerer Rumpf oder Text statt JSON. Beim
32
32
  * Bericht-Download gelten dieselben Gruende fuer alles, was kein PDF ist.
33
- * `reason` trennt die Faelle maschinenlesbar. Auf einem signierenden
34
- * Aufruf hat HTTP 5xx `outcome: 'unknown'`, ebenso HTTP 200 mit
35
- * Kennzeichen, aber leerem oder unlesbarem Rumpf.
33
+ * `reason` trennt die Faelle maschinenlesbar. Auf einem Aufruf mit
34
+ * Wirkung (signierend oder geldbewegend) hat HTTP 5xx
35
+ * `outcome: 'unknown'`, ebenso HTTP 200 mit Kennzeichen, aber leerem oder
36
+ * unlesbarem Rumpf.
36
37
  * - `KasseneckNetworkError` — die Antwort kam gar nicht: Netz weg, DNS,
37
38
  * abgebrochene Verbindung oder Zeitueberschreitung (`timedOut`). Auch hier
38
39
  * gilt `outcome`: war die Anfrage schon unterwegs und ist der Aufruf einer
39
- * der signierenden (`createReceipt`, `cancelReceipt`, `financeWebService`),
40
- * ist er `'unknown'`.
40
+ * mit Wirkung (signierend: `createReceipt`, `cancelReceipt`,
41
+ * `financeWebService`; geldbewegend: `hobexPayApi`, `hobexRefundApi`,
42
+ * `stripeCaptureIntent`), ist er `'unknown'`.
41
43
  * - `KasseneckAuthError` — es kam nicht einmal zur Anfrage, weil die Anmeldung
42
44
  * scheiterte (fehlende Zugangsdaten, oder der Token-/Sitzungsgeber warf).
43
45
  * In der Browser-Kasse mit ihrer 90-Sekunden-Sitzung ist das Alltag, kein
@@ -80,6 +82,7 @@ exports.isKasseneckHttpError = isKasseneckHttpError;
80
82
  exports.isKasseneckNetworkError = isKasseneckNetworkError;
81
83
  exports.isKasseneckAuthError = isKasseneckAuthError;
82
84
  exports.isKasseneckValidationError = isKasseneckValidationError;
85
+ exports.signiertGelesen = signiertGelesen;
83
86
  // Bezeichner-artig: Buchstabe vorn, danach nur Bezeichnerzeichen, hoechstens
84
87
  // 64 Zeichen. Das laesst `TypeError`, `ECONNREFUSED` und `auth/internal-error`
85
88
  // durch, aber keinen Freitext und kein ID-Token (~900 Zeichen).
@@ -236,7 +239,7 @@ function ausgangAusCode(code, details) {
236
239
  /**
237
240
  * Codes, die das Paket selbst vergibt, nicht der Server: `route_missing`
238
241
  * (HTML statt Backend, der Aufruf kam nie an) und `response_unreadable`
239
- * (ein signierender Aufruf meldete Erfolg, die Antwort ist aber unlesbar;
242
+ * (ein Aufruf mit Wirkung meldete Erfolg, die Antwort ist aber unlesbar;
240
243
  * Ausgang unklar). `dialect_mismatch` vergibt das Paket ebenfalls, der Code
241
244
  * gehoert aber schon zum Rand des Servers (`errorCodes.edge`).
242
245
  */
@@ -266,7 +269,7 @@ class KasseneckApiError extends Error {
266
269
  /**
267
270
  * `'unknown'` bei `dialect_mismatch`, `receipt_outcome_unknown`,
268
271
  * `cancellation_outcome_unknown`, `response_unreadable` (Erfolg gemeldet,
269
- * Antwort eines signierenden Aufrufs aber unlesbar) und
272
+ * Antwort eines Aufrufs mit Wirkung aber unlesbar) und
270
273
  * `response_translation_failed` (ausser mit `details.handled === false`);
271
274
  * sonst `'rejected'`. Bei `'unknown'` nie wiederholen, sondern nachlesen.
272
275
  */
@@ -301,8 +304,9 @@ class KasseneckHttpError extends Error {
301
304
  /** Maschinenlesbarer Grund — trennt den Rewrite-Fall vom 500er ohne Textparsen. */
302
305
  reason;
303
306
  /**
304
- * `'unknown'` auf einem signierenden Aufruf (`createReceipt`,
305
- * `cancelReceipt`, `financeWebService`) bei HTTP 5xx und bei HTTP 200 mit
307
+ * `'unknown'` auf einem Aufruf mit Wirkung (`createReceipt`,
308
+ * `cancelReceipt`, `financeWebService`, `hobexPayApi`, `hobexRefundApi`,
309
+ * `stripeCaptureIntent`) bei HTTP 5xx und bei HTTP 200 mit
306
310
  * Kennzeichen, aber unlesbarem Rumpf (`empty-body`, `not-json` auch bei
307
311
  * `text/html`, `missing-status`): der Handler kann gelaufen sein, nie wiederholen,
308
312
  * sondern nachlesen. Sonst `'rejected'` (auch 4xx).
@@ -426,3 +430,24 @@ function isKasseneckAuthError(error) {
426
430
  function isKasseneckValidationError(error) {
427
431
  return error instanceof KasseneckValidationError;
428
432
  }
433
+ /**
434
+ * Liest die Erfolgsantwort eines Aufrufs **mit Wirkung** (signierend oder
435
+ * geldbewegend). Scheitert das Lesen (fehlender Beleg, fehlender Bezug,
436
+ * unbrauchbares Feld oder ein Laufzeitfehler beim Umwandeln), hat der Server
437
+ * trotzdem Erfolg gemeldet: der Beleg ist signiert und im DEP, die Karte
438
+ * belastet bzw. der Einzug gelaufen. Das darf nie als gewoehnlicher Fehler
439
+ * enden, sonst kassiert die Kasse ein zweites Mal. Darum wird daraus
440
+ * `KasseneckApiError` mit Code `response_unreadable` und `outcome: 'unknown'`.
441
+ * Der Grund stammt vom Paket; aus der Antwort selbst wird nichts uebernommen.
442
+ *
443
+ * Paketintern (receipts.ts, payments/); nicht Teil der Paketoberflaeche.
444
+ */
445
+ function signiertGelesen(functionName, lesen) {
446
+ try {
447
+ return lesen();
448
+ }
449
+ catch (ursache) {
450
+ const grund = ursache instanceof KasseneckValidationError ? ursache.reason : 'Antwort nicht lesbar';
451
+ throw new KasseneckApiError(functionName, `Erfolg gemeldet, Antwort aber unlesbar (${grund}). Der Vorgang kann ausgefuehrt sein: nicht wiederholen, sondern nachlesen.`, {}, 'response_unreadable');
452
+ }
453
+ }
@@ -58,12 +58,12 @@ function receiptLayoutFromResult(result, options = {}) {
58
58
  */
59
59
  async function createReceipt(rufen, options) {
60
60
  const daten = await rufen('createReceipt', createReceiptParams(options));
61
- return signiertGelesen('createReceipt', () => belegAusHuelle(daten, 'createReceipt'));
61
+ return (0, errors_js_1.signiertGelesen)('createReceipt', () => belegAusHuelle(daten, 'createReceipt'));
62
62
  }
63
63
  /** Wie [createReceipt], liest aus derselben Antwort zusaetzlich die Firmendaten. */
64
64
  async function createReceiptWithCompany(rufen, options) {
65
65
  const daten = await rufen('createReceipt', createReceiptParams(options));
66
- return signiertGelesen('createReceipt', () => belegMitFirmaAusHuelle(daten, 'createReceipt'));
66
+ return (0, errors_js_1.signiertGelesen)('createReceipt', () => belegMitFirmaAusHuelle(daten, 'createReceipt'));
67
67
  }
68
68
  /**
69
69
  * Baut die Nutzlast von `createReceipt` und prueft die Eingabe. Wirft, bevor
@@ -199,7 +199,7 @@ async function cancelReceipt(transport, options) {
199
199
  if (zahlungen !== undefined)
200
200
  params.payments = zahlungen;
201
201
  const daten = await transport('cancelReceipt', params);
202
- return signiertGelesen('cancelReceipt', () => stornoAusHuelle(daten));
202
+ return (0, errors_js_1.signiertGelesen)('cancelReceipt', () => stornoAusHuelle(daten));
203
203
  }
204
204
  /** Liest die Storno-Antwort `{ receipt, cancellationOf, remaining }`. */
205
205
  function stornoAusHuelle(daten) {
@@ -685,24 +685,6 @@ function kartenanbieter(wert) {
685
685
  function eingabefehler(grund) {
686
686
  return new errors_js_1.KasseneckValidationError('createReceipt', grund, 'request');
687
687
  }
688
- /**
689
- * Liest die Erfolgsantwort eines **signierenden** Aufrufs. Scheitert das
690
- * Lesen (fehlender Beleg, fehlender Bezug, unbrauchbares Feld oder ein
691
- * Laufzeitfehler beim Umwandeln), hat der Server trotzdem Erfolg gemeldet:
692
- * der Beleg ist signiert und im DEP. Das darf nie als gewoehnlicher Fehler
693
- * enden, sonst kassiert die Kasse ein zweites Mal. Darum wird daraus
694
- * `KasseneckApiError` mit Code `response_unreadable` und `outcome: 'unknown'`.
695
- * Der Grund stammt vom Paket; aus der Antwort selbst wird nichts uebernommen.
696
- */
697
- function signiertGelesen(functionName, lesen) {
698
- try {
699
- return lesen();
700
- }
701
- catch (ursache) {
702
- const grund = ursache instanceof errors_js_1.KasseneckValidationError ? ursache.reason : 'Antwort nicht lesbar';
703
- throw new errors_js_1.KasseneckApiError(functionName, `Erfolg gemeldet, Antwort aber unlesbar (${grund}). Der Vorgang kann ausgefuehrt sein: nicht wiederholen, sondern nachlesen.`, {}, 'response_unreadable');
704
- }
705
- }
706
688
  /** Die Antwort meldete Erfolg, trug aber nicht, was der Aufruf zusagt. */
707
689
  function antwortfehler(functionName, grund) {
708
690
  return new errors_js_1.KasseneckValidationError(functionName, grund, 'response');
@@ -62,10 +62,22 @@ const KASSENECK_HOSTS = new Set(['api.kasseneck.at', 'kasse.kasseneck.at']);
62
62
  /** Die 1.x-Linie spricht nur `/v3`: jede Basis endet so (`/v3` oder `/api/v3`). */
63
63
  const V3_ENDE = /\/v3$/;
64
64
  /**
65
- * Aufrufe, die signieren bzw. bei FinanzOnline etwas ausloesen. Ein Netzfehler,
66
- * nachdem die Anfrage unterwegs war, laesst ihren Ausgang offen.
65
+ * Aufrufe mit Wirkung, die nie blind wiederholt werden duerfen: sie signieren
66
+ * (`createReceipt`, `cancelReceipt`), loesen bei FinanzOnline etwas aus
67
+ * (`financeWebService`) oder bewegen Geld (`hobexPayApi` belastet eine Karte,
68
+ * `hobexRefundApi` erstattet, `stripeCaptureIntent` zieht eine vorgemerkte
69
+ * Zahlung ein). Scheitert einer, nachdem die Anfrage unterwegs war (Netz,
70
+ * Zeitlimit, HTTP 5xx, unlesbare Erfolgsantwort, HTML mit Kennzeichen), ist
71
+ * sein Ausgang offen.
67
72
  */
68
- const SIGNIERENDE_AUFRUFE = new Set(['createReceipt', 'cancelReceipt', 'financeWebService']);
73
+ const UNKNOWN_OUTCOME_CALLS = new Set([
74
+ 'createReceipt',
75
+ 'cancelReceipt',
76
+ 'financeWebService',
77
+ 'hobexPayApi',
78
+ 'hobexRefundApi',
79
+ 'stripeCaptureIntent',
80
+ ]);
69
81
  /**
70
82
  * Produkte, die das Backend in `Kasseneck-Client` zaehlt (Positivliste,
71
83
  * Nachtrag §6); alles andere zaehlte dort als `ungueltig`. Das Paket weist
@@ -162,9 +174,9 @@ function createCore(options) {
162
174
  return ursache;
163
175
  }
164
176
  // Bis hierher kam keine verwertbare Antwort: Netz weg oder Zeitlimit.
165
- // Die Anfrage war schon unterwegs: bei einem signierenden Aufruf kann
166
- // der Beleg entstanden sein (Ausgang unklar, nachlesen).
167
- return new errors_js_1.KasseneckNetworkError(fehlerName, abbruch.signal.aborted, zeitlimitMs, (0, errors_js_1.causeDigest)(ursache, geheimnisse), SIGNIERENDE_AUFRUFE.has(functionName) ? 'unknown' : 'rejected');
177
+ // Die Anfrage war schon unterwegs: bei einem Aufruf mit Wirkung kann
178
+ // der Beleg bzw. die Zahlung entstanden sein (Ausgang unklar, nachlesen).
179
+ return new errors_js_1.KasseneckNetworkError(fehlerName, abbruch.signal.aborted, zeitlimitMs, (0, errors_js_1.causeDigest)(ursache, geheimnisse), UNKNOWN_OUTCOME_CALLS.has(functionName) ? 'unknown' : 'rejected');
168
180
  };
169
181
  const basis = basisFuer(functionName);
170
182
  const url = `${basis}/${encodeURIComponent(functionName)}`;
@@ -217,18 +229,18 @@ function createCore(options) {
217
229
  if (fehler)
218
230
  throw fehler;
219
231
  }
220
- // 5xx auf einem signierenden Aufruf: der Handler kann gelaufen sein.
221
- const ausgang = antwort.status >= 500 && SIGNIERENDE_AUFRUFE.has(functionName) ? 'unknown' : 'rejected';
232
+ // 5xx auf einem Aufruf mit Wirkung: der Handler kann gelaufen sein.
233
+ const ausgang = antwort.status >= 500 && UNKNOWN_OUTCOME_CALLS.has(functionName) ? 'unknown' : 'rejected';
222
234
  throw new errors_js_1.KasseneckHttpError(fehlerName, antwort.status, inhaltstyp, 'server-error', ausgang);
223
235
  }
224
236
  // HTTP 200 mit HTML: die Auffangregel der Single-Page-App hat den Aufruf
225
237
  // bedient, keine Function hat ihn gesehen (Nachtrag §5.4, R15).
226
238
  if (inhaltstyp !== undefined && /^\s*text\/html\b/i.test(inhaltstyp)) {
227
239
  // Mit Kennzeichen hat der `/v3`-Rand den Aufruf gesehen (etwa ein
228
- // Proxy, der nur den Inhaltstyp umschreibt). Bei einem signierenden
229
- // Aufruf kann der Beleg dann entstanden sein: unlesbarer Rumpf,
240
+ // Proxy, der nur den Inhaltstyp umschreibt). Bei einem Aufruf mit
241
+ // Wirkung kann der Beleg bzw. die Zahlung dann entstanden sein: unlesbarer Rumpf,
230
242
  // Ausgang unklar, statt `route_missing`.
231
- if (SIGNIERENDE_AUFRUFE.has(functionName) && traegtKennzeichen(antwort)) {
243
+ if (UNKNOWN_OUTCOME_CALLS.has(functionName) && traegtKennzeichen(antwort)) {
232
244
  throw new errors_js_1.KasseneckHttpError(fehlerName, antwort.status, inhaltstyp, 'not-json', 'unknown');
233
245
  }
234
246
  throw new errors_js_1.KasseneckApiError(fehlerName, 'Route fehlt: die Antwort ist eine HTML-Seite statt des Backends', {}, 'route_missing');
@@ -245,8 +257,8 @@ function createCore(options) {
245
257
  }
246
258
  // Ab hier kam HTTP 200 mit Kennzeichen: der `/v3`-Rand hat den Aufruf
247
259
  // gesehen. Ist der Rumpf dann unlesbar (gekuerzt von einem Proxy,
248
- // abgebrochene Verbindung), kann ein signierender Handler gelaufen sein.
249
- const unlesbar = SIGNIERENDE_AUFRUFE.has(functionName) ? 'unknown' : 'rejected';
260
+ // abgebrochene Verbindung), kann ein Handler mit Wirkung gelaufen sein.
261
+ const unlesbar = UNKNOWN_OUTCOME_CALLS.has(functionName) ? 'unknown' : 'rejected';
250
262
  return auswerten(koerper, fehlerName, antwort.status, inhaltstyp, geheimnisse, unlesbar);
251
263
  }
252
264
  finally {
@@ -45,6 +45,10 @@ export interface HobexTransactionIdOptions {
45
45
  *
46
46
  * **Der Endpunkt heisst `hobexPayApi`** — mit Suffix; ohne ihn gibt es ihn
47
47
  * nicht.
48
+ *
49
+ * **Nie wiederholen.** Bei `isOutcomeUnknown(e)` (Netz, Zeitlimit, HTTP 5xx,
50
+ * unlesbare Antwort) kann die Karte belastet sein: den Stand ueber die
51
+ * Transaktionskennung nachlesen, nicht ein zweites Mal belasten.
48
52
  */
49
53
  export declare function hobexPay(transport: InternerTransport, options: HobexPayOptions): Promise<HobexReceipt>;
50
54
  /**
@@ -57,7 +61,11 @@ export declare function hobexPay(transport: InternerTransport, options: HobexPay
57
61
  * schon im Transport, sobald der Status nicht ausdruecklich Erfolg ist. Ein
58
62
  * Wahrheitswert waere hier also immer `true` — eine Luege ueber den
59
63
  * Informationsgehalt, an der ein Aufrufer ein `if` aufhaengt, das nie greift.
60
- * Misserfolg kommt als geworfener Fehler.
64
+ * Misserfolg kommt als geworfener Fehler: eine Fehlerhuelle als
65
+ * [KasseneckApiError] mit Code, Netz, Zeitlimit, HTTP 5xx und eine unlesbare
66
+ * Erfolgsantwort mit `outcome: 'unknown'` (die Erstattung kann gelaufen sein).
67
+ * Ein `false` an dieser Stelle luede zum zweiten Versuch ein, also zur
68
+ * doppelten Erstattung. **Nie wiederholen**, sondern den Stand nachlesen.
61
69
  *
62
70
  * **Der Endpunkt heisst `hobexRefundApi`** — mit Suffix.
63
71
  */
@@ -41,6 +41,10 @@ const ENDPUNKT_REFUND = 'hobexRefundApi';
41
41
  *
42
42
  * **Der Endpunkt heisst `hobexPayApi`** — mit Suffix; ohne ihn gibt es ihn
43
43
  * nicht.
44
+ *
45
+ * **Nie wiederholen.** Bei `isOutcomeUnknown(e)` (Netz, Zeitlimit, HTTP 5xx,
46
+ * unlesbare Antwort) kann die Karte belastet sein: den Stand ueber die
47
+ * Transaktionskennung nachlesen, nicht ein zweites Mal belasten.
44
48
  */
45
49
  async function hobexPay(transport, options) {
46
50
  const params = zahlungsNutzlast(ENDPUNKT_PAY, options);
@@ -48,7 +52,11 @@ async function hobexPay(transport, options) {
48
52
  // geht sie als null raus, nicht gar nicht. Der Transport wirft nur
49
53
  // `undefined` weg, `null` bleibt erhalten — genau diese Unterscheidung.
50
54
  params['reference'] = options.reference ?? null;
51
- return belegAusNutzlast(await transport(ENDPUNKT_PAY, params));
55
+ const daten = await transport(ENDPUNKT_PAY, params);
56
+ // Erfolg gemeldet heisst: die Karte ist belastet. Ist der Beleg dann
57
+ // unlesbar, bleibt der Ausgang unklar (`response_unreadable`), damit keine
58
+ // App ein zweites Mal belastet.
59
+ return (0, errors_js_1.signiertGelesen)(ENDPUNKT_PAY, () => belegAusNutzlast(daten));
52
60
  }
53
61
  /**
54
62
  * Erstattet eine zuvor ueber die Hobex-Cloud getaetigte Zahlung. Das Backend
@@ -60,7 +68,11 @@ async function hobexPay(transport, options) {
60
68
  * schon im Transport, sobald der Status nicht ausdruecklich Erfolg ist. Ein
61
69
  * Wahrheitswert waere hier also immer `true` — eine Luege ueber den
62
70
  * Informationsgehalt, an der ein Aufrufer ein `if` aufhaengt, das nie greift.
63
- * Misserfolg kommt als geworfener Fehler.
71
+ * Misserfolg kommt als geworfener Fehler: eine Fehlerhuelle als
72
+ * [KasseneckApiError] mit Code, Netz, Zeitlimit, HTTP 5xx und eine unlesbare
73
+ * Erfolgsantwort mit `outcome: 'unknown'` (die Erstattung kann gelaufen sein).
74
+ * Ein `false` an dieser Stelle luede zum zweiten Versuch ein, also zur
75
+ * doppelten Erstattung. **Nie wiederholen**, sondern den Stand nachlesen.
64
76
  *
65
77
  * **Der Endpunkt heisst `hobexRefundApi`** — mit Suffix.
66
78
  */
@@ -57,5 +57,8 @@ export declare function createStripeLink(transport: InternerTransport, options:
57
57
  * **Der Parameter heisst `stripe_sessions_id`** — mit "sessions" im Plural, so
58
58
  * das Vorbild und so das Backend. Der naheliegende Singular waere ein fehlender
59
59
  * Pflichtparameter.
60
+ *
61
+ * **Nie wiederholen.** Bei `isOutcomeUnknown(e)` (Netz, Zeitlimit, HTTP 5xx,
62
+ * unlesbare Antwort) kann der Einzug gelaufen sein: den Stand nachlesen.
60
63
  */
61
64
  export declare function stripeCaptureIntent(transport: InternerTransport, stripeSessionId: string): Promise<StripeCaptureResult>;
@@ -72,13 +72,18 @@ async function createStripeLink(transport, options) {
72
72
  * **Der Parameter heisst `stripe_sessions_id`** — mit "sessions" im Plural, so
73
73
  * das Vorbild und so das Backend. Der naheliegende Singular waere ein fehlender
74
74
  * Pflichtparameter.
75
+ *
76
+ * **Nie wiederholen.** Bei `isOutcomeUnknown(e)` (Netz, Zeitlimit, HTTP 5xx,
77
+ * unlesbare Antwort) kann der Einzug gelaufen sein: den Stand nachlesen.
75
78
  */
76
79
  async function stripeCaptureIntent(transport, stripeSessionId) {
77
80
  if (typeof stripeSessionId !== 'string' || !stripeSessionId.trim()) {
78
81
  throw eingabefehler(ENDPUNKT_CAPTURE, 'stripeSessionId fehlt');
79
82
  }
80
83
  const daten = await transport(ENDPUNKT_CAPTURE, { stripe_sessions_id: stripeSessionId });
81
- return einzugAusNutzlast(daten);
84
+ // Erfolg gemeldet heisst: eingezogen. Eine unlesbare Nutzlast bleibt
85
+ // Ausgang unklar (`response_unreadable`), nie ein gewoehnlicher Lesefehler.
86
+ return (0, errors_js_1.signiertGelesen)(ENDPUNKT_CAPTURE, () => einzugAusNutzlast(daten));
82
87
  }
83
88
  /**
84
89
  * Positionen in ihre Nutzlast wandeln und dabei die strenge Schreibpfad-
@@ -4,4 +4,4 @@
4
4
  * Client-Version, Nachtrag §6). Ein Test haelt sie gleich mit `package.json`;
5
5
  * beim Anheben der Version beide Stellen aendern.
6
6
  */
7
- export declare const PACKAGE_VERSION = "1.0.0-rc.1";
7
+ export declare const PACKAGE_VERSION = "1.0.0-rc.2";
@@ -7,4 +7,4 @@ exports.PACKAGE_VERSION = void 0;
7
7
  * Client-Version, Nachtrag §6). Ein Test haelt sie gleich mit `package.json`;
8
8
  * beim Anheben der Version beide Stellen aendern.
9
9
  */
10
- exports.PACKAGE_VERSION = '1.0.0-rc.1';
10
+ exports.PACKAGE_VERSION = '1.0.0-rc.2';
@@ -20,7 +20,7 @@
20
20
  * Single-Page-App hat geantwortet, der Aufruf kam nie an),
21
21
  * `dialect_mismatch` (Antwort ohne Kennzeichen `Kasseneck-Api-Version: v3`:
22
22
  * ein Rand ohne `/v3` hat geantwortet, **Ausgang unklar**) und
23
- * `response_unreadable` (ein signierender Aufruf meldete Erfolg, die
23
+ * `response_unreadable` (ein Aufruf mit Wirkung meldete Erfolg, die
24
24
  * Antwort traegt aber nicht, was er zusagt: **Ausgang unklar**).
25
25
  * **Entscheidend ist `outcome`:** `'rejected'` heisst abgelehnt, nichts
26
26
  * geschehen, Wiederholen hilft nicht. `'unknown'` heisst: der Vorgang kann
@@ -29,14 +29,16 @@
29
29
  * - `KasseneckHttpError` — die Antwort war **keine** verwertbare Huelle:
30
30
  * HTTP 500/404 ohne Huelle, leerer Rumpf oder Text statt JSON. Beim
31
31
  * Bericht-Download gelten dieselben Gruende fuer alles, was kein PDF ist.
32
- * `reason` trennt die Faelle maschinenlesbar. Auf einem signierenden
33
- * Aufruf hat HTTP 5xx `outcome: 'unknown'`, ebenso HTTP 200 mit
34
- * Kennzeichen, aber leerem oder unlesbarem Rumpf.
32
+ * `reason` trennt die Faelle maschinenlesbar. Auf einem Aufruf mit
33
+ * Wirkung (signierend oder geldbewegend) hat HTTP 5xx
34
+ * `outcome: 'unknown'`, ebenso HTTP 200 mit Kennzeichen, aber leerem oder
35
+ * unlesbarem Rumpf.
35
36
  * - `KasseneckNetworkError` — die Antwort kam gar nicht: Netz weg, DNS,
36
37
  * abgebrochene Verbindung oder Zeitueberschreitung (`timedOut`). Auch hier
37
38
  * gilt `outcome`: war die Anfrage schon unterwegs und ist der Aufruf einer
38
- * der signierenden (`createReceipt`, `cancelReceipt`, `financeWebService`),
39
- * ist er `'unknown'`.
39
+ * mit Wirkung (signierend: `createReceipt`, `cancelReceipt`,
40
+ * `financeWebService`; geldbewegend: `hobexPayApi`, `hobexRefundApi`,
41
+ * `stripeCaptureIntent`), ist er `'unknown'`.
40
42
  * - `KasseneckAuthError` — es kam nicht einmal zur Anfrage, weil die Anmeldung
41
43
  * scheiterte (fehlende Zugangsdaten, oder der Token-/Sitzungsgeber warf).
42
44
  * In der Browser-Kasse mit ihrer 90-Sekunden-Sitzung ist das Alltag, kein
@@ -111,7 +113,7 @@ export type ErrorOutcome = 'unknown' | 'rejected';
111
113
  /**
112
114
  * Codes, die das Paket selbst vergibt, nicht der Server: `route_missing`
113
115
  * (HTML statt Backend, der Aufruf kam nie an) und `response_unreadable`
114
- * (ein signierender Aufruf meldete Erfolg, die Antwort ist aber unlesbar;
116
+ * (ein Aufruf mit Wirkung meldete Erfolg, die Antwort ist aber unlesbar;
115
117
  * Ausgang unklar). `dialect_mismatch` vergibt das Paket ebenfalls, der Code
116
118
  * gehoert aber schon zum Rand des Servers (`errorCodes.edge`).
117
119
  */
@@ -142,7 +144,7 @@ export declare class KasseneckApiError extends Error {
142
144
  /**
143
145
  * `'unknown'` bei `dialect_mismatch`, `receipt_outcome_unknown`,
144
146
  * `cancellation_outcome_unknown`, `response_unreadable` (Erfolg gemeldet,
145
- * Antwort eines signierenden Aufrufs aber unlesbar) und
147
+ * Antwort eines Aufrufs mit Wirkung aber unlesbar) und
146
148
  * `response_translation_failed` (ausser mit `details.handled === false`);
147
149
  * sonst `'rejected'`. Bei `'unknown'` nie wiederholen, sondern nachlesen.
148
150
  */
@@ -162,8 +164,9 @@ export declare class KasseneckHttpError extends Error {
162
164
  /** Maschinenlesbarer Grund — trennt den Rewrite-Fall vom 500er ohne Textparsen. */
163
165
  readonly reason: HttpFailureReason;
164
166
  /**
165
- * `'unknown'` auf einem signierenden Aufruf (`createReceipt`,
166
- * `cancelReceipt`, `financeWebService`) bei HTTP 5xx und bei HTTP 200 mit
167
+ * `'unknown'` auf einem Aufruf mit Wirkung (`createReceipt`,
168
+ * `cancelReceipt`, `financeWebService`, `hobexPayApi`, `hobexRefundApi`,
169
+ * `stripeCaptureIntent`) bei HTTP 5xx und bei HTTP 200 mit
167
170
  * Kennzeichen, aber unlesbarem Rumpf (`empty-body`, `not-json` auch bei
168
171
  * `text/html`, `missing-status`): der Handler kann gelaufen sein, nie wiederholen,
169
172
  * sondern nachlesen. Sonst `'rejected'` (auch 4xx).
@@ -249,3 +252,16 @@ export declare function isKasseneckHttpError(error: unknown): error is Kasseneck
249
252
  export declare function isKasseneckNetworkError(error: unknown): error is KasseneckNetworkError;
250
253
  export declare function isKasseneckAuthError(error: unknown): error is KasseneckAuthError;
251
254
  export declare function isKasseneckValidationError(error: unknown): error is KasseneckValidationError;
255
+ /**
256
+ * Liest die Erfolgsantwort eines Aufrufs **mit Wirkung** (signierend oder
257
+ * geldbewegend). Scheitert das Lesen (fehlender Beleg, fehlender Bezug,
258
+ * unbrauchbares Feld oder ein Laufzeitfehler beim Umwandeln), hat der Server
259
+ * trotzdem Erfolg gemeldet: der Beleg ist signiert und im DEP, die Karte
260
+ * belastet bzw. der Einzug gelaufen. Das darf nie als gewoehnlicher Fehler
261
+ * enden, sonst kassiert die Kasse ein zweites Mal. Darum wird daraus
262
+ * `KasseneckApiError` mit Code `response_unreadable` und `outcome: 'unknown'`.
263
+ * Der Grund stammt vom Paket; aus der Antwort selbst wird nichts uebernommen.
264
+ *
265
+ * Paketintern (receipts.ts, payments/); nicht Teil der Paketoberflaeche.
266
+ */
267
+ export declare function signiertGelesen<T>(functionName: string, lesen: () => T): T;
@@ -20,7 +20,7 @@
20
20
  * Single-Page-App hat geantwortet, der Aufruf kam nie an),
21
21
  * `dialect_mismatch` (Antwort ohne Kennzeichen `Kasseneck-Api-Version: v3`:
22
22
  * ein Rand ohne `/v3` hat geantwortet, **Ausgang unklar**) und
23
- * `response_unreadable` (ein signierender Aufruf meldete Erfolg, die
23
+ * `response_unreadable` (ein Aufruf mit Wirkung meldete Erfolg, die
24
24
  * Antwort traegt aber nicht, was er zusagt: **Ausgang unklar**).
25
25
  * **Entscheidend ist `outcome`:** `'rejected'` heisst abgelehnt, nichts
26
26
  * geschehen, Wiederholen hilft nicht. `'unknown'` heisst: der Vorgang kann
@@ -29,14 +29,16 @@
29
29
  * - `KasseneckHttpError` — die Antwort war **keine** verwertbare Huelle:
30
30
  * HTTP 500/404 ohne Huelle, leerer Rumpf oder Text statt JSON. Beim
31
31
  * Bericht-Download gelten dieselben Gruende fuer alles, was kein PDF ist.
32
- * `reason` trennt die Faelle maschinenlesbar. Auf einem signierenden
33
- * Aufruf hat HTTP 5xx `outcome: 'unknown'`, ebenso HTTP 200 mit
34
- * Kennzeichen, aber leerem oder unlesbarem Rumpf.
32
+ * `reason` trennt die Faelle maschinenlesbar. Auf einem Aufruf mit
33
+ * Wirkung (signierend oder geldbewegend) hat HTTP 5xx
34
+ * `outcome: 'unknown'`, ebenso HTTP 200 mit Kennzeichen, aber leerem oder
35
+ * unlesbarem Rumpf.
35
36
  * - `KasseneckNetworkError` — die Antwort kam gar nicht: Netz weg, DNS,
36
37
  * abgebrochene Verbindung oder Zeitueberschreitung (`timedOut`). Auch hier
37
38
  * gilt `outcome`: war die Anfrage schon unterwegs und ist der Aufruf einer
38
- * der signierenden (`createReceipt`, `cancelReceipt`, `financeWebService`),
39
- * ist er `'unknown'`.
39
+ * mit Wirkung (signierend: `createReceipt`, `cancelReceipt`,
40
+ * `financeWebService`; geldbewegend: `hobexPayApi`, `hobexRefundApi`,
41
+ * `stripeCaptureIntent`), ist er `'unknown'`.
40
42
  * - `KasseneckAuthError` — es kam nicht einmal zur Anfrage, weil die Anmeldung
41
43
  * scheiterte (fehlende Zugangsdaten, oder der Token-/Sitzungsgeber warf).
42
44
  * In der Browser-Kasse mit ihrer 90-Sekunden-Sitzung ist das Alltag, kein
@@ -225,7 +227,7 @@ function ausgangAusCode(code, details) {
225
227
  /**
226
228
  * Codes, die das Paket selbst vergibt, nicht der Server: `route_missing`
227
229
  * (HTML statt Backend, der Aufruf kam nie an) und `response_unreadable`
228
- * (ein signierender Aufruf meldete Erfolg, die Antwort ist aber unlesbar;
230
+ * (ein Aufruf mit Wirkung meldete Erfolg, die Antwort ist aber unlesbar;
229
231
  * Ausgang unklar). `dialect_mismatch` vergibt das Paket ebenfalls, der Code
230
232
  * gehoert aber schon zum Rand des Servers (`errorCodes.edge`).
231
233
  */
@@ -255,7 +257,7 @@ export class KasseneckApiError extends Error {
255
257
  /**
256
258
  * `'unknown'` bei `dialect_mismatch`, `receipt_outcome_unknown`,
257
259
  * `cancellation_outcome_unknown`, `response_unreadable` (Erfolg gemeldet,
258
- * Antwort eines signierenden Aufrufs aber unlesbar) und
260
+ * Antwort eines Aufrufs mit Wirkung aber unlesbar) und
259
261
  * `response_translation_failed` (ausser mit `details.handled === false`);
260
262
  * sonst `'rejected'`. Bei `'unknown'` nie wiederholen, sondern nachlesen.
261
263
  */
@@ -289,8 +291,9 @@ export class KasseneckHttpError extends Error {
289
291
  /** Maschinenlesbarer Grund — trennt den Rewrite-Fall vom 500er ohne Textparsen. */
290
292
  reason;
291
293
  /**
292
- * `'unknown'` auf einem signierenden Aufruf (`createReceipt`,
293
- * `cancelReceipt`, `financeWebService`) bei HTTP 5xx und bei HTTP 200 mit
294
+ * `'unknown'` auf einem Aufruf mit Wirkung (`createReceipt`,
295
+ * `cancelReceipt`, `financeWebService`, `hobexPayApi`, `hobexRefundApi`,
296
+ * `stripeCaptureIntent`) bei HTTP 5xx und bei HTTP 200 mit
294
297
  * Kennzeichen, aber unlesbarem Rumpf (`empty-body`, `not-json` auch bei
295
298
  * `text/html`, `missing-status`): der Handler kann gelaufen sein, nie wiederholen,
296
299
  * sondern nachlesen. Sonst `'rejected'` (auch 4xx).
@@ -410,3 +413,24 @@ export function isKasseneckAuthError(error) {
410
413
  export function isKasseneckValidationError(error) {
411
414
  return error instanceof KasseneckValidationError;
412
415
  }
416
+ /**
417
+ * Liest die Erfolgsantwort eines Aufrufs **mit Wirkung** (signierend oder
418
+ * geldbewegend). Scheitert das Lesen (fehlender Beleg, fehlender Bezug,
419
+ * unbrauchbares Feld oder ein Laufzeitfehler beim Umwandeln), hat der Server
420
+ * trotzdem Erfolg gemeldet: der Beleg ist signiert und im DEP, die Karte
421
+ * belastet bzw. der Einzug gelaufen. Das darf nie als gewoehnlicher Fehler
422
+ * enden, sonst kassiert die Kasse ein zweites Mal. Darum wird daraus
423
+ * `KasseneckApiError` mit Code `response_unreadable` und `outcome: 'unknown'`.
424
+ * Der Grund stammt vom Paket; aus der Antwort selbst wird nichts uebernommen.
425
+ *
426
+ * Paketintern (receipts.ts, payments/); nicht Teil der Paketoberflaeche.
427
+ */
428
+ export function signiertGelesen(functionName, lesen) {
429
+ try {
430
+ return lesen();
431
+ }
432
+ catch (ursache) {
433
+ const grund = ursache instanceof KasseneckValidationError ? ursache.reason : 'Antwort nicht lesbar';
434
+ throw new KasseneckApiError(functionName, `Erfolg gemeldet, Antwort aber unlesbar (${grund}). Der Vorgang kann ausgefuehrt sein: nicht wiederholen, sondern nachlesen.`, {}, 'response_unreadable');
435
+ }
436
+ }
@@ -2,7 +2,7 @@ import { ReceiptType, KeckPaymentMethod, CreditCardProvider, VoucherAction, Vouc
2
2
  import { fromReceiptCompanyPayload, fromReceiptPayload, toReceiptItemPayload, toVoucherPayload, receiptItemIsValid, voucherIsValid, fromReceiptSummaryPayload, isCancellationReason, RECEIPT_EMAIL_VIAS, readRegistrationInfo, } from '../models/index.js';
3
3
  import { parseServerTimeStamp, toViennaWallClock } from '../vienna-time.js';
4
4
  import { euroToCents } from '../money.js';
5
- import { KasseneckApiError, KasseneckValidationError, isKasseneckApiError } from './errors.js';
5
+ import { KasseneckValidationError, isKasseneckApiError, signiertGelesen } from './errors.js';
6
6
  import { buildReceiptLayout } from '../receipt/layout.js';
7
7
  /**
8
8
  * Das Zeilenmodell zum Drucken und Anzeigen eines Belegs aus einer Antwort.
@@ -666,24 +666,6 @@ function kartenanbieter(wert) {
666
666
  function eingabefehler(grund) {
667
667
  return new KasseneckValidationError('createReceipt', grund, 'request');
668
668
  }
669
- /**
670
- * Liest die Erfolgsantwort eines **signierenden** Aufrufs. Scheitert das
671
- * Lesen (fehlender Beleg, fehlender Bezug, unbrauchbares Feld oder ein
672
- * Laufzeitfehler beim Umwandeln), hat der Server trotzdem Erfolg gemeldet:
673
- * der Beleg ist signiert und im DEP. Das darf nie als gewoehnlicher Fehler
674
- * enden, sonst kassiert die Kasse ein zweites Mal. Darum wird daraus
675
- * `KasseneckApiError` mit Code `response_unreadable` und `outcome: 'unknown'`.
676
- * Der Grund stammt vom Paket; aus der Antwort selbst wird nichts uebernommen.
677
- */
678
- function signiertGelesen(functionName, lesen) {
679
- try {
680
- return lesen();
681
- }
682
- catch (ursache) {
683
- const grund = ursache instanceof KasseneckValidationError ? ursache.reason : 'Antwort nicht lesbar';
684
- throw new KasseneckApiError(functionName, `Erfolg gemeldet, Antwort aber unlesbar (${grund}). Der Vorgang kann ausgefuehrt sein: nicht wiederholen, sondern nachlesen.`, {}, 'response_unreadable');
685
- }
686
- }
687
669
  /** Die Antwort meldete Erfolg, trug aber nicht, was der Aufruf zusagt. */
688
670
  function antwortfehler(functionName, grund) {
689
671
  return new KasseneckValidationError(functionName, grund, 'response');
@@ -57,10 +57,22 @@ const KASSENECK_HOSTS = new Set(['api.kasseneck.at', 'kasse.kasseneck.at']);
57
57
  /** Die 1.x-Linie spricht nur `/v3`: jede Basis endet so (`/v3` oder `/api/v3`). */
58
58
  const V3_ENDE = /\/v3$/;
59
59
  /**
60
- * Aufrufe, die signieren bzw. bei FinanzOnline etwas ausloesen. Ein Netzfehler,
61
- * nachdem die Anfrage unterwegs war, laesst ihren Ausgang offen.
60
+ * Aufrufe mit Wirkung, die nie blind wiederholt werden duerfen: sie signieren
61
+ * (`createReceipt`, `cancelReceipt`), loesen bei FinanzOnline etwas aus
62
+ * (`financeWebService`) oder bewegen Geld (`hobexPayApi` belastet eine Karte,
63
+ * `hobexRefundApi` erstattet, `stripeCaptureIntent` zieht eine vorgemerkte
64
+ * Zahlung ein). Scheitert einer, nachdem die Anfrage unterwegs war (Netz,
65
+ * Zeitlimit, HTTP 5xx, unlesbare Erfolgsantwort, HTML mit Kennzeichen), ist
66
+ * sein Ausgang offen.
62
67
  */
63
- const SIGNIERENDE_AUFRUFE = new Set(['createReceipt', 'cancelReceipt', 'financeWebService']);
68
+ const UNKNOWN_OUTCOME_CALLS = new Set([
69
+ 'createReceipt',
70
+ 'cancelReceipt',
71
+ 'financeWebService',
72
+ 'hobexPayApi',
73
+ 'hobexRefundApi',
74
+ 'stripeCaptureIntent',
75
+ ]);
64
76
  /**
65
77
  * Produkte, die das Backend in `Kasseneck-Client` zaehlt (Positivliste,
66
78
  * Nachtrag §6); alles andere zaehlte dort als `ungueltig`. Das Paket weist
@@ -157,9 +169,9 @@ function createCore(options) {
157
169
  return ursache;
158
170
  }
159
171
  // Bis hierher kam keine verwertbare Antwort: Netz weg oder Zeitlimit.
160
- // Die Anfrage war schon unterwegs: bei einem signierenden Aufruf kann
161
- // der Beleg entstanden sein (Ausgang unklar, nachlesen).
162
- return new KasseneckNetworkError(fehlerName, abbruch.signal.aborted, zeitlimitMs, causeDigest(ursache, geheimnisse), SIGNIERENDE_AUFRUFE.has(functionName) ? 'unknown' : 'rejected');
172
+ // Die Anfrage war schon unterwegs: bei einem Aufruf mit Wirkung kann
173
+ // der Beleg bzw. die Zahlung entstanden sein (Ausgang unklar, nachlesen).
174
+ return new KasseneckNetworkError(fehlerName, abbruch.signal.aborted, zeitlimitMs, causeDigest(ursache, geheimnisse), UNKNOWN_OUTCOME_CALLS.has(functionName) ? 'unknown' : 'rejected');
163
175
  };
164
176
  const basis = basisFuer(functionName);
165
177
  const url = `${basis}/${encodeURIComponent(functionName)}`;
@@ -212,18 +224,18 @@ function createCore(options) {
212
224
  if (fehler)
213
225
  throw fehler;
214
226
  }
215
- // 5xx auf einem signierenden Aufruf: der Handler kann gelaufen sein.
216
- const ausgang = antwort.status >= 500 && SIGNIERENDE_AUFRUFE.has(functionName) ? 'unknown' : 'rejected';
227
+ // 5xx auf einem Aufruf mit Wirkung: der Handler kann gelaufen sein.
228
+ const ausgang = antwort.status >= 500 && UNKNOWN_OUTCOME_CALLS.has(functionName) ? 'unknown' : 'rejected';
217
229
  throw new KasseneckHttpError(fehlerName, antwort.status, inhaltstyp, 'server-error', ausgang);
218
230
  }
219
231
  // HTTP 200 mit HTML: die Auffangregel der Single-Page-App hat den Aufruf
220
232
  // bedient, keine Function hat ihn gesehen (Nachtrag §5.4, R15).
221
233
  if (inhaltstyp !== undefined && /^\s*text\/html\b/i.test(inhaltstyp)) {
222
234
  // Mit Kennzeichen hat der `/v3`-Rand den Aufruf gesehen (etwa ein
223
- // Proxy, der nur den Inhaltstyp umschreibt). Bei einem signierenden
224
- // Aufruf kann der Beleg dann entstanden sein: unlesbarer Rumpf,
235
+ // Proxy, der nur den Inhaltstyp umschreibt). Bei einem Aufruf mit
236
+ // Wirkung kann der Beleg bzw. die Zahlung dann entstanden sein: unlesbarer Rumpf,
225
237
  // Ausgang unklar, statt `route_missing`.
226
- if (SIGNIERENDE_AUFRUFE.has(functionName) && traegtKennzeichen(antwort)) {
238
+ if (UNKNOWN_OUTCOME_CALLS.has(functionName) && traegtKennzeichen(antwort)) {
227
239
  throw new KasseneckHttpError(fehlerName, antwort.status, inhaltstyp, 'not-json', 'unknown');
228
240
  }
229
241
  throw new KasseneckApiError(fehlerName, 'Route fehlt: die Antwort ist eine HTML-Seite statt des Backends', {}, 'route_missing');
@@ -240,8 +252,8 @@ function createCore(options) {
240
252
  }
241
253
  // Ab hier kam HTTP 200 mit Kennzeichen: der `/v3`-Rand hat den Aufruf
242
254
  // gesehen. Ist der Rumpf dann unlesbar (gekuerzt von einem Proxy,
243
- // abgebrochene Verbindung), kann ein signierender Handler gelaufen sein.
244
- const unlesbar = SIGNIERENDE_AUFRUFE.has(functionName) ? 'unknown' : 'rejected';
255
+ // abgebrochene Verbindung), kann ein Handler mit Wirkung gelaufen sein.
256
+ const unlesbar = UNKNOWN_OUTCOME_CALLS.has(functionName) ? 'unknown' : 'rejected';
245
257
  return auswerten(koerper, fehlerName, antwort.status, inhaltstyp, geheimnisse, unlesbar);
246
258
  }
247
259
  finally {
@@ -45,6 +45,10 @@ export interface HobexTransactionIdOptions {
45
45
  *
46
46
  * **Der Endpunkt heisst `hobexPayApi`** — mit Suffix; ohne ihn gibt es ihn
47
47
  * nicht.
48
+ *
49
+ * **Nie wiederholen.** Bei `isOutcomeUnknown(e)` (Netz, Zeitlimit, HTTP 5xx,
50
+ * unlesbare Antwort) kann die Karte belastet sein: den Stand ueber die
51
+ * Transaktionskennung nachlesen, nicht ein zweites Mal belasten.
48
52
  */
49
53
  export declare function hobexPay(transport: InternerTransport, options: HobexPayOptions): Promise<HobexReceipt>;
50
54
  /**
@@ -57,7 +61,11 @@ export declare function hobexPay(transport: InternerTransport, options: HobexPay
57
61
  * schon im Transport, sobald der Status nicht ausdruecklich Erfolg ist. Ein
58
62
  * Wahrheitswert waere hier also immer `true` — eine Luege ueber den
59
63
  * Informationsgehalt, an der ein Aufrufer ein `if` aufhaengt, das nie greift.
60
- * Misserfolg kommt als geworfener Fehler.
64
+ * Misserfolg kommt als geworfener Fehler: eine Fehlerhuelle als
65
+ * [KasseneckApiError] mit Code, Netz, Zeitlimit, HTTP 5xx und eine unlesbare
66
+ * Erfolgsantwort mit `outcome: 'unknown'` (die Erstattung kann gelaufen sein).
67
+ * Ein `false` an dieser Stelle luede zum zweiten Versuch ein, also zur
68
+ * doppelten Erstattung. **Nie wiederholen**, sondern den Stand nachlesen.
61
69
  *
62
70
  * **Der Endpunkt heisst `hobexRefundApi`** — mit Suffix.
63
71
  */
@@ -1,5 +1,5 @@
1
1
  import { fromHobexReceiptPayload } from '../models/index.js';
2
- import { KasseneckValidationError } from '../client/errors.js';
2
+ import { KasseneckValidationError, signiertGelesen } from '../client/errors.js';
3
3
  import { toViennaWallClock } from '../vienna-time.js';
4
4
  import { centsToEuro } from '../money.js';
5
5
  /**
@@ -36,6 +36,10 @@ const ENDPUNKT_REFUND = 'hobexRefundApi';
36
36
  *
37
37
  * **Der Endpunkt heisst `hobexPayApi`** — mit Suffix; ohne ihn gibt es ihn
38
38
  * nicht.
39
+ *
40
+ * **Nie wiederholen.** Bei `isOutcomeUnknown(e)` (Netz, Zeitlimit, HTTP 5xx,
41
+ * unlesbare Antwort) kann die Karte belastet sein: den Stand ueber die
42
+ * Transaktionskennung nachlesen, nicht ein zweites Mal belasten.
39
43
  */
40
44
  export async function hobexPay(transport, options) {
41
45
  const params = zahlungsNutzlast(ENDPUNKT_PAY, options);
@@ -43,7 +47,11 @@ export async function hobexPay(transport, options) {
43
47
  // geht sie als null raus, nicht gar nicht. Der Transport wirft nur
44
48
  // `undefined` weg, `null` bleibt erhalten — genau diese Unterscheidung.
45
49
  params['reference'] = options.reference ?? null;
46
- return belegAusNutzlast(await transport(ENDPUNKT_PAY, params));
50
+ const daten = await transport(ENDPUNKT_PAY, params);
51
+ // Erfolg gemeldet heisst: die Karte ist belastet. Ist der Beleg dann
52
+ // unlesbar, bleibt der Ausgang unklar (`response_unreadable`), damit keine
53
+ // App ein zweites Mal belastet.
54
+ return signiertGelesen(ENDPUNKT_PAY, () => belegAusNutzlast(daten));
47
55
  }
48
56
  /**
49
57
  * Erstattet eine zuvor ueber die Hobex-Cloud getaetigte Zahlung. Das Backend
@@ -55,7 +63,11 @@ export async function hobexPay(transport, options) {
55
63
  * schon im Transport, sobald der Status nicht ausdruecklich Erfolg ist. Ein
56
64
  * Wahrheitswert waere hier also immer `true` — eine Luege ueber den
57
65
  * Informationsgehalt, an der ein Aufrufer ein `if` aufhaengt, das nie greift.
58
- * Misserfolg kommt als geworfener Fehler.
66
+ * Misserfolg kommt als geworfener Fehler: eine Fehlerhuelle als
67
+ * [KasseneckApiError] mit Code, Netz, Zeitlimit, HTTP 5xx und eine unlesbare
68
+ * Erfolgsantwort mit `outcome: 'unknown'` (die Erstattung kann gelaufen sein).
69
+ * Ein `false` an dieser Stelle luede zum zweiten Versuch ein, also zur
70
+ * doppelten Erstattung. **Nie wiederholen**, sondern den Stand nachlesen.
59
71
  *
60
72
  * **Der Endpunkt heisst `hobexRefundApi`** — mit Suffix.
61
73
  */
@@ -57,5 +57,8 @@ export declare function createStripeLink(transport: InternerTransport, options:
57
57
  * **Der Parameter heisst `stripe_sessions_id`** — mit "sessions" im Plural, so
58
58
  * das Vorbild und so das Backend. Der naheliegende Singular waere ein fehlender
59
59
  * Pflichtparameter.
60
+ *
61
+ * **Nie wiederholen.** Bei `isOutcomeUnknown(e)` (Netz, Zeitlimit, HTTP 5xx,
62
+ * unlesbare Antwort) kann der Einzug gelaufen sein: den Stand nachlesen.
60
63
  */
61
64
  export declare function stripeCaptureIntent(transport: InternerTransport, stripeSessionId: string): Promise<StripeCaptureResult>;
@@ -1,6 +1,6 @@
1
1
  import { StripeLinkMode } from '../enums/index.js';
2
2
  import { fromStripeUrlSessionPayload, toReceiptItemPayload, receiptItemIsValid, } from '../models/index.js';
3
- import { KasseneckValidationError } from '../client/errors.js';
3
+ import { KasseneckValidationError, signiertGelesen } from '../client/errors.js';
4
4
  /**
5
5
  * Stripe-Zahlungslinks — Zwilling der Stripe-Aufrufe in
6
6
  * kasseneck_api/lib/kasseneck_api.dart (Zeilen 443-484).
@@ -68,13 +68,18 @@ export async function createStripeLink(transport, options) {
68
68
  * **Der Parameter heisst `stripe_sessions_id`** — mit "sessions" im Plural, so
69
69
  * das Vorbild und so das Backend. Der naheliegende Singular waere ein fehlender
70
70
  * Pflichtparameter.
71
+ *
72
+ * **Nie wiederholen.** Bei `isOutcomeUnknown(e)` (Netz, Zeitlimit, HTTP 5xx,
73
+ * unlesbare Antwort) kann der Einzug gelaufen sein: den Stand nachlesen.
71
74
  */
72
75
  export async function stripeCaptureIntent(transport, stripeSessionId) {
73
76
  if (typeof stripeSessionId !== 'string' || !stripeSessionId.trim()) {
74
77
  throw eingabefehler(ENDPUNKT_CAPTURE, 'stripeSessionId fehlt');
75
78
  }
76
79
  const daten = await transport(ENDPUNKT_CAPTURE, { stripe_sessions_id: stripeSessionId });
77
- return einzugAusNutzlast(daten);
80
+ // Erfolg gemeldet heisst: eingezogen. Eine unlesbare Nutzlast bleibt
81
+ // Ausgang unklar (`response_unreadable`), nie ein gewoehnlicher Lesefehler.
82
+ return signiertGelesen(ENDPUNKT_CAPTURE, () => einzugAusNutzlast(daten));
78
83
  }
79
84
  /**
80
85
  * Positionen in ihre Nutzlast wandeln und dabei die strenge Schreibpfad-
@@ -4,4 +4,4 @@
4
4
  * Client-Version, Nachtrag §6). Ein Test haelt sie gleich mit `package.json`;
5
5
  * beim Anheben der Version beide Stellen aendern.
6
6
  */
7
- export declare const PACKAGE_VERSION = "1.0.0-rc.1";
7
+ export declare const PACKAGE_VERSION = "1.0.0-rc.2";
@@ -4,4 +4,4 @@
4
4
  * Client-Version, Nachtrag §6). Ein Test haelt sie gleich mit `package.json`;
5
5
  * beim Anheben der Version beide Stellen aendern.
6
6
  */
7
- export const PACKAGE_VERSION = '1.0.0-rc.1';
7
+ export const PACKAGE_VERSION = '1.0.0-rc.2';
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.0.0-rc.1",
2
+ "version": "1.0.0-rc.2",
3
3
  "measuredOn": {
4
4
  "tid": "3600335",
5
5
  "hpsVersion": "1.10.0",
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "title": "Kasseneck Rechnungs-API",
4
4
  "version": 2,
5
- "package": "1.0.0-rc.1",
5
+ "package": "1.0.0-rc.2",
6
6
  "endpoints": {
7
7
  "createCustomer": {
8
8
  "request": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.0.0-rc.1",
2
+ "version": "1.0.0-rc.2",
3
3
  "languages": [
4
4
  "de",
5
5
  "en"
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.0.0-rc.1",
2
+ "version": "1.0.0-rc.2",
3
3
  "messages": {
4
4
  "network.no_connection": {
5
5
  "text": "Keine Verbindung zum Server. Bitte die Internetverbindung prüfen und erneut versuchen."
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.0.0-rc.1",
2
+ "version": "1.0.0-rc.2",
3
3
  "baseUrls": {
4
4
  "public": "https://api.kasseneck.at/v3",
5
5
  "pos": "https://kasse.kasseneck.at/api/v3"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kreiseck/kasseneck-api",
3
- "version": "1.0.0-rc.1",
3
+ "version": "1.0.0-rc.2",
4
4
  "description": "Austrian fiscal cash register (RKSV) client for JavaScript and TypeScript: signed receipts, cancellations, card payments, receipt printing (ESC/POS), invoices. RKSV-Registrierkasse für JavaScript und TypeScript.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Kreiseck",