@kreiseck/kasseneck-api 1.7.0 → 1.8.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 (115) hide show
  1. package/CHANGELOG.md +122 -0
  2. package/README.md +170 -1
  3. package/dist/cjs/client/aufrufe.d.ts +9 -7
  4. package/dist/cjs/client/aufrufe.js +57 -2
  5. package/dist/cjs/client/errors.js +2 -0
  6. package/dist/cjs/client/index.d.ts +1 -1
  7. package/dist/cjs/client/index.js +2 -1
  8. package/dist/cjs/client/transport.d.ts +27 -3
  9. package/dist/cjs/client/transport.js +29 -3
  10. package/dist/cjs/index.d.ts +1 -1
  11. package/dist/cjs/index.js +3 -2
  12. package/dist/cjs/inventory/api.d.ts +21 -2
  13. package/dist/cjs/inventory/api.js +22 -3
  14. package/dist/cjs/inventory/fehler.d.ts +6 -0
  15. package/dist/cjs/inventory/fehler.js +12 -0
  16. package/dist/cjs/inventory/index.d.ts +6 -4
  17. package/dist/cjs/inventory/index.js +33 -3
  18. package/dist/cjs/inventory/inventur.d.ts +104 -0
  19. package/dist/cjs/inventory/inventur.js +245 -0
  20. package/dist/cjs/inventory/lesen.d.ts +20 -1
  21. package/dist/cjs/inventory/lesen.js +307 -0
  22. package/dist/cjs/inventory/typen.d.ts +410 -1
  23. package/dist/cjs/inventory/vertrag.d.ts +94 -8
  24. package/dist/cjs/inventory/vertrag.js +117 -5
  25. package/dist/cjs/invoice/vertrag.d.ts +1 -1
  26. package/dist/cjs/invoice/vertrag.js +2 -0
  27. package/dist/cjs/models/cancellation.d.ts +1 -1
  28. package/dist/cjs/models/cancellation.js +2 -0
  29. package/dist/cjs/models/payment-errors.d.ts +1 -1
  30. package/dist/cjs/models/payment-errors.js +2 -0
  31. package/dist/cjs/models/receipt-email.d.ts +1 -1
  32. package/dist/cjs/models/receipt-email.js +2 -0
  33. package/dist/cjs/models/receipt-errors.d.ts +1 -1
  34. package/dist/cjs/models/receipt-errors.js +2 -0
  35. package/dist/cjs/pos/errors.d.ts +4 -3
  36. package/dist/cjs/pos/errors.js +23 -2
  37. package/dist/cjs/pos/index.d.ts +4 -1
  38. package/dist/cjs/pos/index.js +9 -2
  39. package/dist/cjs/pos/inventur.d.ts +104 -0
  40. package/dist/cjs/pos/inventur.js +117 -0
  41. package/dist/cjs/pos/texte.d.ts +82 -0
  42. package/dist/cjs/pos/texte.js +35 -0
  43. package/dist/cjs/react/index.js +6 -0
  44. package/dist/cjs/register/errors.d.ts +1 -1
  45. package/dist/cjs/register/errors.js +2 -0
  46. package/dist/cjs/stored/vokabular.d.ts +1 -1
  47. package/dist/cjs/stored/vokabular.js +1 -1
  48. package/dist/cjs/version.d.ts +1 -1
  49. package/dist/cjs/version.js +1 -1
  50. package/dist/esm/client/aufrufe.d.ts +9 -7
  51. package/dist/esm/client/aufrufe.js +57 -2
  52. package/dist/esm/client/errors.js +2 -0
  53. package/dist/esm/client/index.d.ts +1 -1
  54. package/dist/esm/client/index.js +1 -1
  55. package/dist/esm/client/transport.d.ts +27 -3
  56. package/dist/esm/client/transport.js +28 -3
  57. package/dist/esm/index.d.ts +1 -1
  58. package/dist/esm/index.js +1 -1
  59. package/dist/esm/inventory/api.d.ts +21 -2
  60. package/dist/esm/inventory/api.js +23 -4
  61. package/dist/esm/inventory/fehler.d.ts +6 -0
  62. package/dist/esm/inventory/fehler.js +11 -0
  63. package/dist/esm/inventory/index.d.ts +6 -4
  64. package/dist/esm/inventory/index.js +5 -3
  65. package/dist/esm/inventory/inventur.d.ts +104 -0
  66. package/dist/esm/inventory/inventur.js +226 -0
  67. package/dist/esm/inventory/lesen.d.ts +20 -1
  68. package/dist/esm/inventory/lesen.js +301 -0
  69. package/dist/esm/inventory/typen.d.ts +410 -1
  70. package/dist/esm/inventory/vertrag.d.ts +94 -8
  71. package/dist/esm/inventory/vertrag.js +116 -4
  72. package/dist/esm/invoice/vertrag.d.ts +1 -1
  73. package/dist/esm/invoice/vertrag.js +2 -0
  74. package/dist/esm/models/cancellation.d.ts +1 -1
  75. package/dist/esm/models/cancellation.js +2 -0
  76. package/dist/esm/models/payment-errors.d.ts +1 -1
  77. package/dist/esm/models/payment-errors.js +2 -0
  78. package/dist/esm/models/receipt-email.d.ts +1 -1
  79. package/dist/esm/models/receipt-email.js +2 -0
  80. package/dist/esm/models/receipt-errors.d.ts +1 -1
  81. package/dist/esm/models/receipt-errors.js +2 -0
  82. package/dist/esm/pos/errors.d.ts +4 -3
  83. package/dist/esm/pos/errors.js +23 -2
  84. package/dist/esm/pos/index.d.ts +4 -1
  85. package/dist/esm/pos/index.js +2 -1
  86. package/dist/esm/pos/inventur.d.ts +104 -0
  87. package/dist/esm/pos/inventur.js +109 -0
  88. package/dist/esm/pos/texte.d.ts +82 -0
  89. package/dist/esm/pos/texte.js +35 -0
  90. package/dist/esm/react/index.js +6 -0
  91. package/dist/esm/register/errors.d.ts +1 -1
  92. package/dist/esm/register/errors.js +2 -0
  93. package/dist/esm/stored/vokabular.d.ts +1 -1
  94. package/dist/esm/stored/vokabular.js +1 -1
  95. package/dist/esm/version.d.ts +1 -1
  96. package/dist/esm/version.js +1 -1
  97. package/fixtures/hobex-hps-codes.json +1 -1
  98. package/fixtures/invoice-api.schema.json +1 -1
  99. package/fixtures/invoice-texts.json +1 -1
  100. package/fixtures/pos-texts.json +94 -1
  101. package/fixtures/stocktake-quantity-cases.json +55 -0
  102. package/fixtures/surface.json +151 -8
  103. package/fixtures/v3/antworten/belege.json +1 -1
  104. package/fixtures/v3/antworten/belegmail.json +1 -1
  105. package/fixtures/v3/antworten/kasse-belege.json +1 -1
  106. package/fixtures/v3/antworten/kasse.json +2958 -3
  107. package/fixtures/v3/antworten/lager.json +1727 -3
  108. package/fixtures/v3/antworten/rechnungen.json +1 -1
  109. package/fixtures/v3/antworten/storno.json +1 -1
  110. package/fixtures/v3/stored/belege.json +1 -1
  111. package/fixtures/v3/stored/kasse.json +414 -3
  112. package/fixtures/v3/stored/rechnungen.json +1 -1
  113. package/fixtures/v3/v3-vokabular.json +475 -8
  114. package/fixtures/v3/zahlbetrag-faelle.json +1 -1
  115. package/package.json +1 -1
