@kreiseck/kasseneck-api 1.4.0 → 1.5.1

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 (105) hide show
  1. package/CHANGELOG.md +166 -0
  2. package/README.md +255 -36
  3. package/dist/cjs/client/aufrufe.d.ts +58 -4
  4. package/dist/cjs/client/aufrufe.js +196 -1
  5. package/dist/cjs/client/errors.d.ts +32 -19
  6. package/dist/cjs/client/errors.js +28 -17
  7. package/dist/cjs/client/index.d.ts +2 -2
  8. package/dist/cjs/client/index.js +2 -1
  9. package/dist/cjs/client/transport.d.ts +27 -1
  10. package/dist/cjs/client/transport.js +25 -27
  11. package/dist/cjs/index.d.ts +1 -1
  12. package/dist/cjs/index.js +3 -2
  13. package/dist/cjs/inventory/api.d.ts +19 -3
  14. package/dist/cjs/inventory/api.js +18 -2
  15. package/dist/cjs/inventory/endpunkte.d.ts +22 -2
  16. package/dist/cjs/inventory/endpunkte.js +6 -1
  17. package/dist/cjs/inventory/fehler.d.ts +11 -1
  18. package/dist/cjs/inventory/fehler.js +19 -0
  19. package/dist/cjs/inventory/index.d.ts +6 -4
  20. package/dist/cjs/inventory/index.js +32 -2
  21. package/dist/cjs/inventory/lesen.d.ts +8 -1
  22. package/dist/cjs/inventory/lesen.js +131 -1
  23. package/dist/cjs/inventory/schreiben.d.ts +86 -0
  24. package/dist/cjs/inventory/schreiben.js +320 -0
  25. package/dist/cjs/inventory/typen.d.ts +371 -11
  26. package/dist/cjs/inventory/vertrag.d.ts +76 -16
  27. package/dist/cjs/inventory/vertrag.js +138 -13
  28. package/dist/cjs/inventory/webhook.d.ts +3 -1
  29. package/dist/cjs/inventory/webhook.js +7 -1
  30. package/dist/cjs/invoice/endpunkte.d.ts +6 -0
  31. package/dist/cjs/invoice/endpunkte.js +13 -2
  32. package/dist/cjs/invoice/index.d.ts +2 -2
  33. package/dist/cjs/invoice/index.js +2 -1
  34. package/dist/cjs/invoice/typen.d.ts +46 -2
  35. package/dist/cjs/invoice/vertrag.d.ts +19 -5
  36. package/dist/cjs/invoice/vertrag.js +37 -5
  37. package/dist/cjs/pos/texte.d.ts +11 -6
  38. package/dist/cjs/pos/texte.js +11 -6
  39. package/dist/cjs/stored/vokabular.d.ts +1 -1
  40. package/dist/cjs/stored/vokabular.js +5 -3
  41. package/dist/cjs/version.d.ts +1 -1
  42. package/dist/cjs/version.js +1 -1
  43. package/dist/esm/client/aufrufe.d.ts +58 -4
  44. package/dist/esm/client/aufrufe.js +194 -0
  45. package/dist/esm/client/errors.d.ts +32 -19
  46. package/dist/esm/client/errors.js +28 -17
  47. package/dist/esm/client/index.d.ts +2 -2
  48. package/dist/esm/client/index.js +1 -1
  49. package/dist/esm/client/transport.d.ts +27 -1
  50. package/dist/esm/client/transport.js +26 -28
  51. package/dist/esm/index.d.ts +1 -1
  52. package/dist/esm/index.js +1 -1
  53. package/dist/esm/inventory/api.d.ts +19 -3
  54. package/dist/esm/inventory/api.js +18 -2
  55. package/dist/esm/inventory/endpunkte.d.ts +22 -2
  56. package/dist/esm/inventory/endpunkte.js +6 -5
  57. package/dist/esm/inventory/fehler.d.ts +11 -1
  58. package/dist/esm/inventory/fehler.js +18 -1
  59. package/dist/esm/inventory/index.d.ts +6 -4
  60. package/dist/esm/inventory/index.js +5 -3
  61. package/dist/esm/inventory/lesen.d.ts +8 -1
  62. package/dist/esm/inventory/lesen.js +127 -1
  63. package/dist/esm/inventory/schreiben.d.ts +86 -0
  64. package/dist/esm/inventory/schreiben.js +303 -0
  65. package/dist/esm/inventory/typen.d.ts +371 -11
  66. package/dist/esm/inventory/vertrag.d.ts +76 -16
  67. package/dist/esm/inventory/vertrag.js +137 -12
  68. package/dist/esm/inventory/webhook.d.ts +3 -1
  69. package/dist/esm/inventory/webhook.js +8 -2
  70. package/dist/esm/invoice/endpunkte.d.ts +6 -0
  71. package/dist/esm/invoice/endpunkte.js +13 -2
  72. package/dist/esm/invoice/index.d.ts +2 -2
  73. package/dist/esm/invoice/index.js +1 -1
  74. package/dist/esm/invoice/typen.d.ts +46 -2
  75. package/dist/esm/invoice/vertrag.d.ts +19 -5
  76. package/dist/esm/invoice/vertrag.js +36 -4
  77. package/dist/esm/pos/texte.d.ts +11 -6
  78. package/dist/esm/pos/texte.js +11 -6
  79. package/dist/esm/stored/vokabular.d.ts +1 -1
  80. package/dist/esm/stored/vokabular.js +5 -3
  81. package/dist/esm/version.d.ts +1 -1
  82. package/dist/esm/version.js +1 -1
  83. package/fixtures/hobex-hps-codes.json +1 -1
  84. package/fixtures/invoice-api-examples/credit-error-reservation.json +26 -0
  85. package/fixtures/invoice-api-examples/issue-error-reservation-article.json +25 -0
  86. package/fixtures/invoice-api-examples/issue-error-vat-id-risk.json +26 -0
  87. package/fixtures/invoice-api-examples/issue-reservation.json +24 -0
  88. package/fixtures/invoice-api-examples/issue-vat-id-risk.json +28 -0
  89. package/fixtures/invoice-api.schema.json +15 -2
  90. package/fixtures/invoice-texts.json +1 -1
  91. package/fixtures/pos-texts.json +1 -1
  92. package/fixtures/surface.json +206 -9
  93. package/fixtures/v3/antworten/belege.json +2 -2
  94. package/fixtures/v3/antworten/belegmail.json +2 -2
  95. package/fixtures/v3/antworten/kasse-belege.json +2 -2
  96. package/fixtures/v3/antworten/kasse.json +3 -3
  97. package/fixtures/v3/antworten/lager.json +2491 -130
  98. package/fixtures/v3/antworten/rechnungen.json +267 -44
  99. package/fixtures/v3/antworten/storno.json +2 -2
  100. package/fixtures/v3/stored/belege.json +2 -2
  101. package/fixtures/v3/stored/kasse.json +3 -3
  102. package/fixtures/v3/stored/rechnungen.json +174 -18
  103. package/fixtures/v3/v3-vokabular.json +593 -23
  104. package/fixtures/v3/zahlbetrag-faelle.json +2 -2
  105. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,172 @@ 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.5.1
