@kreiseck/kasseneck-api 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/CHANGELOG.md +176 -0
  2. package/README.md +293 -1
  3. package/dist/cjs/client/aufrufe.d.ts +2 -2
  4. package/dist/cjs/client/aufrufe.js +58 -0
  5. package/dist/cjs/inventory/api.d.ts +61 -0
  6. package/dist/cjs/inventory/api.js +54 -0
  7. package/dist/cjs/inventory/auth.d.ts +17 -0
  8. package/dist/cjs/inventory/auth.js +27 -0
  9. package/dist/cjs/inventory/endpunkte.d.ts +91 -0
  10. package/dist/cjs/inventory/endpunkte.js +290 -0
  11. package/dist/cjs/inventory/fehler.d.ts +43 -0
  12. package/dist/cjs/inventory/fehler.js +66 -0
  13. package/dist/cjs/inventory/index.d.ts +21 -0
  14. package/dist/cjs/inventory/index.js +94 -0
  15. package/dist/cjs/inventory/lesen.d.ts +45 -0
  16. package/dist/cjs/inventory/lesen.js +424 -0
  17. package/dist/cjs/inventory/schreiben.d.ts +83 -0
  18. package/dist/cjs/inventory/schreiben.js +315 -0
  19. package/dist/cjs/inventory/typen.d.ts +679 -0
  20. package/dist/cjs/inventory/typen.js +14 -0
  21. package/dist/cjs/inventory/vertrag.d.ts +136 -0
  22. package/dist/cjs/inventory/vertrag.js +291 -0
  23. package/dist/cjs/inventory/webhook.d.ts +52 -0
  24. package/dist/cjs/inventory/webhook.js +116 -0
  25. package/dist/cjs/invoice/index.d.ts +2 -2
  26. package/dist/cjs/invoice/index.js +2 -1
  27. package/dist/cjs/invoice/typen.d.ts +46 -2
  28. package/dist/cjs/invoice/vertrag.d.ts +19 -5
  29. package/dist/cjs/invoice/vertrag.js +37 -5
  30. package/dist/cjs/stored/vokabular.d.ts +1 -1
  31. package/dist/cjs/stored/vokabular.js +5 -3
  32. package/dist/cjs/version.d.ts +1 -1
  33. package/dist/cjs/version.js +1 -1
  34. package/dist/esm/client/aufrufe.d.ts +2 -2
  35. package/dist/esm/client/aufrufe.js +58 -0
  36. package/dist/esm/inventory/api.d.ts +61 -0
  37. package/dist/esm/inventory/api.js +51 -0
  38. package/dist/esm/inventory/auth.d.ts +17 -0
  39. package/dist/esm/inventory/auth.js +24 -0
  40. package/dist/esm/inventory/endpunkte.d.ts +91 -0
  41. package/dist/esm/inventory/endpunkte.js +267 -0
  42. package/dist/esm/inventory/fehler.d.ts +43 -0
  43. package/dist/esm/inventory/fehler.js +57 -0
  44. package/dist/esm/inventory/index.d.ts +21 -0
  45. package/dist/esm/inventory/index.js +20 -0
  46. package/dist/esm/inventory/lesen.d.ts +45 -0
  47. package/dist/esm/inventory/lesen.js +404 -0
  48. package/dist/esm/inventory/schreiben.d.ts +83 -0
  49. package/dist/esm/inventory/schreiben.js +298 -0
  50. package/dist/esm/inventory/typen.d.ts +679 -0
  51. package/dist/esm/inventory/typen.js +13 -0
  52. package/dist/esm/inventory/vertrag.d.ts +136 -0
  53. package/dist/esm/inventory/vertrag.js +288 -0
  54. package/dist/esm/inventory/webhook.d.ts +52 -0
  55. package/dist/esm/inventory/webhook.js +107 -0
  56. package/dist/esm/invoice/index.d.ts +2 -2
  57. package/dist/esm/invoice/index.js +1 -1
  58. package/dist/esm/invoice/typen.d.ts +46 -2
  59. package/dist/esm/invoice/vertrag.d.ts +19 -5
  60. package/dist/esm/invoice/vertrag.js +36 -4
  61. package/dist/esm/stored/vokabular.d.ts +1 -1
  62. package/dist/esm/stored/vokabular.js +5 -3
  63. package/dist/esm/version.d.ts +1 -1
  64. package/dist/esm/version.js +1 -1
  65. package/fixtures/hobex-hps-codes.json +1 -1
  66. package/fixtures/invoice-api-examples/credit-error-reservation.json +26 -0
  67. package/fixtures/invoice-api-examples/issue-error-reservation-article.json +25 -0
  68. package/fixtures/invoice-api-examples/issue-error-vat-id-risk.json +26 -0
  69. package/fixtures/invoice-api-examples/issue-reservation.json +24 -0
  70. package/fixtures/invoice-api-examples/issue-vat-id-risk.json +28 -0
  71. package/fixtures/invoice-api.schema.json +15 -2
  72. package/fixtures/invoice-texts.json +1 -1
  73. package/fixtures/pos-texts.json +1 -1
  74. package/fixtures/surface.json +306 -5
  75. package/fixtures/v3/README.md +9 -1
  76. package/fixtures/v3/antworten/belege.json +2 -2
  77. package/fixtures/v3/antworten/belegmail.json +2 -2
  78. package/fixtures/v3/antworten/kasse-belege.json +2 -2
  79. package/fixtures/v3/antworten/kasse.json +3 -3
  80. package/fixtures/v3/antworten/lager.json +3881 -0
  81. package/fixtures/v3/antworten/rechnungen.json +267 -44
  82. package/fixtures/v3/antworten/storno.json +2 -2
  83. package/fixtures/v3/stored/belege.json +2 -2
  84. package/fixtures/v3/stored/kasse.json +404 -4
  85. package/fixtures/v3/stored/rechnungen.json +174 -18
  86. package/fixtures/v3/v3-vokabular.json +1007 -20
  87. package/fixtures/v3/zahlbetrag-faelle.json +2 -2
  88. package/package.json +14 -2
