@kreiseck/kasseneck-api 1.21.0 → 1.22.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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,22 @@ 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
+ ## 1.22.0 - 2026-10-31
8
+
9
+ Idempotent sales: `createReceipt` can carry an `idempotencyKey` (the backend evaluates it under `/v3` since 26.10.2026). Additive; without a key the request stays byte for byte as before.
10
+
11
+ - **`idempotencyKey?: string` on `sellReceipt`, `sellReceiptWithCompany` and `createReceipt`** (in `ReceiptCommonOptions`, so also on `createKasseneckApi().sellReceipt…`), sent as `params.idempotencyKey`. Per register: the same request with the same key returns the receipt already signed (no second receipt, no counter step; the plain codes of newly sold vouchers only the first time); the same key with other content is rejected with `idempotency_conflict` and nothing is written. Use a stable id of your own transaction (order number, number of the paper replacement receipt), never one made up per attempt. When recording a paper receipt (`manualReceipt`) the server also refuses the same paper number on the same day with `manual_receipt_duplicate`, a second, independent lock. Reason: after a lost answer (`outcome: 'unknown'`) a sale could only be looked up, never resent; with a key the same request can go out again unchanged.
12
+ - **Checked before sending like the server** (`beleg-idempotenz-core`): a string of 1 to 120 characters (`String.length`), not only whitespace. Empty, whitespace, longer or not a string throws `KasseneckValidationError` (`scope: 'request'`) before anything is sent; the key is never shortened or trimmed, because the server recognises the repeat by the key as it arrives. Missing or `null` sends no field.
13
+ - **`ReceiptWithCompany.replayed?: true`**: set by `sellReceiptWithCompany` when the server answered with `replayed: true` (the receipt of the first attempt). Missing otherwise, also on the first sale with a key, so the result keeps its shape. `sellReceipt` returns the `Receipt` alone and does not show it.
14
+ - `cancelReceipt` takes no key: the backend's cancellation endpoint does not evaluate one; after `cancellation_outcome_unknown` look the original up as before. `zeroReceipt` is unchanged.
15
+
16
+ ## 1.21.1 - 2026-10-31
17
+
18
+ Paying out a voucher that is no longer active names its status (twin of the backend rule `gutschein-core.darfAuszahlen`, keck, same day). Nothing else changes.
19
+
20
+ - **A payout checks `cancelled` and `blocked` first.** `receiptDueCents` and `receiptDueBreakdown` with a `payout` wish reject a cancelled voucher with `voucher_cancelled` and a blocked one with `voucher_blocked`, before the balance and before `payout: 'never'`. Reason: cancelling a voucher sale sets its balance to 0, so the register told the cashier "already used up" (`voucher_used_up`) for a cancelled voucher. Used up and paid out stay `voucher_used_up`.
21
+ - `fixtures/receipt-due-voucher-generated.json` regenerated from the backend with that rule (486 cases): four fixed payout cases at the end (`gen_voucher_482` to `485`: cancelled, cancelled with `never`, blocked with `never`, used up). The generator now maps the stored status into the check (`STATUS`); the random cases stay `active` and keep their bytes.
22
+
7
23
  ## 1.21.0 - 2026-10-31
8
24
 
9
25
  DEP export at the cash register (*Kassennachschau*, stage 2): the five DEP calls now also run on the register path, for the manager only, plus the words and the folder name both registers use. Additive.
package/README.md CHANGED
@@ -514,14 +514,15 @@ transaction by its `transactionId`, the Stripe session).
514
514
 
515
515
  **With an `idempotencyKey`** (inventory writes and reservations, `issueInvoice`,
516
516
  `cancelInvoice`, `createCreditNote`, `recordInvoicePayment`, `createCustomer`
517
- and `createPartnerCustomer` with a key) the safe retry after `'unknown'` is the
517
+ and `createPartnerCustomer` with a key, and since 1.22.0 `sellReceipt` and
518
+ `sellReceiptWithCompany` with a key) the safe retry after `'unknown'` is the
518
519
  same request with the **same** key: it takes effect exactly once and returns
519
- the stored answer (invoices and partner customers mark it with
520
- `replayed: true`; inventory writes return the stored answer unchanged). Never a
521
- new key: that books a second time. `createInvoiceItem` has no key; the server recognises a repeat by
520
+ the stored answer (invoices, partner customers and `sellReceiptWithCompany`
521
+ mark it with `replayed: true`; inventory writes return the stored answer
522
+ unchanged). Never a new key: that books a second time. `createInvoiceItem` has no key; the server recognises a repeat by
522
523
  `source` + `reference`, so the same request with the same values is safe
