@kreiseck/kasseneck-api 1.10.1 → 1.11.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 (146) hide show
  1. package/CHANGELOG.md +241 -0
  2. package/dist/cjs/client/aufrufe.d.ts +5 -5
  3. package/dist/cjs/client/aufrufe.js +22 -2
  4. package/dist/cjs/client/errors.js +2 -0
  5. package/dist/cjs/client/receipts.d.ts +34 -1
  6. package/dist/cjs/client/receipts.js +34 -1
  7. package/dist/cjs/index.d.ts +1 -1
  8. package/dist/cjs/index.js +9 -2
  9. package/dist/cjs/inventory/vertrag.d.ts +1 -1
  10. package/dist/cjs/inventory/vertrag.js +2 -0
  11. package/dist/cjs/invoice/vertrag.d.ts +1 -1
  12. package/dist/cjs/invoice/vertrag.js +2 -0
  13. package/dist/cjs/models/cancellation.d.ts +49 -1
  14. package/dist/cjs/models/cancellation.js +139 -1
  15. package/dist/cjs/models/index.d.ts +1 -1
  16. package/dist/cjs/models/index.js +9 -2
  17. package/dist/cjs/models/payment-errors.d.ts +1 -1
  18. package/dist/cjs/models/payment-errors.js +2 -0
  19. package/dist/cjs/models/receipt-email.d.ts +1 -1
  20. package/dist/cjs/models/receipt-email.js +2 -0
  21. package/dist/cjs/models/receipt-errors.d.ts +1 -1
  22. package/dist/cjs/models/receipt-errors.js +2 -0
  23. package/dist/cjs/models/receipt-item.d.ts +10 -0
  24. package/dist/cjs/models/receipt-item.js +3 -0
  25. package/dist/cjs/pos/client.js +10 -0
  26. package/dist/cjs/pos/errors.d.ts +4 -3
  27. package/dist/cjs/pos/errors.js +21 -2
  28. package/dist/cjs/pos/index.d.ts +6 -2
  29. package/dist/cjs/pos/index.js +33 -3
  30. package/dist/cjs/pos/schicht-lesen.d.ts +39 -0
  31. package/dist/cjs/pos/schicht-lesen.js +405 -0
  32. package/dist/cjs/pos/schicht-typen.d.ts +380 -0
  33. package/dist/cjs/pos/schicht-typen.js +59 -0
  34. package/dist/cjs/pos/schicht.d.ts +208 -0
  35. package/dist/cjs/pos/schicht.js +235 -0
  36. package/dist/cjs/pos/settings.d.ts +37 -1
  37. package/dist/cjs/pos/settings.js +43 -3
  38. package/dist/cjs/pos/texte.d.ts +627 -0
  39. package/dist/cjs/pos/texte.js +236 -0
  40. package/dist/cjs/react/index.js +1 -1
  41. package/dist/cjs/receipt/grid.js +2 -2
  42. package/dist/cjs/receipt/layout.d.ts +5 -0
  43. package/dist/cjs/receipt/shift-sheet.d.ts +33 -0
  44. package/dist/cjs/receipt/shift-sheet.js +207 -0
  45. package/dist/cjs/register/errors.d.ts +1 -1
  46. package/dist/cjs/register/errors.js +2 -0
  47. package/dist/cjs/stored/draht.d.ts +1 -1
  48. package/dist/cjs/stored/draht.js +4 -3
  49. package/dist/cjs/stored/einstellungen.js +26 -3
  50. package/dist/cjs/stored/vokabular.d.ts +1 -1
  51. package/dist/cjs/stored/vokabular.js +118 -12
  52. package/dist/cjs/version.d.ts +1 -1
  53. package/dist/cjs/version.js +1 -1
  54. package/dist/esm/client/aufrufe.d.ts +5 -5
  55. package/dist/esm/client/aufrufe.js +22 -2
  56. package/dist/esm/client/errors.js +2 -0
  57. package/dist/esm/client/receipts.d.ts +34 -1
  58. package/dist/esm/client/receipts.js +35 -2
  59. package/dist/esm/index.d.ts +1 -1
  60. package/dist/esm/index.js +1 -1
  61. package/dist/esm/inventory/vertrag.d.ts +1 -1
  62. package/dist/esm/inventory/vertrag.js +2 -0
  63. package/dist/esm/invoice/vertrag.d.ts +1 -1
  64. package/dist/esm/invoice/vertrag.js +2 -0
  65. package/dist/esm/models/cancellation.d.ts +49 -1
  66. package/dist/esm/models/cancellation.js +132 -0
  67. package/dist/esm/models/index.d.ts +1 -1
  68. package/dist/esm/models/index.js +1 -1
  69. package/dist/esm/models/payment-errors.d.ts +1 -1
  70. package/dist/esm/models/payment-errors.js +2 -0
  71. package/dist/esm/models/receipt-email.d.ts +1 -1
  72. package/dist/esm/models/receipt-email.js +2 -0
  73. package/dist/esm/models/receipt-errors.d.ts +1 -1
  74. package/dist/esm/models/receipt-errors.js +2 -0
  75. package/dist/esm/models/receipt-item.d.ts +10 -0
  76. package/dist/esm/models/receipt-item.js +3 -0
  77. package/dist/esm/pos/client.js +11 -1
  78. package/dist/esm/pos/errors.d.ts +4 -3
  79. package/dist/esm/pos/errors.js +21 -2
  80. package/dist/esm/pos/index.d.ts +6 -2
  81. package/dist/esm/pos/index.js +7 -2
  82. package/dist/esm/pos/schicht-lesen.d.ts +39 -0
  83. package/dist/esm/pos/schicht-lesen.js +392 -0
  84. package/dist/esm/pos/schicht-typen.d.ts +380 -0
  85. package/dist/esm/pos/schicht-typen.js +56 -0
  86. package/dist/esm/pos/schicht.d.ts +208 -0
  87. package/dist/esm/pos/schicht.js +222 -0
  88. package/dist/esm/pos/settings.d.ts +37 -1
  89. package/dist/esm/pos/settings.js +40 -2
  90. package/dist/esm/pos/texte.d.ts +627 -0
  91. package/dist/esm/pos/texte.js +236 -0
  92. package/dist/esm/react/index.js +1 -1
  93. package/dist/esm/receipt/grid.js +2 -2
  94. package/dist/esm/receipt/layout.d.ts +5 -0
  95. package/dist/esm/receipt/shift-sheet.d.ts +33 -0
  96. package/dist/esm/receipt/shift-sheet.js +203 -0
  97. package/dist/esm/register/errors.d.ts +1 -1
  98. package/dist/esm/register/errors.js +2 -0
  99. package/dist/esm/stored/draht.d.ts +1 -1
  100. package/dist/esm/stored/draht.js +4 -3
  101. package/dist/esm/stored/einstellungen.js +27 -4
  102. package/dist/esm/stored/vokabular.d.ts +1 -1
  103. package/dist/esm/stored/vokabular.js +118 -12
  104. package/dist/esm/version.d.ts +1 -1
  105. package/dist/esm/version.js +1 -1
  106. package/fixtures/cancellation-tip-cases.json +679 -0
  107. package/fixtures/expected/cash-movement.grid32.txt +15 -0
  108. package/fixtures/expected/cash-movement.grid48.txt +15 -0
  109. package/fixtures/expected/cash-movement.lines.json +171 -0
  110. package/fixtures/expected/shift-close-blind.grid32.txt +28 -0
  111. package/fixtures/expected/shift-close-blind.grid48.txt +27 -0
  112. package/fixtures/expected/shift-close-blind.lines.json +275 -0
  113. package/fixtures/expected/shift-close-put-in.grid32.txt +38 -0
  114. package/fixtures/expected/shift-close-put-in.grid48.txt +35 -0
  115. package/fixtures/expected/shift-close-put-in.lines.json +403 -0
  116. package/fixtures/expected/shift-close-take-out.grid32.txt +38 -0
  117. package/fixtures/expected/shift-close-take-out.grid48.txt +35 -0
  118. package/fixtures/expected/shift-close-take-out.lines.json +403 -0
  119. package/fixtures/expected/shift-close.grid32.txt +37 -0
  120. package/fixtures/expected/shift-close.grid48.txt +35 -0
  121. package/fixtures/expected/shift-close.lines.json +413 -0
  122. package/fixtures/expected/shift-interim.grid32.txt +28 -0
  123. package/fixtures/expected/shift-interim.grid48.txt +27 -0
  124. package/fixtures/expected/shift-interim.lines.json +293 -0
  125. package/fixtures/hobex-hps-codes.json +1 -1
  126. package/fixtures/invoice-api.schema.json +1 -1
  127. package/fixtures/invoice-texts.json +1 -1
  128. package/fixtures/pos-message-cases.json +45 -0
  129. package/fixtures/pos-texts.json +723 -1
  130. package/fixtures/receipt-due-generated.json +1 -1
  131. package/fixtures/shift-cases.json +87 -0
  132. package/fixtures/shift-receipts.json +41 -0
  133. package/fixtures/surface.json +119 -3
  134. package/fixtures/v3/antworten/belege.json +1 -1
  135. package/fixtures/v3/antworten/belegmail.json +1 -1
  136. package/fixtures/v3/antworten/kasse-belege.json +1 -1
  137. package/fixtures/v3/antworten/kasse.json +10874 -4865
  138. package/fixtures/v3/antworten/lager.json +1 -1
  139. package/fixtures/v3/antworten/rechnungen.json +1 -1
  140. package/fixtures/v3/antworten/storno.json +1 -1
  141. package/fixtures/v3/stored/belege.json +18 -6
  142. package/fixtures/v3/stored/kasse.json +4667 -183
  143. package/fixtures/v3/stored/rechnungen.json +1 -1
  144. package/fixtures/v3/v3-vokabular.json +739 -32
  145. package/fixtures/v3/zahlbetrag-faelle.json +1 -1
  146. package/package.json +1 -1