@@ -0,0 +1,298 @@
1
+ /**
2
+ * Die schreibenden Aufrufe der Lager-API (Backend Stufe 5b): Artikel anlegen,
3
+ * aendern und stilllegen, Bestand buchen (Wareneingang, Umbuchung, Abgang,
4
+ * Zustand, Gegenbuchung) und reservieren.
5
+ *
6
+ * Wie in `endpunkte.ts` nimmt jede Funktion den Transport als ersten Parameter
7
+ * und ist einzeln importierbar. Die Anfrage geht flach hinaus, genau mit den
8
+ * Feldern, die der Aufrufer nennt (`undefined` faellt weg, `null` bleibt: es
9
+ * leert ein Feld bzw. heisst bei der Anlage „nicht angegeben“).
10
+ *
11
+ * **Vor dem Senden geprueft wird nur, was ohne Netz sicher falsch ist:**
12
+ * - `idempotencyKey` fehlt, ist leer oder laenger als 120 Zeichen. Ohne
13
+ * Schluessel gaebe es keine sichere Wiederholung, und der Server wiese die
14
+ * Anfrage ohnehin ab (`idempotency_key_required` bzw. `validation`);
15
+ * - eine Pflichtkennung fehlt oder ist leer;
16
+ * - eine Menge, ein Betrag oder ein Preis ist keine Ganzzahl (Tausendstel,
17
+ * Cent, Mikro-Euro): `1.5` waere nie „1,5 Stueck“, sondern ein Fehler;
18
+ * - `expiresInMinutes` liegt ausserhalb 5 … 43 200;
19
+ * - eine Aenderung nennt kein Feld, eine Freigabe eine leere Liste.
20
+ * Alles Fachliche (Pruefziffer der EAN, Kataloge, Bestand, Rechte) prueft der
21
+ * Server und meldet es mit seinem Code.
22
+ *
23
+ * **Wiederholen:** nach einem Zeitlimit oder Netzfehler denselben Aufruf mit
24
+ * **demselben** `idempotencyKey` noch einmal senden. Er wirkt genau einmal und
25
+ * liefert die gespeicherte Antwort; ein neuer Schluessel buchte ein zweites Mal.
26
+ */
27
+ import { abfrage, anfragefehler, kennung, seitenweise } from './endpunkte.js';
28
+ import { artikel, liste, naechsterCursor, objekt, reservierung, vorgang, vorschauZeile } from './lesen.js';
29
+ import { INVENTORY_IDEMPOTENCY_KEY_MAX, RESERVATION_MINUTES_MAX, RESERVATION_MINUTES_MIN } from './vertrag.js';
30
+ // ---- Pruefung vor dem Senden ------------------------------------------------------
31
+ /** Die Anfrage als flache Kopie ohne `undefined`; der Aufrufer behaelt sein Objekt. */
32
+ function anfrage(name, request) {
33
+ if (request === null || typeof request !== 'object' || Array.isArray(request)) {
34
+ throw anfragefehler(name, 'Anfrage muss ein Objekt sein');
35
+ }
36
+ const raus = {};
37
+ for (const [feld, w] of Object.entries(request))
38
+ if (w !== undefined)
39
+ raus[feld] = w;
40
+ return raus;
41
+ }
42
+ /**
43
+ * `idempotencyKey`: Text mit 1–120 Zeichen, nicht nur Leerraum. Er wird nie
44
+ * getrimmt oder gekuerzt: ein veraenderter Schluessel waere ein anderer.
45
+ */
46
+ function schluessel(name, p, { pflicht }) {
47
+ const w = p.idempotencyKey;
48
+ if (w === undefined || w === null) {
49
+ if (pflicht) {
50
+ throw anfragefehler(name, 'idempotencyKey fehlt (Pflicht bei jedem Schreiben); bei einer Wiederholung denselben Schluessel senden');
51
+ }
52
+ return;
53
+ }
54
+ if (typeof w !== 'string' || w.trim() === '')
55
+ throw anfragefehler(name, 'idempotencyKey muss ein nicht leerer Text sein');
56
+ if (w.length > INVENTORY_IDEMPOTENCY_KEY_MAX) {
57
+ throw anfragefehler(name, `idempotencyKey ist laenger als ${INVENTORY_IDEMPOTENCY_KEY_MAX} Zeichen`);
58
+ }
59
+ }
60
+ /** Eine Ganzzahl (Tausendstel, Cent, Mikro-Euro); fehlt sie, nur wenn sie darf. `null` nur, wo es leert. */
61
+ function ganz(name, feld, w, { pflicht = false, leerbar = false } = {}) {
62
+ if (w === undefined || (w === null && leerbar)) {
63
+ if (pflicht)
64
+ throw anfragefehler(name, `${feld} fehlt`);
65
+ return;
66
+ }
67
+ if (typeof w !== 'number' || !Number.isSafeInteger(w)) {
68
+ throw anfragefehler(name, `${feld} muss eine ganze Zahl sein (Mengen in Tausendstel, Geld in Cent, Preise in Mikro-Euro)`);
69
+ }
70
+ }
71
+ /** Haltedauer einer Reservierung in ganzen Minuten. */
72
+ function minuten(name, w, { pflicht }) {
73
+ if (w === undefined || (w === null && !pflicht)) {
74
+ if (pflicht)
75
+ throw anfragefehler(name, 'expiresInMinutes fehlt');
76
+ return;
77
+ }
78
+ if (typeof w !== 'number' || !Number.isSafeInteger(w) || w < RESERVATION_MINUTES_MIN || w > RESERVATION_MINUTES_MAX) {
79
+ throw anfragefehler(name, `expiresInMinutes muss eine ganze Zahl von ${RESERVATION_MINUTES_MIN} bis ${RESERVATION_MINUTES_MAX} sein`);
80
+ }
81
+ }
82
+ /**
83
+ * Positionen: eine Liste (Pflicht, wo der Aufruf sie verlangt), jede ein
84
+ * Objekt mit `articleId` und ganzzahliger `quantity`. `weiter` prueft die
85
+ * uebrigen Zahlenfelder einer Position. Leer und zu lang entscheidet der Server
86
+ * (`no_positions`, `too_many_positions`).
87
+ */
88
+ function positionen(name, w, { pflicht, menge }, weiter = () => { }) {
89
+ if (w === undefined) {
90
+ if (pflicht)
91
+ throw anfragefehler(name, 'items fehlt');
92
+ return;
93
+ }
94
+ if (!Array.isArray(w))
95
+ throw anfragefehler(name, 'items muss eine Liste sein');
96
+ w.forEach((x, i) => {
97
+ const f = `items[${i}]`;
98
+ if (x === null || typeof x !== 'object' || Array.isArray(x))
99
+ throw anfragefehler(name, `${f} muss ein Objekt sein`);
100
+ const o = x;
101
+ kennung(name, `${f}.articleId`, o.articleId);
102
+ ganz(name, `${f}.quantity`, o.quantity, { pflicht: menge === 'pflicht' });
103
+ weiter(o, f);
104
+ });
105
+ }
106
+ // ---- Artikel ------------------------------------------------------------------
107
+ /** Zahlenfelder eines Artikels; `null` leert sie (bzw. heisst bei der Anlage „nicht angegeben“). */
108
+ function artikelZahlen(name, p) {
109
+ for (const feld of ['unitPriceCents', 'minStock', 'purchasePriceMicros'])
110
+ ganz(name, feld, p[feld], { leerbar: true });
111
+ const jeStandort = p.minStockByLocation;
112
+ if (jeStandort === undefined || jeStandort === null)
113
+ return;
114
+ if (typeof jeStandort !== 'object' || Array.isArray(jeStandort))
115
+ throw anfragefehler(name, 'minStockByLocation muss ein Objekt { standort: Tausendstel } sein');
116
+ for (const [standort, w] of Object.entries(jeStandort))
117
+ ganz(name, `minStockByLocation.${standort}`, w, { leerbar: true });
118
+ }
119
+ const artikelAus = (name, daten) => artikel({ name, pfad: 'article' }, objekt(daten)?.article);
120
+ /**
121
+ * Legt einen Artikel an. Mit `ean` ein Fremdartikel mit diesem Code (gueltige
122
+ * Pruefziffer, frei im Konto), sonst vergibt der Server den naechsten eigenen
123
+ * Code. Antwort: der Artikel wie `getArticle`.
124
+ */
125
+ export async function createArticle(transport, request) {
126
+ const name = 'createArticle';
127
+ const p = anfrage(name, request);
128
+ schluessel(name, p, { pflicht: true });
129
+ artikelZahlen(name, p);
130
+ return artikelAus(name, await transport(name, p));
131
+ }
132
+ /** Aendert nur die genannten Felder eines Artikels (siehe [UpdateArticleRequest]). */
133
+ export async function updateArticle(transport, request) {
134
+ const name = 'updateArticle';
135
+ const p = anfrage(name, request);
136
+ schluessel(name, p, { pflicht: true });
137
+ kennung(name, 'articleId', p.articleId);
138
+ if (Object.keys(p).every((k) => k === 'idempotencyKey' || k === 'articleId')) {
139
+ throw anfragefehler(name, 'die Aenderung nennt kein Feld');
140
+ }
141
+ artikelZahlen(name, p);
142
+ return artikelAus(name, await transport(name, p));
143
+ }
144
+ /** Legt einen Artikel still (`active: false`); Code und eigene Kennungen werden frei. */
145
+ export async function deactivateArticle(transport, request) {
146
+ const name = 'deactivateArticle';
147
+ const p = anfrage(name, request);
148
+ schluessel(name, p, { pflicht: true });
149
+ kennung(name, 'articleId', p.articleId);
150
+ return artikelAus(name, await transport(name, p));
151
+ }
152
+ // ---- Buchen ------------------------------------------------------------------
153
+ function eingangZahlen(name, p) {
154
+ positionen(name, p.items, { pflicht: true, menge: 'pflicht' }, (x, f) => {
155
+ for (const feld of ['totalCents', 'unitPriceMicros', 'landedCostCents'])
156
+ ganz(name, `${f}.${feld}`, x[feld]);
157
+ });
158
+ const nebenkosten = p.landedCosts;
159
+ if (nebenkosten === undefined)
160
+ return;
161
+ if (!Array.isArray(nebenkosten))
162
+ throw anfragefehler(name, 'landedCosts muss eine Liste sein');
163
+ nebenkosten.forEach((n, i) => {
164
+ if (n === null || typeof n !== 'object' || Array.isArray(n))
165
+ throw anfragefehler(name, `landedCosts[${i}] muss ein Objekt sein`);
166
+ ganz(name, `landedCosts[${i}].amountCents`, n.amountCents, { pflicht: true });
167
+ });
168
+ }
169
+ /**
170
+ * Bucht einen Wareneingang. Ohne `locationId` am Standard-Standort; die
171
+ * Antwort traegt keine Werte, auch mit dem Recht `costs` nicht. Fuer die
172
+ * Vorschau mit Werten: [previewGoodsReceipt].
173
+ */
174
+ export async function receiveGoods(transport, request) {
175
+ const name = 'receiveGoods';
176
+ const p = anfrage(name, request);
177
+ if (p.dryRun === true) {
178
+ throw anfragefehler(name, 'dryRun: true bucht nichts und liefert eine Vorschau statt eines Vorgangs; dafuer previewGoodsReceipt aufrufen');
179
+ }
180
+ schluessel(name, p, { pflicht: true });
181
+ eingangZahlen(name, p);
182
+ return vorgang(name, await transport(name, p));
183
+ }
184
+ /**
185
+ * Vorschau eines Wareneingangs (`receiveGoods` mit `dryRun: true`): prueft
186
+ * Positionen, Artikel, Preise und Nebenkosten und rechnet die Verteilung,
187
+ * schreibt aber nichts. Ein `idempotencyKey` ist freigestellt (`null` wie nicht
188
+ * angegeben, geht nicht hinaus) und wird nicht verbraucht; dieselbe Anfrage
189
+ * laesst sich danach mit ihm buchen. Werte
190
+ * (`baseCents` …) nur mit dem Recht `costs`. Den Standort prueft erst die Buchung.
191
+ */
192
+ export async function previewGoodsReceipt(transport, request) {
193
+ const name = 'receiveGoods';
194
+ const p = anfrage(name, request);
195
+ // null heisst hier „kein Schluessel“ und geht wie undefined nicht hinaus.
196
+ if (p.idempotencyKey === null)
197
+ delete p.idempotencyKey;
198
+ schluessel(name, p, { pflicht: false });
199
+ eingangZahlen(name, p);
200
+ const daten = await transport(name, { ...p, dryRun: true });
201
+ return { preview: liste(name, daten, 'preview', vorschauZeile) };
202
+ }
203
+ const stueckPositionen = (name, p) => positionen(name, p.items, { pflicht: true, menge: 'pflicht' });
204
+ /** Bucht Ware von einem Standort an einen anderen; ueberzieht nie (`exceeds_stock`). */
205
+ export async function transferStock(transport, request) {
206
+ const name = 'transferStock';
207
+ const p = anfrage(name, request);
208
+ schluessel(name, p, { pflicht: true });
209
+ kennung(name, 'fromLocationId', p.fromLocationId);
210
+ kennung(name, 'toLocationId', p.toLocationId);
211
+ stueckPositionen(name, p);
212
+ return vorgang(name, await transport(name, p));
213
+ }
214
+ /** Bucht einen Abgang (Bruch, Schwund, Diebstahl, Entnahme …); ueberzieht nie (`exceeds_stock`). */
215
+ export async function recordStockLoss(transport, request) {
216
+ const name = 'recordStockLoss';
217
+ const p = anfrage(name, request);
218
+ schluessel(name, p, { pflicht: true });
219
+ stueckPositionen(name, p);
220
+ return vorgang(name, await transport(name, p));
221
+ }
222
+ /** Bucht Ware zwischen `sellable` und `defective` um; ueberzieht nie (`exceeds_stock`). */
223
+ export async function changeStockCondition(transport, request) {
224
+ const name = 'changeStockCondition';
225
+ const p = anfrage(name, request);
226
+ schluessel(name, p, { pflicht: true });
227
+ stueckPositionen(name, p);
228
+ return vorgang(name, await transport(name, p));
229
+ }
230
+ /** Nimmt einen ganzen Vorgang (`operationId`) mit einer Gegenbuchung zurueck. */
231
+ export async function reverseStockMovement(transport, request) {
232
+ const name = 'reverseStockMovement';
233
+ const p = anfrage(name, request);
234
+ schluessel(name, p, { pflicht: true });
235
+ kennung(name, 'operationId', p.operationId);
236
+ return vorgang(name, await transport(name, p));
237
+ }
238
+ // ---- Reservierung ---------------------------------------------------------------
239
+ const reservierungAus = (name, daten) => reservierung({ name, pfad: 'reservation' }, objekt(daten)?.reservation);
240
+ /**
241
+ * Reserviert Ware (Checkout im Shop): ganz oder gar nicht, gemessen am
242
+ * verfuegbaren Bestand (`onHand − reserved`). Fehlt etwas, entsteht nichts:
243
+ * `insufficient_available`, die fehlenden Positionen in [inventoryShortfalls].
244
+ * Eingeloest wird ueber eine Rechnung (`issueInvoice` mit `items[].reservationId`),
245
+ * sonst laeuft die Reservierung ab und gibt die Ware wieder frei.
246
+ */
247
+ export async function createReservation(transport, request) {
248
+ const name = 'createReservation';
249
+ const p = anfrage(name, request);
250
+ schluessel(name, p, { pflicht: true });
251
+ positionen(name, p.items, { pflicht: true, menge: 'pflicht' });
252
+ minuten(name, p.expiresInMinutes, { pflicht: false });
253
+ return reservierungAus(name, await transport(name, p));
254
+ }
255
+ /** Verlaengert eine aktive Reservierung: neuer Ablauf = jetzt + `expiresInMinutes`. */
256
+ export async function extendReservation(transport, request) {
257
+ const name = 'extendReservation';
258
+ const p = anfrage(name, request);
259
+ schluessel(name, p, { pflicht: true });
260
+ kennung(name, 'reservationId', p.reservationId);
261
+ minuten(name, p.expiresInMinutes, { pflicht: true });
262
+ return reservierungAus(name, await transport(name, p));
263
+ }
264
+ /**
265
+ * Gibt reservierte Ware frei: ohne `items` alles, sonst je Position (ohne
266
+ * `quantity` der ganze offene Rest). Die Antwort traegt den Stand danach;
267
+ * `released` wird der Status erst, wenn nichts mehr offen ist.
268
+ */
269
+ export async function releaseReservation(transport, request) {
270
+ const name = 'releaseReservation';
271
+ const p = anfrage(name, request);
272
+ schluessel(name, p, { pflicht: true });
273
+ kennung(name, 'reservationId', p.reservationId);
274
+ if (Array.isArray(p.items) && p.items.length === 0) {
275
+ throw anfragefehler(name, 'items ist leer; um alles freizugeben, items ganz weglassen');
276
+ }
277
+ positionen(name, p.items, { pflicht: false, menge: 'frei' });
278
+ return reservierungAus(name, await transport(name, p));
279
+ }
280
+ /** Eine Reservierung mit ihrem aktuellen Stand. */
281
+ export async function getReservation(transport, reservationId) {
282
+ const name = 'getReservation';
283
+ return reservierungAus(name, await transport(name, { reservationId: kennung(name, 'reservationId', reservationId) }));
284
+ }
285
+ /** Reservierungen, neueste zuerst; Filter `status` und/oder `reference`. */
286
+ export async function listReservations(transport, query) {
287
+ const name = 'listReservations';
288
+ const daten = await transport(name, abfrage(name, query));
289
+ return { reservations: liste(name, daten, 'reservations', reservierung), nextCursor: naechsterCursor(name, daten) };
290
+ }
291
+ /** Alle Reservierungen der Abfrage, Seite fuer Seite ueber `nextCursor`. */
292
+ export function iterateReservations(transport, query) {
293
+ const { cursor, ...rest } = query ?? {};
294
+ return seitenweise('listReservations', cursor, async (c) => {
295
+ const s = await listReservations(transport, { ...rest, cursor: c });
296
+ return { eintraege: s.reservations, nextCursor: s.nextCursor };
297
+ });
298
+ }