@@ -0,0 +1,226 @@
1
+ /**
2
+ * Inventur der Lager-API (Lager-Kern Stufe 3): anlegen, zaehlen, Zaehlungen
3
+ * stornieren, pruefen, nachzaehlen, abschliessen, abbrechen, Inventurprotokoll
4
+ * holen; dazu die Listen der Inventuren, Positionen und Zaehlungen.
5
+ *
6
+ * Ablauf: `createStocktake` → `recordStocktakeCount` (beliebig oft, auch von
7
+ * mehreren Geraeten; je Position addiert) → `reviewStocktake` (erst jetzt Soll
8
+ * und Differenz) → bei Bedarf `recountStocktake` und erneut `reviewStocktake`
9
+ * → `closeStocktake` → `getStocktakePdf`. Pruefen und Abschliessen rechnet der
10
+ * Server im Hintergrund: die Antwort traegt den Zwischenstand
11
+ * (`review.complete: false` bzw. `status: 'closing'`), `getStocktake` den
12
+ * Fortgang.
13
+ *
14
+ * **Blind:** vor `review` traegt keine Antwort ein Soll; `blind: false` zeigt
15
+ * beim Zaehlen nur den heutigen Buchbestand (`bookStockNow`), nie das Soll
16
+ * zur Referenzzeit.
17
+ *
18
+ * Rechte wie in Stufe 5b: Lesen mit jedem Schluessel, alles Schreibende (auch
19
+ * Zaehlen) mit dem Konto-Schalter „Lager-API schreiben“ (Test-Schluessel
20
+ * immer), sonst `inventory_api_not_enabled`; Werte und das Protokoll mit
21
+ * Werten nur mit `lagerApi.kosten`.
22
+ *
23
+ * **Vor dem Senden geprueft wird nur, was ohne Netz sicher falsch ist:**
24
+ * `idempotencyKey` (Pflicht bei jedem Schreiben, 1–120 Zeichen), fehlende
25
+ * Kennungen (`stocktakeId`, `articleId`, `countId`, `locationId`), `scope`
26
+ * kein Objekt, eine Menge, die keine Ganzzahl ist (Tausendstel), ein leerer
27
+ * Grund, `items` beim Nachzaehlen leer oder ohne `articleId`,
28
+ * `serialNumbers` keine Liste, `uncountedAsZero` kein Wahrheitswert. Alles
29
+ * Fachliche (Umfang, Stichtag, Seriennummern, Zustand) prueft der Server.
30
+ *
31
+ * **Wiederholen** wie bei jedem Schreiben: nach `outcome: 'unknown'` denselben
32
+ * Aufruf mit **demselben** `idempotencyKey`. Eine Zaehlung wirkt dann genau
33
+ * einmal (das Zaehldokument ist selbst der Nachweis); ein neuer Schluessel
34
+ * zaehlte die Ware ein zweites Mal.
35
+ */
36
+ import { abfrage, anfragefehler, kennung, seitenweise } from './endpunkte.js';
37
+ import { antwortfehler, inventur, inventurPosition, inventurWarnungen, inventurZaehlung, liste, naechsterCursor, objekt, protokollLink, zaehlungMitPosition, } from './lesen.js';
38
+ import { anfrage, ganz, schluessel } from './schreiben.js';
39
+ const istObjekt = (w) => w !== null && typeof w === 'object' && !Array.isArray(w);
40
+ const kopfAus = (name, daten) => inventur({ name, pfad: 'stocktake' }, objekt(daten)?.stocktake);
41
+ /** Ein Grund (Storno, Nachzaehlen, Abbruch): nicht leer. Die Laenge prueft der Server. */
42
+ function grund(name, w) {
43
+ if (typeof w !== 'string' || w.trim() === '')
44
+ throw anfragefehler(name, 'reason fehlt (Grund mit 1 bis 500 Zeichen)');
45
+ }
46
+ /** Schreibende Anfrage mit `stocktakeId`: Schluessel und Kennung geprueft, flach wie gesendet. */
47
+ function schreibend(name, request) {
48
+ const p = anfrage(name, request);
49
+ schluessel(name, p, { pflicht: true });
50
+ kennung(name, 'stocktakeId', p.stocktakeId);
51
+ return p;
52
+ }
53
+ /** Eine Liste unter einer Inventur: `stocktakeId` Pflicht, sonst wie jede Abfrage. */
54
+ function abfrageMitInventur(name, query) {
55
+ if (!istObjekt(query))
56
+ throw anfragefehler(name, 'Abfrage mit stocktakeId erwartet');
57
+ const p = abfrage(name, query);
58
+ kennung(name, 'stocktakeId', p.stocktakeId);
59
+ return p;
60
+ }
61
+ // ---- Anlegen und Lesen -------------------------------------------------------------
62
+ /**
63
+ * Legt eine Inventur an. Je Standort hoechstens eine offene
64
+ * (`stocktake_location_busy`, `data.stocktakeId` nennt sie); Umfang `groups`
65
+ * bzw. `articles` nur mit bestandsgefuehrten Artikeln (`article_not_tracked`).
66
+ * Jede Inventur beginnt in `counting`.
67
+ */
68
+ export async function createStocktake(transport, request) {
69
+ const name = 'createStocktake';
70
+ const p = anfrage(name, request);
71
+ schluessel(name, p, { pflicht: true });
72
+ kennung(name, 'locationId', p.locationId);
73
+ if (!istObjekt(p.scope))
74
+ throw anfragefehler(name, 'scope fehlt oder ist kein Objekt { type, groupIds?, articleIds? }');
75
+ return kopfAus(name, await transport(name, p));
76
+ }
77
+ /**
78
+ * Inventuren des Kontos. Ohne `updatedSince` zuletzt geaenderte zuerst; mit
79
+ * `updatedSince` aufsteigend nach `updatedAt` und inklusive (Abgleich: das
80
+ * groesste gesehene `updatedAt` als naechstes `updatedSince`). Filter
81
+ * `status` und `locationId`.
82
+ */
83
+ export async function listStocktakes(transport, query) {
84
+ const name = 'listStocktakes';
85
+ const daten = await transport(name, abfrage(name, query));
86
+ return { stocktakes: liste(name, daten, 'stocktakes', inventur), nextCursor: naechsterCursor(name, daten) };
87
+ }
88
+ /** Alle Inventuren der Abfrage, Seite fuer Seite, in der Reihenfolge von [listStocktakes]. */
89
+ export function iterateStocktakes(transport, query) {
90
+ const { cursor, ...rest } = query ?? {};
91
+ return seitenweise('listStocktakes', cursor, async (c) => {
92
+ const s = await listStocktakes(transport, { ...rest, cursor: c });
93
+ return { eintraege: s.stocktakes, nextCursor: s.nextCursor };
94
+ });
95
+ }
96
+ /** Eine Inventur samt `progress.counted` (gezaehlte Positionen). */
97
+ export async function getStocktake(transport, stocktakeId) {
98
+ const name = 'getStocktake';
99
+ return kopfAus(name, await transport(name, { stocktakeId: kennung(name, 'stocktakeId', stocktakeId) }));
100
+ }
101
+ /** Positionen einer Inventur nach Kennung; `openOnly` = ungezaehlt bzw. in `review` zum Nachzaehlen offen. */
102
+ export async function listStocktakeItems(transport, query) {
103
+ const name = 'listStocktakeItems';
104
+ const daten = await transport(name, abfrageMitInventur(name, query));
105
+ return { items: liste(name, daten, 'items', inventurPosition), nextCursor: naechsterCursor(name, daten) };
106
+ }
107
+ export function iterateStocktakeItems(transport, query) {
108
+ const { cursor, ...rest } = query;
109
+ return seitenweise('listStocktakeItems', cursor, async (c) => {
110
+ const s = await listStocktakeItems(transport, { ...rest, cursor: c });
111
+ return { eintraege: s.items, nextCursor: s.nextCursor };
112
+ });
113
+ }
114
+ /** Zaehlungen einer Inventur, neueste zuerst, auch stornierte; mit `articleId` nur die des Artikels. */
115
+ export async function listStocktakeCounts(transport, query) {
116
+ const name = 'listStocktakeCounts';
117
+ const daten = await transport(name, abfrageMitInventur(name, query));
118
+ return { counts: liste(name, daten, 'counts', inventurZaehlung), nextCursor: naechsterCursor(name, daten) };
119
+ }
120
+ export function iterateStocktakeCounts(transport, query) {
121
+ const { cursor, ...rest } = query;
122
+ return seitenweise('listStocktakeCounts', cursor, async (c) => {
123
+ const s = await listStocktakeCounts(transport, { ...rest, cursor: c });
124
+ return { eintraege: s.counts, nextCursor: s.nextCursor };
125
+ });
126
+ }
127
+ // ---- Zaehlen und Stornieren ------------------------------------------------------
128
+ /** Pruefung einer Zaehlung vor dem Senden, geteilt mit dem Kassenweg. Paketintern. */
129
+ export function zaehlungPruefen(name, p) {
130
+ kennung(name, 'articleId', p.articleId);
131
+ ganz(name, 'quantity', p.quantity, { pflicht: true });
132
+ if (p.serialNumbers !== undefined && p.serialNumbers !== null && !Array.isArray(p.serialNumbers)) {
133
+ throw anfragefehler(name, 'serialNumbers muss eine Liste sein');
134
+ }
135
+ }
136
+ /** Pruefung eines Stornos vor dem Senden, geteilt mit dem Kassenweg. Paketintern. */
137
+ export function stornoPruefen(name, p) {
138
+ kennung(name, 'countId', p.countId);
139
+ grund(name, p.reason);
140
+ }
141
+ /**
142
+ * Eine Zaehlung. Antwort: die Zaehlung und ihre Position danach (Summe der
143
+ * Runde in `item.quantity`). Gezaehlt wird in `counting`, in `review` nur an
144
+ * Positionen, die zum Nachzaehlen frei sind (sonst `stocktake_not_open`).
145
+ */
146
+ export async function recordStocktakeCount(transport, request) {
147
+ const name = 'recordStocktakeCount';
148
+ const p = schreibend(name, request);
149
+ zaehlungPruefen(name, p);
150
+ return zaehlungMitPosition(name, await transport(name, p));
151
+ }
152
+ /** Storniert eine Zaehlung mit Grund; die Position wird aus den uebrigen Zaehlungen neu summiert. */
153
+ export async function voidStocktakeCount(transport, request) {
154
+ const name = 'voidStocktakeCount';
155
+ const p = schreibend(name, request);
156
+ stornoPruefen(name, p);
157
+ return zaehlungMitPosition(name, await transport(name, p));
158
+ }
159
+ // ---- Pruefen bis Abbruch ------------------------------------------------------------
160
+ /**
161
+ * Pruefen: `counting` → `review`, in `review` neu rechnen (nach dem
162
+ * Nachzaehlen). Die Antwort kommt sofort mit `review.complete: false`; Soll
163
+ * und Differenz stehen an den Positionen, sobald `complete` `true` ist.
164
+ */
165
+ export async function reviewStocktake(transport, request) {
166
+ const name = 'reviewStocktake';
167
+ return kopfAus(name, await transport(name, schreibend(name, request)));
168
+ }
169
+ /** Nachzaehlen: je genannter Position eine neue Runde; danach zaehlen und erneut [reviewStocktake]. */
170
+ export async function recountStocktake(transport, request) {
171
+ const name = 'recountStocktake';
172
+ const p = schreibend(name, request);
173
+ if (!Array.isArray(p.items) || p.items.length === 0)
174
+ throw anfragefehler(name, 'items fehlt oder ist leer');
175
+ p.items.forEach((x, i) => {
176
+ if (!istObjekt(x))
177
+ throw anfragefehler(name, `items[${i}] muss ein Objekt sein`);
178
+ kennung(name, `items[${i}].articleId`, x.articleId);
179
+ });
180
+ grund(name, p.reason);
181
+ return kopfAus(name, await transport(name, p));
182
+ }
183
+ /**
184
+ * Abschliessen (nur aus `review`, sonst `stocktake_not_in_review`; offene
185
+ * Nachzaehlungen ergeben `stocktake_recount_open`). Der Server bucht in
186
+ * Teilen weiter; die Antwort traegt meist `status: 'closing'`. Ein erneuter
187
+ * Aufruf waehrend `closing` stoesst den Abschluss wieder an.
188
+ */
189
+ export async function closeStocktake(transport, request) {
190
+ const name = 'closeStocktake';
191
+ const p = schreibend(name, request);
192
+ if (p.uncountedAsZero !== undefined && typeof p.uncountedAsZero !== 'boolean') {
193
+ throw anfragefehler(name, 'uncountedAsZero muss true oder false sein');
194
+ }
195
+ const daten = await transport(name, p);
196
+ return { stocktake: kopfAus(name, daten), warnings: inventurWarnungen({ name, pfad: 'warnings' }, objekt(daten)?.warnings) };
197
+ }
198
+ /** Abbrechen mit Grund (aus `counting` oder `review`); der Standort ist danach frei. */
199
+ export async function cancelStocktake(transport, request) {
200
+ const name = 'cancelStocktake';
201
+ const p = schreibend(name, request);
202
+ grund(name, p.reason);
203
+ return kopfAus(name, await transport(name, p));
204
+ }
205
+ // ---- Protokoll ------------------------------------------------------------------
206
+ /**
207
+ * Das Inventurprotokoll (PDF), erst nach dem Abschluss (`stocktake_not_closed`,
208
+ * auch fuer eine abgebrochene Inventur). Mit `lagerApi.kosten` die Fassung mit
209
+ * Werten, sonst die nur mit Mengen. Bis 9 MiB kommt die Datei selbst
210
+ * (`kind: 'pdf'`), darueber ein signierter Lese-Link fuer 15 Minuten
211
+ * (`kind: 'download'`); die geladene Datei an `download.sha256` pruefen.
212
+ *
213
+ * Als freie Funktion braucht sie den Transport aus `createPdfOrDataTransport`
214
+ * (Paketwurzel): ein JSON-Transport laese die Datei als Text, ein reiner
215
+ * Binaer-Transport kennt den Link nicht. [createInventoryClient] bindet ihn selbst.
216
+ */
217
+ export async function getStocktakePdf(transport, stocktakeId) {
218
+ const name = 'getStocktakePdf';
219
+ const ergebnis = await transport(name, { stocktakeId: kennung(name, 'stocktakeId', stocktakeId) });
220
+ if ('pdf' in ergebnis)
221
+ return { kind: 'pdf', pdf: ergebnis.pdf };
222
+ const link = objekt(ergebnis.data)?.download;
223
+ if (link === undefined || link === null)
224
+ throw antwortfehler(name, 'Antwort ist weder ein PDF noch ein Lese-Link (data.download fehlt)');
225
+ return { kind: 'download', download: protokollLink(name, link) };
226
+ }
@@ -12,7 +12,7 @@
12
12
  * wenn der Server sie sendet: „fehlt“ heisst dort „kein Recht“, nicht „leer“.