@@ -0,0 +1,222 @@
1
+ import { KasseneckValidationError } from '../client/errors.js';
2
+ import { anfrage, schluessel } from '../inventory/schreiben.js';
3
+ import { abschluss, bericht, gebucht, geoeffnet, gemeldet, ladeStand } from './schicht-lesen.js';
4
+ import { labelText } from './texte.js';
5
+ import { CASH_LOCATIONS, EUR_DENOMINATIONS, } from './schicht-typen.js';
6
+ const antwort = (name) => ({ name, pfad: '', scope: 'response' });
7
+ function anfragefehler(name, grund) {
8
+ return new KasseneckValidationError(name, grund, 'request');
9
+ }
10
+ /** Eine Kennung, die gesendet werden muss. */
11
+ function kennung(name, p, feld) {
12
+ const w = p[feld];
13
+ if (typeof w !== 'string' || w.trim() === '')
14
+ throw anfragefehler(name, `${feld} fehlt`);
15
+ }
16
+ /** Ein Betrag in Cent: ganze Zahl, sonst wirft der Aufruf vor dem Senden. */
17
+ function cent(name, feld, w) {
18
+ if (typeof w !== 'number' || !Number.isSafeInteger(w))
19
+ throw anfragefehler(name, `${feld} muss eine ganze Zahl (Cent) sein`);
20
+ }
21
+ function stueckelungPruefen(name, p) {
22
+ const w = p.denominations;
23
+ if (w === undefined || w === null)
24
+ return;
25
+ if (typeof w !== 'object' || Array.isArray(w))
26
+ throw anfragefehler(name, 'denominations muss eine Karte Wert -> Anzahl sein');
27
+ for (const [k, anzahl] of Object.entries(w))
28
+ cent(name, `denominations.${k}`, anzahl);
29
+ }
30
+ async function rufe(transport, name, p, lese) {
31
+ return lese(antwort(name), await transport(name, p));
32
+ }
33
+ /** Stand der Lade: Rechte, offene Schicht, Trinkgeld-Ansprueche und (mit `cashView`) Soll und letzte Oeffnungen. */
34
+ export async function getMyDrawerState(transport, options = {}) {
35
+ const name = 'getMyDrawerState';
36
+ const p = anfrage(name, options);
37
+ if (p.drawerId === '')
38
+ delete p.drawerId;
39
+ return rufe(transport, name, p, ladeStand);
40
+ }
41
+ /** Schicht oeffnen: gezaehltes Startgeld oder den Bestand uebernehmen (Recht `shift`). */
42
+ export async function openMyShift(transport, options) {
43
+ const name = 'openMyShift';
44
+ const p = anfrage(name, options);
45
+ kennung(name, p, 'drawerId');
46
+ schluessel(name, p, { pflicht: true });
47
+ if (p.openingFloatCents !== undefined)
48
+ cent(name, 'openingFloatCents', p.openingFloatCents);
49
+ stueckelungPruefen(name, p);
50
+ return rufe(transport, name, p, geoeffnet);
51
+ }
52
+ /** Ein- oder Auszahlung buchen (Recht `cashMove`); Antwort mit den Bon-Daten „Kassenbewegung“. */
53
+ export async function recordMyCashMovement(transport, options) {
54
+ const name = 'recordMyCashMovement';
55
+ const p = anfrage(name, options);
56
+ kennung(name, p, 'drawerId');
57
+ schluessel(name, p, { pflicht: true });
58
+ cent(name, 'amountCents', p.amountCents);
59
+ // Nur pruefen, wenn gesetzt: Bank/Tresor fragt die Kasse nicht mehr ab.
60
+ if (p.location !== undefined && !CASH_LOCATIONS.includes(p.location)) {
61
+ throw anfragefehler(name, `location muss ${CASH_LOCATIONS.join(' oder ')} sein`);
62
+ }
63
+ return rufe(transport, name, p, gebucht);
64
+ }
65
+ /** Oeffnungen der Lade melden (auch nachgesendet); doppelt gemeldete zaehlen einmal. */
66
+ export async function logMyDrawerOpenings(transport, options) {
67
+ const name = 'logMyDrawerOpenings';
68
+ const p = anfrage(name, options);
69
+ kennung(name, p, 'drawerId');
70
+ const eintraege = p.entries;
71
+ if (!Array.isArray(eintraege) || eintraege.length === 0)
72
+ throw anfragefehler(name, 'entries muss 1–50 Eintraege haben');
73
+ eintraege.forEach((e, i) => {
74
+ const k = e !== null && typeof e === 'object' ? e.idempotencyKey : undefined;
75
+ if (typeof k !== 'string' || k.trim() === '')
76
+ throw anfragefehler(name, `entries[${i}].idempotencyKey fehlt`);
77
+ });
78
+ return rufe(transport, name, p, gemeldet);
79
+ }
80
+ /** Bericht der Schicht: Schicht, Bewegungen und Bon-Daten (X-Bon, bei geschlossener Schicht der Z-Bon). */
81
+ export async function getMyShiftReport(transport, options) {
82
+ const name = 'getMyShiftReport';
83
+ const p = anfrage(name, options);
84
+ kennung(name, p, 'shiftId');
85
+ return rufe(transport, name, p, bericht);
86
+ }
87
+ /**
88
+ * Schicht schliessen (Recht `shift`). `status: 'recount'` heisst: bitte noch
89
+ * einmal zaehlen und erneut senden (kein Fehler); nach dem zweiten Zaehlen
90
+ * schliesst der Server, bei grosser Abweichung nur mit `comment`. Mit
91
+ * `checkOnly` nur pruefen: `status: 'ready'` statt Schliessen.
92
+ */
93
+ export async function closeMyShift(transport, options) {
94
+ const name = 'closeMyShift';
95
+ const p = anfrage(name, options);
96
+ kennung(name, p, 'shiftId');
97
+ schluessel(name, p, { pflicht: true });
98
+ cent(name, 'countedCents', p.countedCents);
99
+ stueckelungPruefen(name, p);
100
+ if (p.tipPayouts !== undefined && p.tipPayouts !== null) {
101
+ if (!Array.isArray(p.tipPayouts))
102
+ throw anfragefehler(name, 'tipPayouts muss eine Liste sein');
103
+ p.tipPayouts.forEach((a, i) => cent(name, `tipPayouts[${i}].cents`, a !== null && typeof a === 'object' ? a.cents : undefined));
104
+ }
105
+ if (p.checkOnly !== undefined && typeof p.checkOnly !== 'boolean')
106
+ throw anfragefehler(name, 'checkOnly muss true oder false sein');
107
+ // Nur die Form: die Art (Katalog) und die Grenzen prueft der Server.
108
+ if (p.afterCount !== undefined && p.afterCount !== null) {
109
+ if (typeof p.afterCount !== 'object' || Array.isArray(p.afterCount))
110
+ throw anfragefehler(name, 'afterCount muss ein Objekt { type, amountCents } sein');
111
+ cent(name, 'afterCount.amountCents', p.afterCount.amountCents);
112
+ }
113
+ return rufe(transport, name, p, abschluss);
114
+ }
115
+ // ---- Helfer der Kasse ---------------------------------------------------------
116
+ const ANZAHL_MAX = 100000;
117
+ const WERTE = new Set(EUR_DENOMINATIONS.map(String));
118
+ /**
119
+ * Summe einer Stueckelung in Cent, oder `null`, wenn sie keine ist: ein
120
+ * Schluessel, der kein Euro-Wert ist ([EUR_DENOMINATIONS], genau so
121
+ * geschrieben, `"500"`, nicht `"0500"`), eine Anzahl, die keine ganze Zahl
122
+ * von 0 bis 100 000 ist. Dieselbe Regel wie am Server
123
+ * (`invalid_denominations`). Gemeinsame Faelle mit dem Dart-Zwilling:
124
+ * `fixtures/shift-cases.json`.
125
+ */
126
+ export function sumDenominations(denominations) {
127
+ if (denominations === null || typeof denominations !== 'object' || Array.isArray(denominations))
128
+ return null;
129
+ let summe = 0;
130
+ for (const [wert, anzahl] of Object.entries(denominations)) {
131
+ if (!WERTE.has(wert))
132
+ return null;
133
+ if (typeof anzahl !== 'number' || !Number.isSafeInteger(anzahl) || anzahl < 0 || anzahl > ANZAHL_MAX)
134
+ return null;
135
+ summe += Number(wert) * anzahl;
136
+ }
137
+ return summe;
138
+ }
139
+ /**
140
+ * Ein eingetippter Betrag in Cent, **ohne Gleitkomma**: `'12,5'` → `1250`,
141
+ * `'0'` → `0` (anders als beim Kassieren ist 0 hier gueltig: eine leere Lade,
142
+ * kein Startgeld). Dezimaltrenner Komma oder Punkt mit einer oder zwei
143
+ * Nachkommastellen; `null` fuer leer, Vorzeichen, Tausendertrenner
144
+ * (`'1.000'`, `'1 000'`), drei Nachkommastellen, Euro-Zeichen und alles
145
+ * ueber einer sicheren Ganzzahl. Gemeinsame Faelle mit dem Dart-Zwilling:
146
+ * `fixtures/shift-cases.json`.
147
+ */
148
+ export function parseCentsAllowZero(text) {
149
+ if (typeof text !== 'string')
150
+ return null;
151
+ const m = /^(\d+)(?:[.,](\d{1,2}))?$/.exec(text.trim());
152
+ if (!m)
153
+ return null;
154
+ const cents = BigInt(m[1]) * 100n + BigInt((m[2] ?? '').padEnd(2, '0'));
155
+ return cents <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(cents) : null;
156
+ }
157
+ /**
158
+ * Ein frischer Idempotenzschluessel fuer einen neuen Vorgang: `kasse-` und
159
+ * 32 Hex-Zeichen aus dem sicheren Zufall (`crypto.getRandomValues`), dieselbe
160
+ * Form wie beim Zaehlen der Inventur in der Kassen-App. Fuer „Erneut senden“
161
+ * nie einen neuen ziehen, sondern den alten behalten.
162
+ */
163
+ export function newIdempotencyKey() {
164
+ const zufall = globalThis.crypto;
165
+ if (!zufall || typeof zufall.getRandomValues !== 'function') {
166
+ throw new KasseneckValidationError('newIdempotencyKey', 'kein sicherer Zufall vorhanden (crypto.getRandomValues)', 'request');
167
+ }
168
+ const bytes = zufall.getRandomValues(new Uint8Array(16));
169
+ return `kasse-${Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('')}`;
170
+ }
171
+ /**
172
+ * Was vom Trinkgeld eines Mitarbeiters dasteht – auf dem Bon, im Kassenstand
173
+ * und beim Abschluss gleich (Rulings R50/R51): „verdient“ (sofort + über
174
+ * Lohn) immer, „über Lohn“, „schon genommen“, „selbst zurückgegeben“ (R62)
175
+ * und „offen“ nur, wenn nicht 0. Davor „aus früheren Schichten“ (R64, nur ≠ 0):
176
+ * offen = aus früheren Schichten + verdient − über Lohn − schon genommen +
177
+ * selbst zurückgegeben.
178
+ * Ist alles 0, gibt es keine Zeile, und der Mitarbeiter entfaellt ganz. Ohne
179
+ * `openCents` (Laufstand) zaehlt offen als 0. Gemeinsame Faelle mit dem
180
+ * Dart-Zwilling: `fixtures/shift-cases.json`.
181
+ */
182
+ export function shiftTipLines(tip) {
183
+ const verdient = tip.receivedImmediatelyCents + tip.retainedCents;
184
+ const vortrag = tip.carriedOverCents ?? 0;
185
+ const zeilen = [
186
+ ['shift_receipt.tip_retained', tip.retainedCents],
187
+ ['shift_receipt.tip_paid_out', tip.paidOutCents],
188
+ ['shift_receipt.tip_refunded_by_employee', tip.refundedByEmployeeCents ?? 0],
189
+ ['shift_receipt.tip_open', tip.openCents ?? 0],
190
+ ];
191
+ const rest = zeilen.filter(([, cents]) => cents !== 0);
192
+ if (verdient === 0 && rest.length === 0 && vortrag === 0)
193
+ return [];
194
+ const davor = vortrag !== 0 ? [['shift_receipt.tip_carried_over', vortrag]] : [];
195
+ return [...davor, ['shift_receipt.tip_earned', verdient], ...rest].map(([key, cents]) => ({ key, text: labelText(key), cents }));
196
+ }
197
+ /**
198
+ * Dauer für Menschen aus ganzen Minuten: unter 1 Std. „45 Min.“, unter 1 Tag
199
+ * „8 Std. 32 Min.“ (bei 0 Minuten nur „8 Std.“), ab 1 Tag „1 Tag 3 Std.“ bzw.
200
+ * „2 Tage“ (bei 0 Stunden ohne Stunden). Negatives und Unsinn zählt als 0;
201
+ * Bruchteile werden abgeschnitten. Gemeinsame Fälle mit dem Dart-Zwilling:
202
+ * `fixtures/shift-cases.json` (`durationText`).
203
+ */
204
+ export function durationText(minutes) {
205
+ const gesamt = Number.isFinite(minutes) && minutes > 0 ? Math.floor(minutes) : 0;
206
+ const tage = Math.floor(gesamt / 1440);
207
+ const stunden = Math.floor((gesamt % 1440) / 60);
208
+ const rest = gesamt % 60;
209
+ const teile = [];
210
+ if (tage > 0) {
211
+ teile.push(labelText(tage === 1 ? 'duration.days_one' : 'duration.days_other', { days: tage }));
212
+ if (stunden > 0)
213
+ teile.push(labelText('duration.hours', { hours: stunden }));
214
+ }
215
+ else {
216
+ if (stunden > 0)
217
+ teile.push(labelText('duration.hours', { hours: stunden }));
218
+ if (rest > 0 || stunden === 0)
219
+ teile.push(labelText('duration.minutes', { minutes: rest }));
220
+ }
221
+ return teile.join(' ');
222
+ }
@@ -189,6 +189,13 @@ export interface PosBusinessSettings {
189
189
  glass: boolean;
190
190
  /** Hilfetexte in den Chef-Einstellungen anzeigen. */
191
191
  hints: boolean;
192
+ /**
193
+ * Bar-Trinkgeld behaelt der Mitarbeiter gleich: Vorbelegung des Schalters
194
+ * „gleich behalten“ beim Kassieren (je Verkauf aenderbar). Optional: der
195
+ * Server sendet das Feld nur, wenn es gespeichert ist; fehlt es, gilt aus.
196
+ * Lesen ueber [posTipKeptByDefault]. Siehe [POS_BUSINESS_OPTIONAL_FIELDS].
197
+ */
198
+ tipKeptByDefault?: boolean;
192
199
  }