523
524
  (`replayed: true`; other data gives `item_reference_conflict`). Without a key
524
- (receipts, cancellations, money calls, settings, webhooks, `updateCustomer`,
525
+ (receipts without one, cancellations, money calls, settings, webhooks, `updateCustomer`,
525
526
  `createCustomer` and `createPartnerCustomer` without one, `updateInvoiceItem`,
526
527
  `withdrawInvoiceItem`, `setCustomerMandate`, `revokeCustomerMandate`) read the
527
528
  state first (for invoice items `getInvoiceItem`) and only then decide.
@@ -545,6 +546,21 @@ try {
545
546
  }
546
547
  ```
547
548
 
549
+ **A sale with a key of your own** (since 1.22.0): pass a stable id of the
550
+ transaction, for example the order number or the number of the paper
551
+ replacement receipt, never one made up per attempt. The key holds per
552
+ register, has at most 120 characters and goes out unchanged. After
553
+ `isOutcomeUnknown(error)` the same request with the same key returns the
554
+ receipt of the first attempt instead of signing a second one; the same key
555
+ with other content gives `idempotency_conflict`, and nothing is written. When
556
+ recording a paper receipt (`manualReceipt`) the server also refuses the same
557
+ paper number on the same day (`manual_receipt_duplicate`).
558
+
559
+ ```ts
560
+ const result = await api.sellReceiptWithCompany({ items, payments, idempotencyKey: `order-${orderNumber}` });
561
+ if (result.replayed) showAlreadyIssued(result.receipt); // the receipt of the first attempt
562
+ ```
563
+
548
564
  ## Receipts
549
565
 
550
566
  The facade from `createKasseneckApi` offers `sellReceipt` and
@@ -26,7 +26,10 @@ import type { SheetLogoSize } from '../receipt/blatt.js';
26
26
  * (`receipt_outcome_unknown`, `cancellation_outcome_unknown`, Netz- oder
27
27
  * Serverfehler nach dem Senden) erst nachlesen, und bei
28
28
  * `payments_sum_mismatch` nennt [paymentsExpectedCents] den Betrag des
29
- * Servers; auch dann wiederholt das Paket nie selbst.
29
+ * Servers; auch dann wiederholt das Paket nie selbst. Ausnahme fuer den
30
+ * Aufrufer: ein Verkauf mit `idempotencyKey` darf nach einem unklaren Ausgang
31
+ * unveraendert mit **demselben** Schluessel erneut hinaus (siehe
32
+ * [ReceiptCommonOptions.idempotencyKey]).
30
33
  *
31
34
  * **Kassen-Benutzer-Weg (`registerUserAuth`, Browser-Kasse):** Von den
32
35
  * Endpunkten dieser Datei setzen [listMyReceipts], [getReceipt],
@@ -62,6 +65,27 @@ export interface ReceiptCommonOptions {
62
65
  * Hauptlade der Kasse. Leer oder kein Text: wirft vor dem Senden.
63
66
  */
64
67
  drawerId?: string;
68
+ /**
69
+ * Schutz vor einem zweiten Beleg, wenn die Antwort verloren geht (Netzabbruch,
70
+ * Zeitablauf, `outcome: 'unknown'`). Gilt je Kasse: dieselbe Anfrage mit
71
+ * demselben Schluessel liefert den schon signierten Beleg zurueck, ohne neuen
72
+ * Beleg, ohne Zaehlerschritt (beim `…WithCompany`-Aufruf mit `replayed: true`;
73
+ * Klartext-Codes neu verkaufter Gutscheine nur beim ersten Mal). Derselbe
74
+ * Schluessel mit anderem Inhalt wird mit `idempotency_conflict` abgewiesen,
75
+ * geschrieben wird dann nichts.
76
+ *
77
+ * Empfohlen ist eine eigene, stabile Kennung des Vorgangs (Bestellnummer,
78
+ * Nummer des Ersatzbelegs), nie eine je Versuch neu erzeugte: nach einem
79
+ * unklaren Ausgang geht dieselbe Anfrage mit **demselben** Schluessel erneut
80
+ * hinaus; ein neuer Schluessel waere ein zweiter Beleg. Bei der Nacherfassung
81
+ * ([manualReceipt]) sperrt der Server dieselbe Handbeleg-Nummer am selben Tag
82
+ * zusaetzlich mit `manual_receipt_duplicate`.
83
+ *
84
+ * Text mit 1 bis 120 Zeichen (Laenge wie `String.length`), geht unveraendert
85
+ * hinaus. Leer, nur Leerraum, laenger oder kein Text: wirft vor dem Senden,
86
+ * nie gekuerzt. Ohne Angabe (oder `null`) bleibt der Aufruf wie bisher.
87
+ */
88
+ idempotencyKey?: string;
65
89
  }
66
90
  /** Trinkgeld mit eigener Zahlart und/oder Empfaengern (Kassen-Benutzer-IDs, Summe = cents). */
67
91
  export interface TipOptions {
@@ -231,6 +255,12 @@ export interface ReceiptWithCompany {
231
255
  * faellt weg, der Beleg bleibt lesbar.
232
256
  */
233
257
  outputs?: ReceiptOutput[];
258
+ /**
259
+ * Nur nach einem Verkauf mit `idempotencyKey`: `true`, wenn der Server den
260
+ * schon signierten Beleg zu diesem Schluessel zurueckgab (Wiederholung, kein
261
+ * neuer Beleg). Fehlt sonst, auch beim ersten Verkauf mit Schluessel.
262
+ */
263
+ replayed?: true;
234
264
  }
235
265
  /**
236
266
  * Das Zeilenmodell zum Drucken und Anzeigen eines Belegs aus einer Antwort.
@@ -209,8 +209,25 @@ function createReceiptParams(options) {
209
209
  if (options.drawerId !== undefined) {
210
210
  params['drawerId'] = ladeKennung('createReceipt', options.drawerId);
211
211
  }
212
+ if (options.idempotencyKey != null) {
213
+ params['idempotencyKey'] = belegSchluessel(options.idempotencyKey);
214
+ }
212
215
  return params;
213
216
  }
217
+ /** Hoechstlaenge des `idempotencyKey` am Beleg, wie am Server (beleg-idempotenz-core `SCHLUESSEL_MAX`). */
218
+ const BELEG_SCHLUESSEL_MAX = 120;
219
+ /**
220
+ * Der Idempotenz-Schluessel eines Belegs: dieselbe Regel wie am Server (Text,
221
+ * nicht nur Leerraum, hoechstens 120 Zeichen). Er geht unveraendert hinaus:
222
+ * der Server erkennt die Wiederholung am Schluessel, wie er ankommt, ein
223
+ * gekuerzter oder getrimmter waere ein anderer.
224
+ */
225
+ function belegSchluessel(wert) {
226
+ if (typeof wert !== 'string' || wert.trim() === '' || wert.length > BELEG_SCHLUESSEL_MAX) {
227
+ throw eingabefehler(`idempotencyKey muss ein Text mit 1 bis ${BELEG_SCHLUESSEL_MAX} Zeichen sein (oder weglassen); er wird nie gekuerzt.`);
228
+ }
229
+ return wert;
230
+ }
214
231
  /** Die Lade eines Belegs: ein nicht leerer Text, sonst wirft der Aufruf vor dem Senden. */
215
232
  function ladeKennung(name, wert) {
216
233
  if (typeof wert !== 'string' || wert.trim() === '') {
@@ -1027,6 +1044,8 @@ function belegMitFirmaAusHuelle(daten, functionName) {
1027
1044
  registrationInfo: angaben,
1028
1045
  logoScale: d.logo_scale === 'S' || d.logo_scale === 'M' || d.logo_scale === 'L' || d.logo_scale === 'XL' ? d.logo_scale : 'M',
1029
1046
  ...(outputs !== undefined ? { outputs } : {}),
1047
+ // Nur genau true: ohne Wiederholung bleibt die Form des Ergebnisses wie bisher.
1048
+ ...(d.replayed === true ? { replayed: true } : {}),
1030
1049
  };
1031
1050
  }
1032
1051
  /**
@@ -5,7 +5,9 @@ import { KasseneckApiError } from '../client/errors.js';
5
5
  * [PAYMENT_ERROR_CODES] und die des Rands (`validation`, `dialect_mismatch`, …).
6
6
  *
7
7
  * `receipt_outcome_unknown` heisst: der Beleg ist moeglicherweise signiert
8
- * (`outcome: 'unknown'`). Nie wiederholen, sondern die Belegliste nachlesen.
8
+ * (`outcome: 'unknown'`). Ohne `idempotencyKey` nie wiederholen, sondern die
9
+ * Belegliste nachlesen; mit Schluessel dieselbe Anfrage mit demselben
10
+ * Schluessel erneut senden.
9
11
  *
10
12
  * Dahinter (ab 1.0) die Codes, die Anmeldung und Rand auf diesem Endpunkt
11
13
  * erzeugen koennen: `errorCodes.auth` ohne die sieben des Partner-Zugangs und
@@ -12,7 +12,9 @@ const fehlercodes_js_1 = require("../client/fehlercodes.js");
12
12
  * [PAYMENT_ERROR_CODES] und die des Rands (`validation`, `dialect_mismatch`, …).
13
13
  *
14
14
  * `receipt_outcome_unknown` heisst: der Beleg ist moeglicherweise signiert
15
- * (`outcome: 'unknown'`). Nie wiederholen, sondern die Belegliste nachlesen.
15
+ * (`outcome: 'unknown'`). Ohne `idempotencyKey` nie wiederholen, sondern die
16
+ * Belegliste nachlesen; mit Schluessel dieselbe Anfrage mit demselben
17
+ * Schluessel erneut senden.
16
18
  *
17
19
  * Dahinter (ab 1.0) die Codes, die Anmeldung und Rand auf diesem Endpunkt
18
20
  * erzeugen koennen: `errorCodes.auth` ohne die sieben des Partner-Zugangs und
@@ -29,8 +31,8 @@ exports.RECEIPT_ERROR_CODES = Object.freeze([
29
31
  'cashregister_decommissioned',
30
32
  'final_receipt_expired',
31
33
  'final_receipt_not_allowed',
32
- // Seit 1.15.0 (Vertrag): derselbe idempotencyKey mit anderem Inhalt, nichts geschrieben. Der Server nimmt den
33
- // Schluessel optional an; dieses Paket sendet (noch) keinen, der Code bleibt damit nur lesbar.
34
+ // Seit 1.15.0 (Vertrag): derselbe idempotencyKey mit anderem Inhalt, nichts geschrieben. Seit 1.22.0 sendet
35
+ // dieses Paket den Schluessel, wenn der Aufrufer ihn angibt (`idempotencyKey` an sellReceipt/createReceipt).
34
36
  'idempotency_conflict',
35
37
  'module_inactive',
36
38
  'not_permitted',
@@ -325,7 +325,12 @@ function planeGutscheine(kunde, vouchers, art, o) {
325
325
  if ('fehler' in check)
326
326
  return check;
327
327
  if (v.action === 'payout') {
328
- // `gutschein-core.darfAuszahlen`: Auszahlung nicht gesperrt, aktiv oder abgelaufen bis zum Ende der Rueckzahlbarkeit.
328
+ // `gutschein-core.darfAuszahlen`: storniert und gesperrt zuerst (der Storno setzt den Saldo auf 0, die Kasse soll
329
+ // „storniert“ sagen, nicht „aufgebraucht“), dann Auszahlung nicht gesperrt, aktiv oder abgelaufen bis zum Ende der
330
+ // Rueckzahlbarkeit.
331
+ if (check.status === 'cancelled' || check.status === 'blocked') {
332
+ return nein(STATUS_FEHLER[check.status] ?? 'voucher_invalid', `Gutschein ${i + 1}: Status ${check.status}.`);
333
+ }
329
334
  if (check.payout === 'never')
330
335
  return nein('voucher_payout_not_allowed', `Gutschein ${i + 1}: dieser Gutschein wird nie ausgezahlt.`);
331
336
  if (!((check.balanceCents ?? 0) > 0))
@@ -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.21.0";
7
+ export declare const PACKAGE_VERSION = "1.22.0";
@@ -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.21.0';
10
+ exports.PACKAGE_VERSION = '1.22.0';
@@ -26,7 +26,10 @@ import type { SheetLogoSize } from '../receipt/blatt.js';
26
26
  * (`receipt_outcome_unknown`, `cancellation_outcome_unknown`, Netz- oder
27
27
  * Serverfehler nach dem Senden) erst nachlesen, und bei
28
28
  * `payments_sum_mismatch` nennt [paymentsExpectedCents] den Betrag des
29
- * Servers; auch dann wiederholt das Paket nie selbst.
29
+ * Servers; auch dann wiederholt das Paket nie selbst. Ausnahme fuer den
30
+ * Aufrufer: ein Verkauf mit `idempotencyKey` darf nach einem unklaren Ausgang
31
+ * unveraendert mit **demselben** Schluessel erneut hinaus (siehe
32
+ * [ReceiptCommonOptions.idempotencyKey]).
30
33
  *
31
34
  * **Kassen-Benutzer-Weg (`registerUserAuth`, Browser-Kasse):** Von den
32
35
  * Endpunkten dieser Datei setzen [listMyReceipts], [getReceipt],
@@ -62,6 +65,27 @@ export interface ReceiptCommonOptions {
62
65
  * Hauptlade der Kasse. Leer oder kein Text: wirft vor dem Senden.
63
66
  */
64
67
  drawerId?: string;
68
+ /**
69
+ * Schutz vor einem zweiten Beleg, wenn die Antwort verloren geht (Netzabbruch,
70
+ * Zeitablauf, `outcome: 'unknown'`). Gilt je Kasse: dieselbe Anfrage mit
71
+ * demselben Schluessel liefert den schon signierten Beleg zurueck, ohne neuen
72
+ * Beleg, ohne Zaehlerschritt (beim `…WithCompany`-Aufruf mit `replayed: true`;
73
+ * Klartext-Codes neu verkaufter Gutscheine nur beim ersten Mal). Derselbe
74
+ * Schluessel mit anderem Inhalt wird mit `idempotency_conflict` abgewiesen,
75
+ * geschrieben wird dann nichts.
76
+ *
77
+ * Empfohlen ist eine eigene, stabile Kennung des Vorgangs (Bestellnummer,
78
+ * Nummer des Ersatzbelegs), nie eine je Versuch neu erzeugte: nach einem
79
+ * unklaren Ausgang geht dieselbe Anfrage mit **demselben** Schluessel erneut
80
+ * hinaus; ein neuer Schluessel waere ein zweiter Beleg. Bei der Nacherfassung
81
+ * ([manualReceipt]) sperrt der Server dieselbe Handbeleg-Nummer am selben Tag
82
+ * zusaetzlich mit `manual_receipt_duplicate`.
83
+ *
84
+ * Text mit 1 bis 120 Zeichen (Laenge wie `String.length`), geht unveraendert
85
+ * hinaus. Leer, nur Leerraum, laenger oder kein Text: wirft vor dem Senden,
86
+ * nie gekuerzt. Ohne Angabe (oder `null`) bleibt der Aufruf wie bisher.
87
+ */
88
+ idempotencyKey?: string;
65
89
  }
66
90
  /** Trinkgeld mit eigener Zahlart und/oder Empfaengern (Kassen-Benutzer-IDs, Summe = cents). */
67
91
  export interface TipOptions {
@@ -231,6 +255,12 @@ export interface ReceiptWithCompany {
231
255
  * faellt weg, der Beleg bleibt lesbar.
232
256
  */
233
257
  outputs?: ReceiptOutput[];
258
+ /**
259
+ * Nur nach einem Verkauf mit `idempotencyKey`: `true`, wenn der Server den
260
+ * schon signierten Beleg zu diesem Schluessel zurueckgab (Wiederholung, kein
261
+ * neuer Beleg). Fehlt sonst, auch beim ersten Verkauf mit Schluessel.
262
+ */
263
+ replayed?: true;
234
264
  }
235
265
  /**
236
266
  * Das Zeilenmodell zum Drucken und Anzeigen eines Belegs aus einer Antwort.
@@ -188,8 +188,25 @@ export function createReceiptParams(options) {
188
188
  if (options.drawerId !== undefined) {
189
189
  params['drawerId'] = ladeKennung('createReceipt', options.drawerId);
190
190
  }
191
+ if (options.idempotencyKey != null) {
192
+ params['idempotencyKey'] = belegSchluessel(options.idempotencyKey);
193
+ }
191
194
  return params;
192
195
  }
196
+ /** Hoechstlaenge des `idempotencyKey` am Beleg, wie am Server (beleg-idempotenz-core `SCHLUESSEL_MAX`). */
197
+ const BELEG_SCHLUESSEL_MAX = 120;
198
+ /**
199
+ * Der Idempotenz-Schluessel eines Belegs: dieselbe Regel wie am Server (Text,
200
+ * nicht nur Leerraum, hoechstens 120 Zeichen). Er geht unveraendert hinaus:
201
+ * der Server erkennt die Wiederholung am Schluessel, wie er ankommt, ein
202
+ * gekuerzter oder getrimmter waere ein anderer.
203
+ */
204
+ function belegSchluessel(wert) {
205
+ if (typeof wert !== 'string' || wert.trim() === '' || wert.length > BELEG_SCHLUESSEL_MAX) {
206
+ throw eingabefehler(`idempotencyKey muss ein Text mit 1 bis ${BELEG_SCHLUESSEL_MAX} Zeichen sein (oder weglassen); er wird nie gekuerzt.`);
207
+ }
208
+ return wert;
209
+ }
193
210
  /** Die Lade eines Belegs: ein nicht leerer Text, sonst wirft der Aufruf vor dem Senden. */
194
211
  function ladeKennung(name, wert) {
195
212
  if (typeof wert !== 'string' || wert.trim() === '') {
@@ -1006,6 +1023,8 @@ export function belegMitFirmaAusHuelle(daten, functionName) {
1006
1023
  registrationInfo: angaben,
1007
1024
  logoScale: d.logo_scale === 'S' || d.logo_scale === 'M' || d.logo_scale === 'L' || d.logo_scale === 'XL' ? d.logo_scale : 'M',
1008
1025
  ...(outputs !== undefined ? { outputs } : {}),
1026
+ // Nur genau true: ohne Wiederholung bleibt die Form des Ergebnisses wie bisher.
1027
+ ...(d.replayed === true ? { replayed: true } : {}),
1009
1028
  };
1010
1029
  }
1011
1030
  /**
@@ -5,7 +5,9 @@ import { KasseneckApiError } from '../client/errors.js';
5
5
  * [PAYMENT_ERROR_CODES] und die des Rands (`validation`, `dialect_mismatch`, …).
6
6
  *
7
7
  * `receipt_outcome_unknown` heisst: der Beleg ist moeglicherweise signiert
8
- * (`outcome: 'unknown'`). Nie wiederholen, sondern die Belegliste nachlesen.
8
+ * (`outcome: 'unknown'`). Ohne `idempotencyKey` nie wiederholen, sondern die
9
+ * Belegliste nachlesen; mit Schluessel dieselbe Anfrage mit demselben
10
+ * Schluessel erneut senden.
9
11
  *
10
12
  * Dahinter (ab 1.0) die Codes, die Anmeldung und Rand auf diesem Endpunkt
11
13
  * erzeugen koennen: `errorCodes.auth` ohne die sieben des Partner-Zugangs und
@@ -5,7 +5,9 @@ import { bekannterCode, feldfehlerVon } from '../client/fehlercodes.js';
5
5
  * [PAYMENT_ERROR_CODES] und die des Rands (`validation`, `dialect_mismatch`, …).
6
6
  *
7
7
  * `receipt_outcome_unknown` heisst: der Beleg ist moeglicherweise signiert
8
- * (`outcome: 'unknown'`). Nie wiederholen, sondern die Belegliste nachlesen.
8
+ * (`outcome: 'unknown'`). Ohne `idempotencyKey` nie wiederholen, sondern die
9
+ * Belegliste nachlesen; mit Schluessel dieselbe Anfrage mit demselben
10
+ * Schluessel erneut senden.
9
11
  *
10
12
  * Dahinter (ab 1.0) die Codes, die Anmeldung und Rand auf diesem Endpunkt
11
13
  * erzeugen koennen: `errorCodes.auth` ohne die sieben des Partner-Zugangs und
@@ -22,8 +24,8 @@ export const RECEIPT_ERROR_CODES = Object.freeze([
22
24
  'cashregister_decommissioned',
23
25
  'final_receipt_expired',
24
26
  'final_receipt_not_allowed',
25
- // Seit 1.15.0 (Vertrag): derselbe idempotencyKey mit anderem Inhalt, nichts geschrieben. Der Server nimmt den
26
- // Schluessel optional an; dieses Paket sendet (noch) keinen, der Code bleibt damit nur lesbar.
27
+ // Seit 1.15.0 (Vertrag): derselbe idempotencyKey mit anderem Inhalt, nichts geschrieben. Seit 1.22.0 sendet
28
+ // dieses Paket den Schluessel, wenn der Aufrufer ihn angibt (`idempotencyKey` an sellReceipt/createReceipt).
27
29
  'idempotency_conflict',
28
30
  'module_inactive',
29
31
  'not_permitted',
@@ -316,7 +316,12 @@ export function planeGutscheine(kunde, vouchers, art, o) {
316
316
  if ('fehler' in check)
317
317
  return check;
318
318
  if (v.action === 'payout') {
319
- // `gutschein-core.darfAuszahlen`: Auszahlung nicht gesperrt, aktiv oder abgelaufen bis zum Ende der Rueckzahlbarkeit.
319
+ // `gutschein-core.darfAuszahlen`: storniert und gesperrt zuerst (der Storno setzt den Saldo auf 0, die Kasse soll
320
+ // „storniert“ sagen, nicht „aufgebraucht“), dann Auszahlung nicht gesperrt, aktiv oder abgelaufen bis zum Ende der
321
+ // Rueckzahlbarkeit.
322
+ if (check.status === 'cancelled' || check.status === 'blocked') {
323
+ return nein(STATUS_FEHLER[check.status] ?? 'voucher_invalid', `Gutschein ${i + 1}: Status ${check.status}.`);
324
+ }
320
325
  if (check.payout === 'never')
321
326
  return nein('voucher_payout_not_allowed', `Gutschein ${i + 1}: dieser Gutschein wird nie ausgezahlt.`);
322
327
  if (!((check.balanceCents ?? 0) > 0))
@@ -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.21.0";
7
+ export declare const PACKAGE_VERSION = "1.22.0";
@@ -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.21.0';
7
+ export const PACKAGE_VERSION = '1.22.0';
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.21.0",
2
+ "version": "1.22.0",
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.21.0",
5
+ "package": "1.22.0",
6
6
  "endpoints": {
7
7
  "createCustomer": {
8
8
  "request": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.21.0",
2
+ "version": "1.22.0",
3
3
  "languages": [
4
4
  "de",
5
5
  "en"
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.21.0",
2
+ "version": "1.22.0",
3
3
  "messages": {
4
4
  "network.no_connection": {
5
5
  "text": "Keine Verbindung zum Server. Bitte die Internetverbindung prüfen und erneut versuchen."
@@ -3,7 +3,7 @@
3
3
  "_source": {
4
4
  "beleg-toepfe.js": "60630493a589469d1cdfeea6fef667d20feb8f3a156819ba4d90b21501fdc4ea",
5
5
  "gutschein-beleg.js": "36750aa4eec8d94f57c2eebce0d34d13c7ffb3f0f5ea558fac1f56f15ff50004",
6
- "gutschein-core.js": "8eeb04a12f771c6b70b23ab7a361354745182ab18235213d3c2f1f9191ebadef",
6
+ "gutschein-core.js": "b4d3188c666a43e3be8cc1c2c54e5b8de24e10db785e33bb2ce81aa38ae47929",
7
7
  "tip-core.js": "2d5b65c3cbe96935c8537d35f02b30906a7477afa340e18325b60bd60af1c7d9",
8
8
  "vat-buckets.js": "92c69632c0120dbb09a22cef5a36b052e81605d99907eeda7289b0d3db78454c",
9
9
  "zahlungen-core.js": "7ea8b07dfd222741049a47bec0149953d5696b0e1e089bf9919d4bd2cafff530"
@@ -491,6 +491,10 @@
491
491
  {"name":"gen_voucher_478_single_purpose_exempt","input":{"receiptType":"standard","items":[{"name":"P","quantity":1,"priceCents":2521,"vatRate":4.9,"articleId":"item_mani"},{"name":"P","quantity":2.75,"priceCents":2266,"vatRate":13},{"name":"P","quantity":2,"priceCents":64,"vatRate":10,"articleId":"item_mani"},{"name":"Discount","quantity":1,"priceCents":-146,"vatRate":13,"kind":"discount"}],"vouchers":[{"type":"single_purpose","action":"redeem","code":"DTD5-9ECF-5KNZ","amountCents":58}],"today":"2026-10-19","voucherInfo":[{"code":"DTD5-9ECF-5KNZ","check":{"type":"single_purpose","status":"active","codeSuffix":"5KNZ","balanceCents":2402,"vatRateBp":0,"exempt":true}}]},"expected":{"dueCents":8677,"counterDeltaCents":8677,"valueVoucherFlowCents":0,"bucketsCents":{"amountRateStandard":0,"amountRateReduced1":127,"amountRateReduced2":6046,"amountRateZero":0,"amountRateSpecial":2504,"amountRatOthers":0}}},
492
492
  {"name":"gen_voucher_479_sell_multi_purpose","input":{"receiptType":"standard","items":[{"name":"P","quantity":3,"priceCents":80,"vatRate":4.9}],"vouchers":[{"action":"sell","type":"multi_purpose","typeId":"vt_mehrzweck_479","amountCents":7217}],"today":"2100-01-04","voucherTypes":[{"id":"vt_mehrzweck_479","kind":"multi_purpose","vatRateBp":null,"amount":{"mode":"fixed","cents":7217}}],"smallBusiness":true},"expected":{"dueCents":7457,"counterDeltaCents":7457,"valueVoucherFlowCents":0,"bucketsCents":{"amountRateStandard":0,"amountRateReduced1":0,"amountRateReduced2":0,"amountRateZero":7217,"amountRateSpecial":240,"amountRatOthers":0}}},
493
493
  {"name":"gen_voucher_480_redeem_wrong_kind","input":{"receiptType":"standard","items":[{"name":"P","quantity":1,"priceCents":2000,"vatRate":20}],"vouchers":[{"type":"single_purpose","action":"redeem","code":"DTD5-9ECF-5KNZ","amountCents":1000}],"today":"2026-10-19","voucherInfo":[{"code":"DTD5-9ECF-5KNZ","check":{"type":"multi_purpose","status":"active","codeSuffix":"5KNZ","balanceCents":5000}}]},"expected":{"voucherError":"voucher_invalid"}},
494
- {"name":"gen_voucher_481_redeem_wrong_kind","input":{"receiptType":"standard","items":[{"name":"P","quantity":1,"priceCents":2000,"vatRate":20}],"vouchers":[{"type":"multi_purpose","action":"redeem","code":"DTD5-9ECF-5KNZ","amountCents":1000}],"today":"2026-10-19","voucherInfo":[{"code":"DTD5-9ECF-5KNZ","check":{"type":"single_purpose","status":"active","codeSuffix":"5KNZ","balanceCents":5000,"vatRateBp":2000}}]},"expected":{"voucherError":"voucher_invalid"}}
494
+ {"name":"gen_voucher_481_redeem_wrong_kind","input":{"receiptType":"standard","items":[{"name":"P","quantity":1,"priceCents":2000,"vatRate":20}],"vouchers":[{"type":"multi_purpose","action":"redeem","code":"DTD5-9ECF-5KNZ","amountCents":1000}],"today":"2026-10-19","voucherInfo":[{"code":"DTD5-9ECF-5KNZ","check":{"type":"single_purpose","status":"active","codeSuffix":"5KNZ","balanceCents":5000,"vatRateBp":2000}}]},"expected":{"voucherError":"voucher_invalid"}},
495
+ {"name":"gen_voucher_482_payout_multi_purpose_cancelled","input":{"receiptType":"standard","items":[],"vouchers":[{"type":"multi_purpose","action":"payout","code":"DTD5-9ECF-5KNZ","amountCents":1000,"reason":"Goodwill"}],"today":"2026-10-19","voucherInfo":[{"code":"DTD5-9ECF-5KNZ","check":{"type":"multi_purpose","status":"cancelled","codeSuffix":"5KNZ","balanceCents":0}}]},"expected":{"voucherError":"voucher_cancelled"}},
496
+ {"name":"gen_voucher_483_payout_single_purpose_cancelled","input":{"receiptType":"standard","items":[],"vouchers":[{"type":"single_purpose","action":"payout","code":"DTD5-9ECF-5KNZ","amountCents":1000,"reason":"Goodwill"}],"today":"2026-10-19","voucherInfo":[{"code":"DTD5-9ECF-5KNZ","check":{"type":"single_purpose","status":"cancelled","codeSuffix":"5KNZ","payout":"never","balanceCents":0,"vatRateBp":2000}}]},"expected":{"voucherError":"voucher_cancelled"}},
497
+ {"name":"gen_voucher_484_payout_multi_purpose_blocked","input":{"receiptType":"standard","items":[],"vouchers":[{"type":"multi_purpose","action":"payout","code":"DTD5-9ECF-5KNZ","amountCents":1000,"reason":"Goodwill"}],"today":"2026-10-19","voucherInfo":[{"code":"DTD5-9ECF-5KNZ","check":{"type":"multi_purpose","status":"blocked","codeSuffix":"5KNZ","payout":"never","balanceCents":5000}}]},"expected":{"voucherError":"voucher_blocked"}},
498
+ {"name":"gen_voucher_485_payout_multi_purpose_used_up","input":{"receiptType":"standard","items":[],"vouchers":[{"type":"multi_purpose","action":"payout","code":"DTD5-9ECF-5KNZ","amountCents":1000,"reason":"Goodwill"}],"today":"2026-10-19","voucherInfo":[{"code":"DTD5-9ECF-5KNZ","check":{"type":"multi_purpose","status":"used_up","codeSuffix":"5KNZ","balanceCents":0}}]},"expected":{"voucherError":"voucher_used_up"}}
495
499
  ]
496
500
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.21.0",
2
+ "version": "1.22.0",
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.21.0",
3
+ "version": "1.22.0",
4
4
  "description": "Austrian fiscal cash register (RKSV) client for JavaScript and TypeScript: signed receipts, cancellations, card payments, receipt printing (ESC/POS), invoices, inventory. RKSV-Registrierkasse für JavaScript und TypeScript.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Kreiseck",