13
13
  */
14
14
  import { KasseneckValidationError } from '../client/errors.js';
15
- import type { Article, GoodsReceiptPreviewLine, InventoryShortfall, InventoryWebhook, InventoryWebhookDelivery, InventoryWebhookTestDelivery, Location, Reservation, StockBelowMinimumEventData, StockChangedEventData, StockLevel, StockMovement, StockOperation, StockValue, VariantGroup } from './typen.js';
15
+ import type { Article, GoodsReceiptPreviewLine, InventoryShortfall, InventoryWebhook, InventoryWebhookDelivery, InventoryWebhookTestDelivery, Location, Reservation, StockBelowMinimumEventData, StockChangedEventData, StockLevel, StockMovement, StockOperation, StockValue, Stocktake, StocktakeCount, StocktakeCountResult, StocktakeItem, StocktakePdfDownload, StocktakeWarning, VariantGroup } from './typen.js';
16
16
  type Roh = Record<string, unknown>;
17
17
  /** Ein Leser bekommt den Vorgang (fuer die Meldung) und den Pfad im Rumpf. */
18
18
  export interface Ort {
@@ -44,6 +44,25 @@ export declare function fehlmengen(roh: unknown): InventoryShortfall[];
44
44
  * bzw. „keine Varianten“).
45
45
  */