193
200
  /** Aktionen der Kasse, die eine Taste bekommen koennen (Schluessel von `device.shortcuts`). */
194
201
  export declare const POS_SHORTCUT_ACTIONS: readonly ["checkout", "complete", "cancel", "customAmount", "cash", "card", "exactAmount", "receipts", "undoLast", "settings", "logout", "tip", "fullscreen", "clearTendered", "clearCart", "splitPayment"];
@@ -241,6 +248,13 @@ export interface PosDeviceSettings {
241
248
  qrMode: PosOpen<PosQrMode>;
242
249
  drawerEnabled: boolean;
243
250
  drawerAutoOpen: PosOpen<PosDrawerAutoOpen>;
251
+ /**
252
+ * Lade dieses Geraets (Schichtbetrieb). Optional: der Server sendet das
253
+ * Feld nur, wenn eine Lade zugeordnet ist; fehlt es, gilt die Hauptlade der
254
+ * Kasse. Beim Schreiben entfernt `null` die Zuordnung. Siehe
255
+ * [POS_DEVICE_OPTIONAL_FIELDS].
256
+ */
257
+ drawerId?: string | null;
244
258
  terminalIp: string;
245
259
  terminalPort: number;
246
260
  terminalVia: PosOpen<PosTerminalVia>;
@@ -257,6 +271,25 @@ export interface PosSettings {
257
271
  }
258
272
  export declare const POS_BUSINESS_DEFAULTS: Readonly<PosBusinessSettings>;
259
273
  export declare const POS_DEVICE_DEFAULTS: Readonly<PosDeviceSettings>;
274
+ /**
275
+ * Optionale Geraete-Einstellungen: ohne Standardwert, der Server sendet sie
276
+ * nur, wenn sie gesetzt sind (Backend `GERAET_OPTIONAL`). Sie fehlen darum in
277
+ * [POS_DEVICE_DEFAULTS]; [mergePosSettings] uebernimmt sie, wenn sie als
278
+ * Kennung kommen, und der Schreibweg nimmt sie als Kennung oder `null`
279
+ * (Zuordnung entfernen).
280
+ */
281
+ export declare const POS_DEVICE_OPTIONAL_FIELDS: readonly ["drawerId"];
282
+ /** Gilt dieser Wert fuer ein optionales Feld? Lesen: nur eine Kennung; Schreiben: auch `null`. */
283
+ export declare function _optionalerWert(wert: unknown, schreiben: boolean): boolean;
284
+ /**
285
+ * Optionale Betriebs-Einstellungen (Schalter): ohne Standardwert, der Server
286
+ * sendet sie nur, wenn sie gespeichert sind (Backend `BETRIEB_OPTIONAL`).
287
+ * [mergePosSettings] uebernimmt sie nur als Wahrheitswert; der Schreibweg
288
+ * nimmt nur `true`/`false`.
289
+ */
290
+ export declare const POS_BUSINESS_OPTIONAL_FIELDS: readonly ["tipKeptByDefault"];
291
+ /** Bar-Trinkgeld „gleich behalten“ vorbelegt? Fehlt die Einstellung, gilt aus. */
292
+ export declare function posTipKeptByDefault(business: Readonly<Partial<PosBusinessSettings>> | null | undefined): boolean;
260
293
  /**
261
294
  * Die Wertemenge je Feld, soweit das Feld eine hat: dagegen prueft der
262
295
  * Schreibweg vor dem Senden (ein deutscher Wert wie `'nacht'` geht so gar
@@ -275,7 +308,10 @@ export declare const _ALTFORM_0X: Readonly<{
275
308
  /**
276
309
  * Standard + stored. Landkarten (`vatRates`, `tipSteps`, `shortcuts`)
277
310
  * werden je Schluessel gemischt, damit neue Saetze beim Altbestand ankommen;
278
- * Schluessel, die der Standard nicht fuehrt, bleiben draussen.
311
+ * Schluessel, die der Standard nicht fuehrt, bleiben draussen, ausser einer
312
+ * optionalen Geraete-Einstellung ([POS_DEVICE_OPTIONAL_FIELDS]) mit gueltiger
313
+ * Kennung und einem optionalen Betriebs-Schalter
314
+ * ([POS_BUSINESS_OPTIONAL_FIELDS]) als Wahrheitswert.
279
315
  *
280
316
  * **Immer** (unabhaengig davon, welches Standard-Objekt uebergeben wird, auch
281
317
  * eine Kopie oder das einer zweiten Paketkopie) faellt ein Wert der inneren
@@ -173,6 +173,33 @@ export const POS_DEVICE_DEFAULTS = Object.freeze({
173
173
  terminalIp: '', terminalPort: 8080, terminalTid: '', terminalVia: 'direct',
174
174
  terminalType: 'none', shortcutHints: true,
175
175
  });
176
+ /**
177
+ * Optionale Geraete-Einstellungen: ohne Standardwert, der Server sendet sie
178
+ * nur, wenn sie gesetzt sind (Backend `GERAET_OPTIONAL`). Sie fehlen darum in
179
+ * [POS_DEVICE_DEFAULTS]; [mergePosSettings] uebernimmt sie, wenn sie als
180
+ * Kennung kommen, und der Schreibweg nimmt sie als Kennung oder `null`
181
+ * (Zuordnung entfernen).
182
+ */
183
+ export const POS_DEVICE_OPTIONAL_FIELDS = Object.freeze(['drawerId']);
184
+ /** Form einer optionalen Kennung (Backend `ladeKennung`). */
185
+ const OPTIONALE_KENNUNG = /^[A-Za-z0-9_-]{1,64}$/;
186
+ /** Gilt dieser Wert fuer ein optionales Feld? Lesen: nur eine Kennung; Schreiben: auch `null`. */
187
+ export function _optionalerWert(wert, schreiben) {
188
+ return (schreiben && wert === null) || (typeof wert === 'string' && OPTIONALE_KENNUNG.test(wert));
189
+ }
190
+ const OPTIONALE = new Set(POS_DEVICE_OPTIONAL_FIELDS);
191
+ /**
192
+ * Optionale Betriebs-Einstellungen (Schalter): ohne Standardwert, der Server
193
+ * sendet sie nur, wenn sie gespeichert sind (Backend `BETRIEB_OPTIONAL`).
194
+ * [mergePosSettings] uebernimmt sie nur als Wahrheitswert; der Schreibweg
195
+ * nimmt nur `true`/`false`.
196
+ */
197
+ export const POS_BUSINESS_OPTIONAL_FIELDS = Object.freeze(['tipKeptByDefault']);
198
+ const OPTIONALE_SCHALTER = new Set(POS_BUSINESS_OPTIONAL_FIELDS);
199
+ /** Bar-Trinkgeld „gleich behalten“ vorbelegt? Fehlt die Einstellung, gilt aus. */
200
+ export function posTipKeptByDefault(business) {
201
+ return !!business && business.tipKeptByDefault === true;
202
+ }
176
203
  /**
177
204
  * Die Wertemenge je Feld, soweit das Feld eine hat: dagegen prueft der
178
205
  * Schreibweg vor dem Senden (ein deutscher Wert wie `'nacht'` geht so gar
@@ -234,7 +261,10 @@ export const _ALTFORM_0X = Object.freeze({ werte: ALTWERTE_0X, aktionen: [...ALT
234
261
  /**
235
262
  * Standard + stored. Landkarten (`vatRates`, `tipSteps`, `shortcuts`)
236
263
  * werden je Schluessel gemischt, damit neue Saetze beim Altbestand ankommen;
237
- * Schluessel, die der Standard nicht fuehrt, bleiben draussen.
264
+ * Schluessel, die der Standard nicht fuehrt, bleiben draussen, ausser einer
265
+ * optionalen Geraete-Einstellung ([POS_DEVICE_OPTIONAL_FIELDS]) mit gueltiger
266
+ * Kennung und einem optionalen Betriebs-Schalter
267
+ * ([POS_BUSINESS_OPTIONAL_FIELDS]) als Wahrheitswert.
238
268
  *
239
269
  * **Immer** (unabhaengig davon, welches Standard-Objekt uebergeben wird, auch
240
270
  * eine Kopie oder das einer zweiten Paketkopie) faellt ein Wert der inneren
@@ -247,8 +277,16 @@ export function mergePosSettings(standard, stored) {
247
277
  const out = JSON.parse(JSON.stringify(standard));
248
278
  if (stored && typeof stored === 'object') {
249
279
  for (const key of Object.keys(stored)) {
250
- if (!eigen(out, key))
280
+ if (!eigen(out, key)) {
281
+ // Optionale Geraete-Einstellung (ohne Standardwert): nur als Kennung;
282
+ // optionaler Betriebs-Schalter: nur als Wahrheitswert.
283
+ const wert = stored[key];
284
+ if (OPTIONALE.has(key) && _optionalerWert(wert, false))
285
+ out[key] = wert;
286
+ if (OPTIONALE_SCHALTER.has(key) && typeof wert === 'boolean')
287
+ out[key] = wert;
251
288
  continue;
289
+ }
252
290
  const wert = stored[key];
253
291
  const alt = out[key];
254
292
  if (alt && typeof alt === 'object' && !Array.isArray(alt) && wert && typeof wert === 'object' && !Array.isArray(wert)) {