8
+
9
+ Every call with an effect reports `outcome: 'unknown'` when it fails after
10
+ sending began: network error, timeout, HTTP 5xx, HTTP 200 with the `/v3`
11
+ marker but an empty, non-JSON or status-less body, and HTML with the marker.
12
+ Reason: until 1.5.0 only the six signing and money calls did; a goods receipt,
13
+ a reservation or `issueInvoice` came back as `'rejected'` ("did not happen")
14
+ although the server may have booked it. Whoever believed that and resent with
15
+ a **new** `idempotencyKey` booked twice.
16
+
17
+ A change to the safe side, no change on the wire; requests send the same body
18
+ as in 1.5.0 (only the `Kasseneck-Client` header carries the new version).
19
+
20
+ - **Which calls**: `UNKNOWN_OUTCOME_CALLS` (exported, sorted), also in
21
+ `fixtures/surface.json` as `unknownOutcomeCalls` for the Dart twin. New in it:
22
+ inventory writes (`createArticle`, `updateArticle`, `deactivateArticle`,
23
+ `receiveGoods`, `transferStock`, `recordStockLoss`, `changeStockCondition`,
24
+ `reverseStockMovement`), reservations (`createReservation`,
25
+ `extendReservation`, `releaseReservation`), inventory and partner webhooks
26
+ (create, update, delete, rotate the secret, test delivery), the invoice API
27
+ (`issueInvoice`, `cancelInvoice`, `createCreditNote`, `recordInvoicePayment`,
28
+ `createCustomer`, `updateCustomer`), invoice items and SEPA mandates
29
+ (`createInvoiceItem`, `updateInvoiceItem`, `withdrawInvoiceItem`,
30
+ `setCustomerMandate`, `revokeCustomerMandate`), the partner calls with an
31
+ effect (`createPartnerCustomer`, `sendPartnerCustomerFonLink`,
32
+ `requestCustomerSignature`, `createCustomerCashregister`,
33
+ `activateCashregister`, `reportCustomerContract`), and on the register
34
+ `pairRegisterDevice`, `unpairRegisterDevice`, `setMyKasseSettings`,
35
+ `setMyKasseLogo`, `setMyRegisterDeviceSettings`,
36
+ `setMyCashregisterStockLocation`, `createPrintJob`, `sendReceiptEmail`.
37
+ - **Which stay `'rejected'`**: reading calls, dry runs (`previewGoodsReceipt`,
38
+ `previewInvoice`, `issueInvoice` with `dryRun: true`), the register sign-in
39
+ sessions (`registerUserLogin`, `registerPinLogin`, `renewRegisterSession`,
40
+ `endRegisterSession`) and `createPaymentLinkStripe`: a repeat books nothing.
41
+ A timeout already during sign-in stays `'rejected'` as before: nothing was
42
+ sent.
43
+ - **What to do**: with an `idempotencyKey`, send the same request with the
44
+ **same** key; it takes effect once and returns the stored answer.
45
+ `createInvoiceItem` is recognised by `source` + `reference`: the same values
46
+ again are safe. Without a key (receipts, cancellations, money calls,
47
+ settings, webhooks, changing or withdrawing invoice items, mandates), read
48
+ the state first. The rule for receipts and payments is unchanged: look it up,
49
+ never resend.
50
+ - **Per call**: the transport takes a fifth argument `{ hasEffect }`
51
+ (`TransportCallOptions`). `true` marks a call with an effect that this
52
+ package does not know. `false` only counts together with
53
+ `params.dryRun === true` (a dry run under the name of the real call; the
54
+ package's own previews use it); without `dryRun: true` a call from the list
55
+ stays `'unknown'`, so no option can turn a real booking into `'rejected'`.
56
+ Without the option the list decides, and an unknown name counts as having no
57
+ effect.
58
+ - **Guard**: every call the package knows (`ALL_CALLS`, `PUBLIC_CALLS`,
59
+ `POS_CALLS`) is classified as with or without effect; a new call without a
60
+ classification fails the tests.
61
+ - `CALLS_WITH_EFFECT` of the register texts and `fixtures/pos-texts.json` are
62
+ unchanged; `messageOutcome` follows the transport first, so the register
63
+ shows the careful sentence for every call above.
64
+
65
+ ## 1.5.0
66
+
67
+ Inventory API, write side and reservations: `./inventory` gains the 13
68
+ endpoints of backend stage 5b, and the invoice API learns `reservationId`.
69
+ Reason: a shop that only reads stock still oversells. It has to hold the goods
70
+ at checkout, book what arrives and leaves, keep its articles in sync, and turn
71
+ the reservation into a sale when the order is invoiced, all with retries that
72
+ never book twice. The invoice API also catches up with the VAT ID check of
73
+ the backend (stage 4, in the 0.x line since 0.33.0).
74
+
75
+ Additive, no breaking change; existing calls send the same bytes as in 1.4.0.
76
+ New response fields are added to the models.
77
+
78
+ - **Articles**: `createArticle`, `updateArticle`, `deactivateArticle`, each
79
+ answering with the article as `getArticle` returns it. Fields: `name`,
80
+ `description`, `unitPriceCents`, `vatRate`, `unit`, `number`, `ean` (with
81
+ `ean` a foreign article with that code, otherwise the server assigns the next
82
+ own code), `groupId`, `revenueGroupId`, `stockTracked`, `stockLocationIds`,
83
+ `minStockByLocation`, `minStock` (legacy, triggers nothing), `stockKind`,
84
+ `purchasePriceMicros` (only with the permission `costs`), `externalIds` and
85
+ `metadata`. In an update `null` clears an optional field; `externalIds` and
86
+ `metadata` are replaced, `minStockByLocation` is merged per location.
87
+ - **Booking**: `receiveGoods`, `transferStock`, `recordStockLoss`,
88
+ `changeStockCondition`, `reverseStockMovement`, each answering with a
89
+ `StockOperation` (`operationId`, `movementIds`, `lotIds`, `warnings[]`).
90
+ `previewGoodsReceipt` is `receiveGoods` with `dryRun: true`: it books nothing,
91
+ needs no key (a `null` key counts as none and is not sent) and returns the
92
+ values only with the permission `costs`. `receiveGoods` with `dryRun: true`
93
+ is refused before sending: a preview answers without an operation, so it
94
+ goes through `previewGoodsReceipt`.
95
+ Transfers, losses and condition changes never overdraw (`exceeds_stock`).
96
+ Catalogs `STOCK_LOSS_REASONS`, `WITHDRAWAL_TYPES`, `LANDED_COST_TYPES`,
97
+ `LANDED_COST_ALLOCATIONS`; warnings `INVENTORY_WARNING_CODES`
98
+ (`insufficient_stock`, `below_minimum`, `return_exceeds_sale`,
99
+ `reservation_exceeded`) with `isInventoryWarningCode`. A warning is not an
100
+ error: the booking took effect.
101
+ - **Reservations**: `createReservation` (all or nothing, measured against
102
+ `available`; `insufficient_available` lists the missing positions in
103
+ `inventoryShortfalls(error)`), `extendReservation`, `releaseReservation` (all,
104
+ or per position), `getReservation`, `listReservations` and
105
+ `iterateReservations`. `Reservation` carries `status` (`active`, `redeemed`,
106
+ `released`, `expired`; `RESERVATION_STATUSES`), `reference`, `items[]` with
107
+ `quantity`, `redeemed`, `released`, and `expiresAt`. `expiresInMinutes` is
108
+ 5 to 43,200.
109
+ - **Idempotency**: every write takes `idempotencyKey` (1 to 120 characters,
110
+ required in the type). The client refuses to send a write without a valid
111
+ key, because a write without one cannot be retried safely; it never trims or
112
+ shortens the key. Retry after a timeout with the **same** key: the call takes
113
+ effect once and returns the stored answer; the same key with other content
114
+ gives `idempotency_conflict`.
115
+ - **Checked before sending**, nothing else: the key, required ids, integer
116
+ quantities, amounts and prices (`1.5` is never 1.5 pieces), the range of
117
+ `expiresInMinutes`, an update without a field, an empty release list,
118
+ `receiveGoods` with `dryRun: true` (use `previewGoodsReceipt`). All of
119
+ these throw `KasseneckValidationError` with `scope: 'request'`.
120
+ - **Models**: `Article` gains `description`, `stockKind` and
121
+ `minStockByLocation` (missing on an older server: `null`, `null`, `{}`).
122
+ `StockMovement` gains `reservedDelta` (0 for every movement that is not a
123
+ reservation) and `stockAfter.reserved` (only on reservation movements,
124
+ otherwise `null`); the movement type `reservation` has `quantityDelta: 0`.
125
+ The minimum stock (`below_minimum`, `listStock({ belowMinimum })`,
126
+ `stock.below_minimum`) is now measured against `available = onHand −
127
+ reserved`, so a reservation alone can trigger it. A fractional value in any
128
+ of these fields throws `KasseneckValidationError` with `scope: 'response'`.
129
+ - **Events**: `reservation.expired`, `reservation.released` and
130
+ `reservation.redeemed` in `INVENTORY_WEBHOOK_EVENTS` and
131
+ `parseInventoryWebhookEvent`, carrying the reservation with its status
132
+ afterwards. `released` and `redeemed` fire on every effective release or
133
+ redemption, also a partial one (status stays `active`).
134
+ - **Errors**: `INVENTORY_ERROR_CODES` keeps its 12 codes in front and appends
135
+ the codes of the write side in contract order (`idempotency_key_required`,
136
+ `idempotency_conflict`, `exceeds_stock`, `code_taken`,
137
+ `external_id_taken`, `stock_kind_locked`, `article_inactive`,
138
+ `insufficient_available`, `reservation_not_found`,
139
+ `reservation_not_active` …).
140
+ - **Invoice API, reservations**: `issueInvoice` items take `reservationId`
141
+ (`IssueInvoiceItemInput`, needs `articleId`; not on credit notes, where the
142
+ server rejects it). New codes at the end of `INVOICE_ERROR_CODES`:
143
+ `reservation_not_found`, `reservation_mismatch`, `reservation_not_active`;
144
+ new notice `reservation_expired` (with `reservationId`): the invoice is
145
+ issued anyway and sells without the reservation. Three new examples in
146
+ `fixtures/invoice-api-examples/`.
147
+ - **Invoice API, VAT ID check**: an invoice without VAT that relies on the
148
+ customer's VAT ID (intra-Community supply, reverse charge) is only issued
149
+ with a result of the VAT ID check (FinanzOnline, otherwise VIES) on the day
150
+ of issue. Reason: the caller has to decide by the code whether to retry
151
+ later or not to issue at all.
152
+ - Codes `vat_id_invalid` and `vat_id_check_pending` in
153
+ `INVOICE_ERROR_CODES`, after `amount_too_large` and before the
154
+ reservation codes (the backend order). `vat_id_check_pending` carries
155
+ `details.retryAfter` (seconds until the next sensible attempt, with the
156
+ same `idempotencyKey`).
157
+ - `IssueInvoiceRequest.acceptVatIdRisk` (optional boolean): issues despite
158
+ a pending check, the issuer bears the risk. An invalid VAT ID blocks even
159
+ with it. It does not count towards the idempotency of the request.
160
+ - `Invoice.vatIdProof` (`InvoiceVatIdProof`: `checkedOn`, `source`
161
+ `finanzonline` | `vies`, `level` 1 | 2, `code`) and `Invoice.vatIdRisk`
162
+ (`InvoiceVatIdRisk`: `acceptedOn`), otherwise `null`.
163
+ - `fixtures/invoice-api.schema.json` regenerated (`acceptVatIdRisk`, the
164
+ two codes), two new examples `issue-vat-id-risk.json` and
165
+ `issue-error-vat-id-risk.json`.
166
+ - **Contract**: `fixtures/v3/` pulled again from the backend (the 13 new
167
+ endpoints, catalogs, `warningCodes`, events and the 5b cases in
168
+ `antworten/lager.json`, the VAT ID cases in `antworten/rechnungen.json`);
169
+ `PUBLIC_CALLS` and `ALL_CALLS` gain the 13 names (87 public calls);
170
+ `surface.json` gains the new lists under `inventory` and the two invoice
171
+ codes.
172
+
7
173
  ## 1.4.0
8
174
 
9
175
  Inventory API, read side and account webhooks: new subpath `./inventory`.
package/README.md CHANGED
@@ -212,7 +212,7 @@ adapter:
212
212
  | `…/stored` | Stored Firestore documents (inner form, German) as the same English models the `/v3` wire returns: receipts with company and layout, register settings, articles. For clients that read Firestore directly, such as the admin panel. |
213
213
  | `…/invoice` | Invoice API: create and search customers, issue finalised invoices, credit notes and cancellation, PDF and e-invoice XML, the contract as data. **Belongs on a server.** |
214
214
  | `…/invoice/calc` | Pure calculation core for invoice totals (integers, no transport, no dependency beyond types). Safe to run in the browser. |
215
- | `…/inventory` | Inventory API: read articles, locations, stock and the stock ledger, manage account webhooks, verify and parse incoming stock and article events. **Belongs on a server.** |
215
+ | `…/inventory` | Inventory API: read articles, locations, stock and the stock ledger; create and update articles, book goods receipts, transfers and losses, reserve stock at checkout; manage account webhooks, verify and parse incoming stock, article and reservation events. **Belongs on a server.** |
216
216
  | `…/react` | Thin React adapter that renders a receipt layout or a receipt sheet. Needs React. |
217
217
  | `…/fixtures/*` | Golden receipts (JSON): inputs `receipts/<name>.json`, promised line output `expected/<name>.lines.json`, `manifest.json` with checksums. The backend, the browser register and the Flutter package check against the same files. |
218
218
 
@@ -433,30 +433,42 @@ receipt email, and values outside a settings field's value list throw a
433
433
  `KasseneckValidationError` before anything is sent, instead of being dropped
434
434
  silently. For a layout option of 0.x the message names its English successor.
435
435
 
436
- ### `outcome: 'unknown'`: never retry, look it up
436
+ ### `outcome: 'unknown'`: never resend blindly
437
437
 
438
438
  `KasseneckApiError`, `KasseneckHttpError` and `KasseneckNetworkError` carry
439
439
  `outcome`. `'rejected'` means the server turned the request down; whether
440
440
  anything else is safe to do depends on the code, and on the money calls it
441
441
  is only set for the codes listed below. `'unknown'` means the operation **may have been carried out**, for
442
- `createReceipt` a signed receipt in the chain. Then never send it again: read
443
- the result back (`getReceipt`, `listMyReceipts`, the original of a
444
- cancellation) and continue from there. `isOutcomeUnknown(error)` covers all
445
- three classes. The outcome is unknown for:
442
+ `createReceipt` a signed receipt in the chain. Then never send it again blindly:
443
+ read the result back (`getReceipt`, `listMyReceipts`, the original of a
444
+ cancellation) and continue from there. A call with an `idempotencyKey` (or,
445
+ for invoice items, the same `source` and `reference`) is the exception: send
446
+ it again unchanged (see below).
447
+ `isOutcomeUnknown(error)` covers all three classes. The outcome is unknown for:
446
448
 
447
449
  - `dialect_mismatch`, `receipt_outcome_unknown`, `cancellation_outcome_unknown`;
448
450
  - `response_translation_failed`, unless `details.handled === false`;
449
451
  - `response_unreadable`: a signing or money-moving call reported success,
450
452
  but the response lacks what the call promises (no receipt, no reference, no
451
453
  remaining quantities, no Hobex receipt, no captured payment intent);
452
- - on the calls with an effect: the signing calls `createReceipt`,
453
- `cancelReceipt` and `financeWebService`, and the money calls `hobexPay`
454
- (`hobexPayApi`, charges a card), `hobexRefund` (`hobexRefundApi`) and
455
- `stripeCaptureIntent`: a network error or timeout after sending began, HTTP
456
- 5xx, and HTTP 200 with the `Kasseneck-Api-Version: v3` marker but an empty,
454
+ - on every call with an effect (`UNKNOWN_OUTCOME_CALLS`, also in
455
+ `fixtures/surface.json` as `unknownOutcomeCalls`): the signing calls
456
+ `createReceipt`, `cancelReceipt` and `financeWebService`, the money calls
457
+ `hobexPay` (`hobexPayApi`, charges a card), `hobexRefund`
458
+ (`hobexRefundApi`) and `stripeCaptureIntent`, and since 1.5.1 every other
459
+ call that books, issues, creates, changes, deletes or sends something:
460
+ inventory writes and reservations, webhooks, invoices, credit notes,
461
+ recorded payments, customers, invoice items and SEPA mandates, partner
462
+ businesses, register settings, pairing, print jobs and receipt emails.
463
+ There it is a network error or timeout after sending began, HTTP 5xx, and
464
+ HTTP 200 with the `Kasseneck-Api-Version: v3` marker but an empty,
457
465
  non-JSON (also `text/html`) or status-less body (`KasseneckHttpError`,
458
466
  `reason` `empty-body`, `not-json` or `missing-status`). HTML without the
459
- marker stays `route_missing` with `'rejected'`: no function saw the call;
467
+ marker stays `route_missing` with `'rejected'`: no function saw the call.
468
+ Reading calls and dry runs (`previewGoodsReceipt`, `previewInvoice`) stay
469
+ `'rejected'`, and so do the register sign-in sessions and
470
+ `createPaymentLinkStripe`: a repeat books nothing (a second short-lived
471
+ session, a second link nobody else has seen);
460
472
  - on the money calls: every error envelope **without a code**, and every
461
473
  code that is not one of the rejection codes below. The Hobex and Stripe
462
474
  handlers also answer with a plain error message after the provider was
@@ -490,17 +502,33 @@ failed `hobexRefund` throws a `KasseneckApiError`; it never returns `false`,
490
502
  because a `false` on an unclear outcome invites a second refund. Even a
491
503
  `'rejected'` is no invitation to resend blindly: fix the cause first.
492
504
 
493
- **Never retry these six calls, and never put a retrying layer under the
494
- package.** A `fetch` passed in `options.fetch` (or a proxy or service worker in
495
- front of it) must not resend a request by itself after a network error, a
496
- timeout or a 5xx: a silent second send is a second signed receipt, a second
497
- charge or a second refund, and the package cannot see it. Look the result up
498
- instead (`getReceipt`, `listMyReceipts`, the Hobex transaction by its
499
- `transactionId`, the Stripe session).
500
-
501
- `outcome` only covers signing and the money calls. After a network error on
502
- `issueInvoice` an invoice may still have been issued; retry it with the same
503
- `idempotencyKey`.
505
+ **Never retry a call with an effect blindly, and never put a retrying layer
506
+ under the package.** A `fetch` passed in `options.fetch` (or a proxy or service
507
+ worker in front of it) must not resend a request by itself after a network
508
+ error, a timeout or a 5xx: a silent second send is a second signed receipt, a
509
+ second charge, a second refund or a second booking, and the package cannot
510
+ see it. Look the result up instead (`getReceipt`, `listMyReceipts`, the Hobex
511
+ transaction by its `transactionId`, the Stripe session).
512
+
513
+ **With an `idempotencyKey`** (inventory writes and reservations, `issueInvoice`,
514
+ `cancelInvoice`, `createCreditNote`, `recordInvoicePayment`, `createCustomer`
515
+ and `createPartnerCustomer` with a key) the safe retry after `'unknown'` is the
516
+ same request with the **same** key: it takes effect exactly once and returns
517
+ the stored answer (invoices and partner customers mark it with
518
+ `replayed: true`; inventory writes return the stored answer unchanged). Never a
519
+ new key: that books a second time. `createInvoiceItem` has no key; the server recognises a repeat by
520
+ `source` + `reference`, so the same request with the same values is safe
521
+ (`replayed: true`; other data gives `item_reference_conflict`). Without a key
522
+ (receipts, cancellations, money calls, settings, webhooks, `updateCustomer`,
523
+ `createCustomer` and `createPartnerCustomer` without one, `updateInvoiceItem`,
524
+ `withdrawInvoiceItem`, `setCustomerMandate`, `revokeCustomerMandate`) read the
525
+ state first (for invoice items `getInvoiceItem`) and only then decide.
526
+
527
+ A call through the open transport that this package does not know counts as
528
+ having no effect; say so with `transport(name, params, undefined, undefined,
529
+ { hasEffect: true })`. `hasEffect: false` only counts together with
530
+ `params.dryRun === true` (a dry run under the name of the real call); without
531
+ it a call with an effect stays `'unknown'`.
504
532
 
505
533
  ```ts
506
534
  import { isOutcomeUnknown, paymentsExpectedCents } from '@kreiseck/kasseneck-api';
@@ -944,7 +972,7 @@ process:
944
972
  `hobexPay`, `hobexRefund` and `stripeCaptureIntent` move money. They are never
945
973
  retried, neither by the app nor by a retrying `fetch` under the package: on
946
974
  `isOutcomeUnknown(error)` look the payment up (see
947
- [`outcome: 'unknown'`](#outcome-unknown-never-retry-look-it-up)).
975
+ [`outcome: 'unknown'`](#outcome-unknown-never-resend-blindly)).
948
976
  - **Hobex HPS via Kasseneck Connect**, described below.
949
977
 
950
978
  ### Hobex HPS via Kasseneck Connect
@@ -1351,9 +1379,35 @@ was classified.
1351
1379
  `previewInvoice` and `recordInvoicePayment`. An intra-Community supply carries
1352
1380
  `recapitulative_statement_due` (recapitulative statement).
1353
1381
 
1382
+ **VAT ID check.** An invoice without VAT that relies on the customer's VAT ID
1383
+ (intra-Community supply, reverse charge) is only issued with a result of the
1384
+ VAT ID check (FinanzOnline, otherwise VIES) on the day of issue.
1385
+ `vat_id_invalid` blocks the invoice. `vat_id_check_pending` means there is no
1386
+ result yet: retry after `retryAfter` seconds with the same `idempotencyKey`,
1387
+ or send `acceptVatIdRisk: true` to issue anyway, at the issuer's risk. The
1388
+ invoice then carries `vatIdRisk` (`acceptedOn`), otherwise the frozen proof
1389
+ `vatIdProof` (`checkedOn`, `source`, `level`, `code`); both are `null` where
1390
+ nothing was checked.
1391
+
1392
+ ```ts
1393
+ try {
1394
+ await invoices.issueInvoice(request);
1395
+ } catch (error) {
1396
+ if (!isInvoiceError(error, 'vat_id_check_pending')) throw error;
1397
+ const retryAfter = error.details['retryAfter']; // seconds
1398
+ // later: same request and key, or with acceptVatIdRisk: true
1399
+ }
1400
+ ```
1401
+
1354
1402
  **After a timeout, retry with the same `idempotencyKey`**, never with a new one:
1355
1403
  you then get the invoice that was already issued (`replayed: true`). The same
1356
- key with different data gives `idempotency_conflict`.
1404
+ key with different data gives `idempotency_conflict`. Since 1.5.1 every invoice
1405
+ call with an effect (`issueInvoice`, `cancelInvoice`, `createCreditNote`,
1406
+ `recordInvoicePayment`, `createCustomer`, `updateCustomer`) reports
1407
+ `outcome: 'unknown'` after a timeout, a network error, HTTP 5xx or an
1408
+ unreadable answer (before: `'rejected'`); `previewInvoice` and the reading
1409
+ calls stay `'rejected'`. See
1410
+ [`outcome: 'unknown'`](#outcome-unknown-never-resend-blindly).
1357
1411
 
1358
1412
  Validation errors arrive as `validation` with field paths
1359
1413
  (`invoiceFieldErrors(error)` → `[{ field: 'items[0].vatRate', message }]`).
@@ -1425,10 +1479,16 @@ the old formula: `fixtures/invoice-totals.json`.
1425
1479
  For online shops and other systems that show or mirror the stock of a
1426
1480
  Kasseneck account: read articles, locations, stock per location and the stock
1427
1481
  ledger, and get every stock change pushed by webhook within seconds, also the
1428
- ones made at the register in the shop. Uses the `api_key` of the account and
1429
- belongs on a **server**. Reading needs the module `lager`; purchase prices and
1430
- stock values appear only when the account has the permission `costs`
1431
- (otherwise the fields are absent, not `null`).
1482
+ ones made at the register in the shop. Since 1.5.0 also the write side:
1483
+ create and update articles, book goods receipts, transfers, losses and
1484
+ condition changes, and reserve stock at checkout (see
1485
+ [Shop: reserve and invoice](#shop-reserve-and-invoice)). Uses the `api_key` of
1486
+ the account and belongs on a **server**. Reading needs the module `lager`,
1487
+ writing additionally the account switch *Lager-API schreiben*, which Kasseneck
1488
+ turns on (always on in the test environment, `kr_test_…`; otherwise
1489
+ `inventory_api_not_enabled`). Purchase prices and stock values appear only when
1490
+ the account has the permission `costs` (otherwise the fields are absent, not
1491
+ `null`).
1432
1492
 
1433
1493
  **Integers with a fixed scale:** quantities in thousandths of the base unit
1434
1494
  (`1000` = 1 piece, `250` = 0.250 kg), money in cents, purchase prices in
@@ -1500,16 +1560,22 @@ app.post('/kasseneck-webhook', express.raw({ type: '*/*' }), async (req, res) =>
1500
1560
  - **Events.** `stock.changed` carries the current state of one article at one
1501
1561
  location (`onHand`, `reserved`, `available`, `defective`, `sequence`,
1502
1562
  `updatedAt`) plus `cause` (`sale`, `invoice`, `goods_receipt`, `transfer`,
1503
- `takeover` …) and `movementId`; changes within 10 seconds are combined into
1504
- one delivery. `stock.below_minimum` fires once when `onHand` drops below the
1505
- minimum stock set for the location; `minStock` in the payload is that
1506
- threshold. The article's own `minStock` never triggers it, and
1563
+ `takeover`, `reservation` …) and `movementId`; changes within 10 seconds are
1564
+ combined into one delivery. `stock.below_minimum` fires once when
1565
+ `available` (`onHand − reserved`, so a reservation alone can trigger it)
1566
+ drops below the minimum stock set for the location (`minStockByLocation` of
1567
+ the article); `minStock` in the payload is that threshold. The article's own
1568
+ `minStock` is a legacy field and never triggers it, and
1507
1569
  `listStock({ belowMinimum: true })` follows the same rule. In movements,
1508
1570
  `goods_receipt` is a goods receipt; `receipt` only ever means a sales receipt
1509
1571
  (`source.type`).
1510
1572
  `article.created`, `article.updated` and `article.deactivated` carry the
1511
- article as `getArticle` returns it, without purchase prices. Deduplicate on
1512
- `event.id`; deliveries are retried after 1 min, 5 min, 30 min, 2 h and 12 h.
1573
+ article as `getArticle` returns it, without purchase prices.
1574
+ `reservation.expired`, `reservation.released` and `reservation.redeemed`
1575
+ carry the reservation as `getReservation` returns it, with its status after
1576
+ the change; `released` and `redeemed` also fire for a partial release or
1577
+ redemption (status still `active`). Deduplicate on `event.id`; deliveries
1578
+ are retried after 1 min, 5 min, 30 min, 2 h and 12 h.
1513
1579
  - **Safety net without webhooks.** `listStock({ changedSince })` and
1514
1580
  `listArticles({ updatedSince })` are sorted by `updatedAt` ascending and
1515
1581
  include the boundary, so remembering the last `updatedAt` and asking again
@@ -1528,6 +1594,159 @@ app.post('/kasseneck-webhook', express.raw({ type: '*/*' }), async (req, res) =>
1528
1594
  `invalid_webhook_url`, `event_not_subscribed` and `webhook_inactive` are
1529
1595
  decided on the code with `isInventoryError(error, code)`.
1530
1596
 
1597
+ ### Writing: articles and bookings
1598
+
1599
+ Every write takes an `idempotencyKey` (1 to 120 characters). The same request
1600
+ with the same key takes effect exactly once; repeating it returns the stored
1601
+ answer of the first call, the same key with different content gives
1602
+ `idempotency_conflict`. After a timeout or a network error, repeat with the
1603
+ **same** key, never with a new one. Since 1.5.1 every write, reservation and
1604
+ webhook call reports `outcome: 'unknown'` in that case (also after HTTP 5xx or
1605
+ an unreadable answer; before: `'rejected'`), so `isOutcomeUnknown(error)` is
1606
+ the signal to resend with the same key; `previewGoodsReceipt` and the reading
1607
+ calls stay `'rejected'`. The client refuses to send a write without
1608
+ a valid key, and checks before sending only what is certainly wrong without the
1609
+ network (required ids, integer quantities, amounts and prices, the range of
1610
+ `expiresInMinutes`); everything else the server decides and reports with its
1611
+ code.
1612
+
1613
+ ```ts
1614
+ import { createInventoryClient, isInventoryError, inventoryFieldErrors } from '@kreiseck/kasseneck-api/inventory';
1615
+
1616
+ const inventory = createInventoryClient({ apiKey: process.env.KASSENECK_API_KEY! });
1617
+
1618
+ // A foreign article with its EAN; without ean the server assigns an own code.
1619
+ const roll = await inventory.createArticle({
1620
+ idempotencyKey: 'shop-article-1001',
1621
+ name: 'Kaisersemmel',
1622
+ ean: '9001234567896',
1623
+ unitPriceCents: 65,
1624
+ vatRate: 10,
1625
+ stockTracked: true,
1626
+ minStockByLocation: { haupt: 20000 }, // thousandths: warn below 20 pieces
1627
+ externalIds: { shop: '1001' }, // lookupArticleByCode({ externalSystem: 'shop', externalId: '1001' })
1628
+ });
1629
+
1630
+ // Goods receipt: 60 pieces at 0.38 EUR each, plus freight spread by value.
1631
+ const receipt = await inventory.receiveGoods({
1632
+ idempotencyKey: 'shop-goods-receipt-118',
1633
+ items: [{ articleId: roll.id, quantity: 60000, unitPriceMicros: 380000 }],
1634
+ landedCosts: [{ type: 'freight', amountCents: 450 }],
1635
+ reference: 'LS-2026-118',
1636
+ });
1637
+ // receipt: { operationId, movementIds, lotIds, warnings: [] }
1638
+
1639
+ try {
1640
+ await inventory.recordStockLoss({ idempotencyKey: 'shop-loss-7', reason: 'breakage', items: [{ articleId: roll.id, quantity: 2000 }] });
1641
+ } catch (error) {
1642
+ if (isInventoryError(error, 'exceeds_stock')) { /* losses, transfers and condition changes never overdraw */ }
1643
+ else if (isInventoryError(error, 'validation')) console.log(inventoryFieldErrors(error));
1644
+ else throw error;
1645
+ }
1646
+ ```
1647
+
1648
+ - **Articles.** `updateArticle({ idempotencyKey, articleId, …fields })` changes
1649
+ only the fields it names; `null` clears an optional one. `externalIds` and
1650
+ `metadata` are replaced as a whole, `minStockByLocation` is merged per
1651
+ location (`{ haupt: null }` removes just that one). `stockKind` is fixed after
1652
+ the first movement (`stock_kind_locked`). `deactivateArticle` frees the code
1653
+ and the external ids. `code_taken` and `external_id_taken` carry `field` and
1654
+ the `articleId` that holds the code in `error.details`. `minStock` is a
1655
+ legacy field without effect; use `minStockByLocation`.
1656
+ - **Bookings.** `receiveGoods`, `transferStock`, `recordStockLoss` (`reason`
1657
+ from `STOCK_LOSS_REASONS`; `other` needs `note`, `withdrawal` needs
1658
+ `withdrawalType`), `changeStockCondition` (`sellable` ↔ `defective`) and
1659
+ `reverseStockMovement({ operationId, reason })` answer with a
1660
+ `StockOperation`. `warnings[]` are notices from `INVENTORY_WARNING_CODES`,
1661
+ never errors: the booking took effect. Prices may be sent without the
1662
+ permission `costs`; values come back only in `previewGoodsReceipt`, which is
1663
+ `receiveGoods` with `dryRun: true` (books nothing, needs no key; a `null`
1664
+ key counts as none and is not sent). `receiveGoods` itself refuses
1665
+ `dryRun: true` before sending (`KasseneckValidationError`, `scope:
1666
+ 'request'`): a preview is not a booking, call `previewGoodsReceipt`.
1667
+
1668
+ ### Shop: reserve and invoice
1669
+
1670
+ Hold the goods while the customer pays, turn the reservation into a sale with
1671
+ the invoice, and let the server release what was never paid:
1672
+
1673
+ ```ts
1674
+ import { createInventoryClient, inventoryShortfalls, isInventoryError, parseInventoryWebhookEvent } from '@kreiseck/kasseneck-api/inventory';
1675
+ import { createInvoiceApi } from '@kreiseck/kasseneck-api/invoice';
1676
+
1677
+ const inventory = createInventoryClient({ apiKey: process.env.KASSENECK_API_KEY! });
1678
+ const invoices = createInvoiceApi({ apiKey: process.env.KASSENECK_API_KEY! });
1679
+
1680
+ // 1. Checkout: reserve everything or nothing, measured against `available`.
1681
+ async function checkout(order: Order) {
1682
+ try {
1683
+ return await inventory.createReservation({
1684
+ idempotencyKey: `checkout-${order.id}`, // the same key on every retry of this checkout
1685
+ reference: `Bestellung ${order.number}`,
1686
+ expiresInMinutes: 30,
1687
+ items: [{ articleId: 'kaisersemmel', quantity: 6000 }], // 6 pieces, at the default location
1688
+ });
1689
+ } catch (error) {
1690
+ if (!isInventoryError(error, 'insufficient_available')) throw error;
1691
+ // Nothing was reserved. Tell the customer what is short:
1692
+ // [{ articleId, locationId, requested: 6000, available: 2000 }]
1693
+ return showShortage(inventoryShortfalls(error));
1694
+ }
1695
+ }
1696
+
1697
+ // 2. Paid: issue the invoice; the position redeems the reservation.
1698
+ async function paid(order: Order, reservationId: string) {
1699
+ const { invoice, notice } = await invoices.issueInvoice({
1700
+ idempotencyKey: `invoice-${order.id}`,
1701
+ priceMode: 'gross',
1702
+ serviceStart: '2026-10-06',
1703
+ orderReference: `Bestellung ${order.number}`,
1704
+ items: [{
1705
+ description: 'Kaisersemmel', quantity: 6, unitPriceCents: 65, vatRate: 10,
1706
+ articleId: 'kaisersemmel', reservationId,
1707
+ }],
1708
+ payment: { method: 'online' },
1709
+ });
1710
+ // Expired before the invoice: no error, the invoice sells without it.
1711
+ if (notice?.some((n) => n.code === 'reservation_expired')) log('sold without reservation', invoice.number);
1712
+ }
1713
+
1714
+ // 3. Abandoned cart: give the goods back at once instead of waiting for expiry.
1715
+ async function abandoned(order: Order, reservationId: string) {
1716
+ await inventory.releaseReservation({ idempotencyKey: `release-${order.id}`, reservationId });
1717
+ }
1718
+
1719
+ // 4. Webhooks (subscribe to reservation.* with createWebhook, verify as above).
1720
+ function onDelivery(rawBody: Buffer) {
1721
+ const event = parseInventoryWebhookEvent(rawBody);
1722
+ if (event && !event.test && event.type === 'reservation.expired') {
1723
+ return shop.cancelUnpaidOrder(event.data.reference); // the stock is available again
1724
+ }
1725
+ }
1726
+ ```
1727
+
1728
+ - **Redeeming.** A position with `reservationId` needs `articleId`. The server
1729
+ checks it when the invoice is issued: `reservation_not_found` (unknown or
1730
+ another account), `reservation_not_active` (already redeemed or released),
1731
+ `reservation_mismatch` (no open quantity of this article at the invoice's
1732
+ stock location, `stockLocationId` or the default location); nothing is issued
1733
+ then. An expired reservation is no error but the notice `reservation_expired`
1734
+ (with `reservationId`). The reservation is redeemed when the invoice is
1735
+ booked into stock: selling less than reserved releases the rest, selling
1736
+ more gives the warning `reservation_exceeded`. Credit notes take no
1737
+ `reservationId`.
1738
+ - **Expiry.** Without `expiresInMinutes` the account's default applies (7 days);
1739
+ 5 to 43,200 minutes are allowed. `extendReservation` sets a new expiry from
1740
+ now, only for an active reservation that is not yet due
1741
+ (`reservation_not_active`). `releaseReservation` without `items` releases
1742
+ everything, with `items` per position (without `quantity` the whole rest);
1743
+ the status becomes `released` only when nothing is open.
1744
+ - **Events.** `reservation.expired`, `reservation.released` and
1745
+ `reservation.redeemed` carry the reservation with its status afterwards.
1746
+ `listReservations({ status: 'active' })` or `({ reference })` is the safety
1747
+ net without webhooks; the stock ledger shows reservations as movements of
1748
+ type `reservation` with `quantityDelta: 0` and the change in `reservedDelta`.
1749
+
1531
1750
  ## Migrating from 0.x
1532
1751
 
1533
1752
  1.0 is one breaking step: the wire, the exported names and the contract files
@@ -1603,7 +1822,7 @@ in machine form what both sides agreed on, among them:
1603
1822
  |---|---|---|
1604
1823
  | `pos-settings-defaults.json` | field names and defaults of the register settings as sent on `/api/v3` (`business`, `device`), derived from the backend contract in `fixtures/v3/` | `npm run fixtures:kasse` |
1605
1824
  | `stored/pos-settings-defaults.json` | the same defaults in the stored (internal, German) form (`betrieb`, `geraet`) | `npm run fixtures:kasse` |
1606
- | `surface.json` | base URLs (`baseUrls.public`, `baseUrls.pos`), the calls per path (`calls`, and the backend's `routes`), settings value lists (`enums`, keyed by field), other register lists (`pos`), register error codes, permission keys, shortcut actions, partner, invoice and inventory lists | `npm run fixtures:oberflaeche` |
1825
+ | `surface.json` | base URLs (`baseUrls.public`, `baseUrls.pos`), the calls per path (`calls`, and the backend's `routes`), the calls with an effect whose outcome is `unknown` after a timeout, network error, 5xx or unreadable answer (`unknownOutcomeCalls`, sorted), settings value lists (`enums`, keyed by field), other register lists (`pos`), register error codes, permission keys, shortcut actions, partner, invoice and inventory lists | `npm run fixtures:oberflaeche` |
1607
1826
  | `hobex-hps-codes.json` | measured HPS result codes, their meaning and whether they settle an outcome (the contract behind `isConclusive`) | `npm run fixtures:hobex-hps-codes` |
1608
1827
  | `pos-texts.json` | the register's message catalogue | `npm run fixtures:texte` |
1609
1828
  | `invoice-texts.json` | invoice texts in both languages | `npm run fixtures:rechnungstexte` |