46
46
  export declare function variantengruppe(ort: Ort, w: unknown): VariantGroup;
47
+ /** Hinweise einer Inventur; fehlt die Liste, gibt es keine. */
48
+ export declare function inventurWarnungen(ort: Ort, w: unknown): StocktakeWarning[];
49
+ /**
50
+ * Kopf einer Inventur. Was erst nach dem Abschluss kommt (Summen, Hinweise,
51
+ * Siegel, Pruefsumme, Protokoll), steht im Modell nur, wenn der Server es
52
+ * sendet; Werte nur mit dem Kosten-Recht.
53
+ */
54
+ export declare function inventur(ort: Ort, w: unknown): Stocktake;
55
+ /**
56
+ * Eine Position. Soll, Differenz und alles aus Pruefung und Abschluss stehen
57
+ * im Modell nur, wenn der Server sie sendet (blind: vor `review` nie).
58
+ */
59
+ export declare function inventurPosition(ort: Ort, w: unknown): StocktakeItem;
60
+ /** Eine Zaehlung: Menge, Runde und Kennungen muessen da sein. */
61
+ export declare function inventurZaehlung(ort: Ort, w: unknown): StocktakeCount;
62
+ /** Antwort von Zaehlen und Stornieren: beide Teile sind zugesagt. */
63
+ export declare function zaehlungMitPosition(name: string, daten: unknown): StocktakeCountResult;
64
+ /** Lese-Link auf ein grosses Inventurprotokoll; ohne Adresse, Ablauf, Groesse oder Pruefsumme ist er unbrauchbar. */
65
+ export declare function protokollLink(name: string, w: unknown): StocktakePdfDownload;
47
66
  /** Eine zugesagte Liste `data.<feld>`, Eintrag fuer Eintrag gelesen. */
48
67
  export declare function liste<T>(name: string, daten: unknown, feld: string, lesen: (ort: Ort, e: unknown) => T): T[];
49
68
  /** Der Cursor der naechsten Seite; `null` am Ende. Etwas anderes als Text oder `null` ist kaputt. */
@@ -455,6 +455,307 @@ export function variantengruppe(ort, w) {
455
455
  updatedAt: text(g.updatedAt),
456
456
  };
457
457
  }
458
+ // ---- Inventur (Lager-Kern Stufe 3) ---------------------------------------------------
459
+ const unter = (ort, feld) => ({ name: ort.name, pfad: ort.pfad ? `${ort.pfad}.${feld}` : feld });
460
+ /** Ein Unterobjekt, das `null` sein darf (`review`, `closing` …); etwas anderes als Objekt oder `null` ist kaputt. */
461
+ function objektOderNull(ort, feld, w) {
462
+ if (w === undefined || w === null)
463
+ return null;
464
+ const o = objekt(w);
465
+ if (!o)
466
+ throw antwortfehler(ort.name, `Antwort ist unbrauchbar (data.${ort.pfad}.${feld} ist kein Objekt)`);
467
+ return o;
468
+ }
469
+ /** Ein Wahrheitswert, der da sein muss: ein fehlendes „gezaehlt“ ist nicht „ungezaehlt“. */
470
+ function wahrheitswert(ort, feld, w) {
471
+ if (typeof w !== 'boolean')
472
+ throw antwortfehler(ort.name, `Antwort enthaelt keinen Wahrheitswert (data.${ort.pfad}.${feld})`);
473
+ return w;
474
+ }
475
+ /** Eine Liste von Texten, die fehlen darf (dann leer); etwas anderes ist kaputt. */
476
+ function textlisteOderLeer(ort, feld, w) {
477
+ if (w === undefined || w === null)
478
+ return [];
479
+ return textliste(ort, feld, w);
480
+ }
481
+ /** Wer etwas tat; fehlt die Angabe, `null`. Etwas anderes als ein Objekt ist kaputt, nie still „niemand“. */
482
+ function akteur(ort, feld, w) {
483
+ if (w === undefined || w === null)
484
+ return null;
485
+ const a = objekt(w);
486
+ if (!a)
487
+ throw antwortfehler(ort.name, `Antwort ist unbrauchbar (data.${ort.pfad}.${feld} ist kein Objekt)`);
488
+ return { type: textOderNull(a.type), id: textOderNull(a.id), name: textOderNull(a.name) };
489
+ }
490
+ function inventurWarnung(ort, w) {
491
+ const h = eintrag(ort, w);
492
+ if (typeof h.code !== 'string' || h.code === '')
493
+ throw antwortfehler(ort.name, `Hinweis ohne Code (data.${ort.pfad}.code)`);
494
+ return { code: h.code, items: ganzzahl(ort, 'items', h.items), message: typeof h.message === 'string' ? h.message : null };
495
+ }
496
+ /** Hinweise einer Inventur; fehlt die Liste, gibt es keine. */
497
+ export function inventurWarnungen(ort, w) {
498
+ if (w === undefined || w === null)
499
+ return [];
500
+ if (!Array.isArray(w))
501
+ throw antwortfehler(ort.name, `Antwort ist unbrauchbar (data.${ort.pfad} ist keine Liste)`);
502
+ return w.map((h, i) => inventurWarnung({ name: ort.name, pfad: `${ort.pfad}[${i}]` }, h));
503
+ }
504
+ function siegel(ort, w) {
505
+ const s = eintrag(ort, w);
506
+ const raus = {
507
+ fromDay: textOderNull(s.fromDay),
508
+ toDay: textOderNull(s.toDay),
509
+ daysChecked: ganzzahlOderNull(ort, 'daysChecked', s.daysChecked),
510
+ verified: typeof s.verified === 'boolean' ? s.verified : null,
511
+ firstBreak: textOderNull(s.firstBreak),
512
+ gaps: textlisteOderLeer(ort, 'gaps', s.gaps),
513
+ gapCount: ganzzahlOderNull(ort, 'gapCount', s.gapCount),
514
+ };
515
+ if (typeof s.notChecked === 'string')
516
+ raus.notChecked = s.notChecked;
517
+ if (hat(s, 'checkedUntil'))
518
+ raus.checkedUntil = textOderNull(s.checkedUntil);
519
+ return raus;
520
+ }
521
+ /**
522
+ * Kopf einer Inventur. Was erst nach dem Abschluss kommt (Summen, Hinweise,
523
+ * Siegel, Pruefsumme, Protokoll), steht im Modell nur, wenn der Server es
524
+ * sendet; Werte nur mit dem Kosten-Recht.
525
+ */
526
+ export function inventur(ort, w) {
527
+ const k = eintrag(ort, w);
528
+ const umfang = objektOderNull(ort, 'scope', k.scope);
529
+ const umfangOrt = unter(ort, 'scope');
530
+ const fortschritt = objektOderNull(ort, 'progress', k.progress);
531
+ const fOrt = unter(ort, 'progress');
532
+ const pruefung = objektOderNull(ort, 'review', k.review);
533
+ const pOrt = unter(ort, 'review');
534
+ const abschluss = objektOderNull(ort, 'closing', k.closing);
535
+ const aOrt = unter(ort, 'closing');
536
+ const abbruch = objektOderNull(ort, 'cancellation', k.cancellation);
537
+ const raus = {
538
+ id: kennung(ort, 'id', k.id),
539
+ name: text(k.name),
540
+ locationId: textOderNull(k.locationId),
541
+ scope: umfang
542
+ ? {
543
+ type: textOderNull(umfang.type),
544
+ groupIds: textlisteOderLeer(umfangOrt, 'groupIds', umfang.groupIds),
545
+ articleIds: textlisteOderLeer(umfangOrt, 'articleIds', umfang.articleIds),
546
+ }
547
+ : null,
548
+ type: textOderNull(k.type),
549
+ keyDate: textOderNull(k.keyDate),
550
+ // Blind ist die sichere Vorgabe: nur ein ausdrueckliches false zeigt Bestand.
551
+ blind: k.blind !== false,
552
+ status: textOderNull(k.status),
553
+ progress: fortschritt
554
+ ? {
555
+ items: ganzzahl(fOrt, 'items', fortschritt.items),
556
+ counted: ganzzahlOderNull(fOrt, 'counted', fortschritt.counted),
557
+ recountOpen: fortschritt.recountOpen === true,
558
+ }
559
+ : null,
560
+ createdAt: text(k.createdAt),
561
+ createdBy: akteur(ort, 'createdBy', k.createdBy),
562
+ source: textOderNull(k.source),
563
+ updatedAt: text(k.updatedAt),
564
+ review: pruefung
565
+ ? {
566
+ startedAt: text(pruefung.startedAt),
567
+ startedBy: akteur(pOrt, 'startedBy', pruefung.startedBy),
568
+ complete: pruefung.complete === true,
569
+ expectedAsOf: text(pruefung.expectedAsOf),
570
+ recountUncounted: ganzzahlOderNull(pOrt, 'recountUncounted', pruefung.recountUncounted),
571
+ }
572
+ : null,
573
+ closing: abschluss
574
+ ? {
575
+ startedAt: text(abschluss.startedAt),
576
+ startedBy: akteur(aOrt, 'startedBy', abschluss.startedBy),
577
+ uncountedAsZero: abschluss.uncountedAsZero === true,
578
+ parts: ganzzahlOderNull(aOrt, 'parts', abschluss.parts),
579
+ bookedParts: ganzzahlOderNull(aOrt, 'bookedParts', abschluss.bookedParts),
580
+ completedAt: text(abschluss.completedAt),
581
+ }
582
+ : null,
583
+ cancellation: abbruch
584
+ ? { reason: text(abbruch.reason), cancelledAt: text(abbruch.cancelledAt), cancelledBy: akteur(unter(ort, 'cancellation'), 'cancelledBy', abbruch.cancelledBy) }
585
+ : null,
586
+ };
587
+ const summen = objektOderNull(ort, 'totals', k.totals);
588
+ if (summen) {
589
+ const sOrt = unter(ort, 'totals');
590
+ raus.totals = {
591
+ items: ganzzahl(sOrt, 'items', summen.items),
592
+ counted: ganzzahl(sOrt, 'counted', summen.counted),
593
+ uncounted: ganzzahl(sOrt, 'uncounted', summen.uncounted),
594
+ recounted: ganzzahl(sOrt, 'recounted', summen.recounted),
595
+ withDifference: ganzzahl(sOrt, 'withDifference', summen.withDifference),
596
+ needsCheck: ganzzahl(sOrt, 'needsCheck', summen.needsCheck),
597
+ notBooked: ganzzahl(sOrt, 'notBooked', summen.notBooked),
598
+ };
599
+ for (const feld of ['differenceValueCents', 'inventoryValueCents']) {
600
+ if (hat(summen, feld))
601
+ raus.totals[feld] = ganzzahlOderNull(sOrt, feld, summen[feld]);
602
+ }
603
+ }
604
+ if (hat(k, 'warnings'))
605
+ raus.warnings = inventurWarnungen(unter(ort, 'warnings'), k.warnings);
606
+ if (hat(k, 'seal') && k.seal !== null)
607
+ raus.seal = siegel(unter(ort, 'seal'), k.seal);
608
+ if (hat(k, 'checksum'))
609
+ raus.checksum = textOderNull(k.checksum);
610
+ if (hat(k, 'inventoryAsOf'))
611
+ raus.inventoryAsOf = textOderNull(k.inventoryAsOf);
612
+ const pdf = objektOderNull(ort, 'pdf', k.pdf);
613
+ if (pdf) {
614
+ raus.pdf = { available: pdf.available === true };
615
+ if (typeof pdf.valuesSha256 === 'string')
616
+ raus.pdf.valuesSha256 = pdf.valuesSha256;
617
+ if (typeof pdf.quantitiesSha256 === 'string')
618
+ raus.pdf.quantitiesSha256 = pdf.quantitiesSha256;
619
+ }
620
+ return raus;
621
+ }
622
+ function nichtGebucht(ort, w) {
623
+ const n = eintrag(ort, w);
624
+ if (typeof n.code !== 'string' || n.code === '')
625
+ throw antwortfehler(ort.name, `Antwort enthaelt keinen Grund (data.${ort.pfad}.code)`);
626
+ const raus = { code: n.code, quantity: ganzzahlOderNull(ort, 'quantity', n.quantity) };
627
+ if (n.reasons !== undefined && n.reasons !== null) {
628
+ if (!Array.isArray(n.reasons))
629
+ throw antwortfehler(ort.name, `Antwort ist unbrauchbar (data.${ort.pfad}.reasons ist keine Liste)`);
630
+ raus.reasons = n.reasons.map((g, i) => {
631
+ const gOrt = { name: ort.name, pfad: `${ort.pfad}.reasons[${i}]` };
632
+ const r = eintrag(gOrt, g);
633
+ if (typeof r.code !== 'string' || r.code === '')
634
+ throw antwortfehler(ort.name, `Antwort enthaelt keinen Grund (data.${gOrt.pfad}.code)`);
635
+ return { code: r.code, quantity: ganzzahlOderNull(gOrt, 'quantity', r.quantity) };
636
+ });
637
+ }
638
+ return raus;
639
+ }
640
+ /**
641
+ * Eine Position. Soll, Differenz und alles aus Pruefung und Abschluss stehen
642
+ * im Modell nur, wenn der Server sie sendet (blind: vor `review` nie).
643
+ */
644
+ export function inventurPosition(ort, w) {
645
+ const p = eintrag(ort, w);
646
+ // Wer gezaehlt hat, ist zugesagt: fehlt die Liste, ist die Antwort kaputt (nie „niemand“).
647
+ if (!Array.isArray(p.countedBy))
648
+ throw antwortfehler(ort.name, `Antwort ist unbrauchbar (data.${ort.pfad}.countedBy ist keine Liste)`);
649
+ const zaehler = p.countedBy;
650
+ const nachzaehlen = objektOderNull(ort, 'recount', p.recount);
651
+ const nOrt = unter(ort, 'recount');
652
+ const raus = {
653
+ articleId: kennung(ort, 'articleId', p.articleId),
654
+ condition: kennung(ort, 'condition', p.condition),
655
+ name: text(p.name),
656
+ number: text(p.number),
657
+ unit: text(p.unit),
658
+ round: ganzzahl(ort, 'round', p.round),
659
+ counted: wahrheitswert(ort, 'counted', p.counted),
660
+ quantity: ganzzahlOderNull(ort, 'quantity', p.quantity),
661
+ counts: ganzzahl(ort, 'counts', p.counts),
662
+ firstCountedAt: text(p.firstCountedAt),
663
+ referenceTime: text(p.referenceTime),
664
+ countedBy: zaehler.map((a, i) => {
665
+ const wer = akteur(ort, `countedBy[${i}]`, a);
666
+ if (!wer)
667
+ throw antwortfehler(ort.name, `Antwort ist unbrauchbar (data.${ort.pfad}.countedBy[${i}] ist kein Objekt)`);
668
+ return wer;
669
+ }),
670
+ recountRequested: p.recountRequested === true,
671
+ recount: nachzaehlen
672
+ ? {
673
+ reason: text(nachzaehlen.reason),
674
+ requestedAt: text(nachzaehlen.requestedAt),
675
+ requestedBy: akteur(nOrt, 'requestedBy', nachzaehlen.requestedBy),
676
+ round: ganzzahlOderNull(nOrt, 'round', nachzaehlen.round),
677
+ }
678
+ : null,
679
+ addedLater: p.addedLater === true,
680
+ };
681
+ // Die Zaehl- und Storno-Antwort sendet die Position ohne Seriennummern: dann fehlt das Feld, nie [].
682
+ if (hat(p, 'serialNumbers'))
683
+ raus.serialNumbers = textlisteOderLeer(ort, 'serialNumbers', p.serialNumbers);
684
+ // Gezaehlt heisst: es gibt eine Menge. Beides zusammen kaputt waere „0“ oder „nichts“ geraten.
685
+ if (raus.counted && raus.quantity === null)
686
+ throw antwortfehler(ort.name, `Antwort ist unbrauchbar (data.${ort.pfad}.quantity fehlt bei counted: true)`);
687
+ for (const feld of ['bookStockNow', 'expectedQuantity', 'differenceQuantity', 'differenceValueCents', 'bookedQuantity']) {
688
+ if (hat(p, feld))
689
+ raus[feld] = ganzzahlOderNull(ort, feld, p[feld]);
690
+ }
691
+ if (hat(p, 'needsCheck'))
692
+ raus.needsCheck = p.needsCheck === true;
693
+ if (hat(p, 'checkReasons'))
694
+ raus.checkReasons = textlisteOderLeer(ort, 'checkReasons', p.checkReasons);
695
+ if (hat(p, 'expectedAsOf'))
696
+ raus.expectedAsOf = text(p.expectedAsOf);
697
+ if (hat(p, 'missingSerialNumbers'))
698
+ raus.missingSerialNumbers = textlisteOderLeer(ort, 'missingSerialNumbers', p.missingSerialNumbers);
699
+ if (hat(p, 'extraSerialNumbers'))
700
+ raus.extraSerialNumbers = textlisteOderLeer(ort, 'extraSerialNumbers', p.extraSerialNumbers);
701
+ if (hat(p, 'notBooked'))
702
+ raus.notBooked = p.notBooked === null ? null : nichtGebucht(unter(ort, 'notBooked'), p.notBooked);
703
+ if (hat(p, 'inventory')) {
704
+ const inv = objektOderNull(ort, 'inventory', p.inventory);
705
+ if (!inv) {
706
+ raus.inventory = null;
707
+ }
708
+ else {
709
+ const iOrt = unter(ort, 'inventory');
710
+ raus.inventory = { quantity: ganzzahl(iOrt, 'quantity', inv.quantity), countedOn: textOderNull(inv.countedOn) };
711
+ for (const feld of ['unitValueMicros', 'valueCents']) {
712
+ if (hat(inv, feld))
713
+ raus.inventory[feld] = ganzzahlOderNull(iOrt, feld, inv[feld]);
714
+ }
715
+ }
716
+ }
717
+ return raus;
718
+ }
719
+ /** Eine Zaehlung: Menge, Runde und Kennungen muessen da sein. */
720
+ export function inventurZaehlung(ort, w) {
721
+ const z = eintrag(ort, w);
722
+ const storno = objektOderNull(ort, 'voided', z.voided);
723
+ return {
724
+ id: kennung(ort, 'id', z.id),
725
+ articleId: kennung(ort, 'articleId', z.articleId),
726
+ condition: kennung(ort, 'condition', z.condition),
727
+ quantity: ganzzahl(ort, 'quantity', z.quantity),
728
+ // Die Zaehlung traegt ihre Seriennummern immer (leer bei Mengenartikeln); fehlt die Liste, ist sie kaputt.
729
+ serialNumbers: textliste(ort, 'serialNumbers', z.serialNumbers),
730
+ round: ganzzahl(ort, 'round', z.round),
731
+ countedBy: akteur(ort, 'countedBy', z.countedBy),
732
+ source: textOderNull(z.source),
733
+ cashregisterId: textOderNull(z.cashregisterId),
734
+ countedAt: text(z.countedAt),
735
+ note: text(z.note),
736
+ voided: storno
737
+ ? { reason: text(storno.reason), voidedAt: text(storno.voidedAt), voidedBy: akteur(unter(ort, 'voided'), 'voidedBy', storno.voidedBy) }
738
+ : null,
739
+ };
740
+ }
741
+ /** Antwort von Zaehlen und Stornieren: beide Teile sind zugesagt. */
742
+ export function zaehlungMitPosition(name, daten) {
743
+ const d = objekt(daten);
744
+ return { count: inventurZaehlung({ name, pfad: 'count' }, d?.count), item: inventurPosition({ name, pfad: 'item' }, d?.item) };
745
+ }
746
+ /** Lese-Link auf ein grosses Inventurprotokoll; ohne Adresse, Ablauf, Groesse oder Pruefsumme ist er unbrauchbar. */
747
+ export function protokollLink(name, w) {
748
+ const ort = { name, pfad: 'download' };
749
+ const d = eintrag(ort, w);
750
+ return {
751
+ url: kennung(ort, 'url', d.url),
752
+ expiresAt: kennung(ort, 'expiresAt', d.expiresAt),
753
+ sizeBytes: ganzzahl(ort, 'sizeBytes', d.sizeBytes),
754
+ sha256: kennung(ort, 'sha256', d.sha256),
755
+ fileName: textOderNull(d.fileName),
756
+ contentType: textOderNull(d.contentType),
757
+ };
758
+ }
458
759
  // ---- Listen ------------------------------------------------------------------
459
760
  /** Eine zugesagte Liste `data.<feld>`, Eintrag fuer Eintrag gelesen. */
460
761
  export function liste(name, daten, feld, lesen) {