@kreiseck/kasseneck-api 1.0.0-rc.4 → 1.0.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 (53) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/README.md +40 -4
  3. package/dist/cjs/index.d.ts +1 -1
  4. package/dist/cjs/index.js +4 -1
  5. package/dist/cjs/pos/index.d.ts +1 -1
  6. package/dist/cjs/pos/index.js +6 -1
  7. package/dist/cjs/pos/texte.d.ts +95 -2
  8. package/dist/cjs/pos/texte.js +111 -3
  9. package/dist/cjs/receipt/due.d.ts +33 -0
  10. package/dist/cjs/receipt/due.js +61 -15
  11. package/dist/cjs/receipt/index.d.ts +1 -1
  12. package/dist/cjs/receipt/index.js +4 -1
  13. package/dist/cjs/stored/index.d.ts +1 -0
  14. package/dist/cjs/stored/index.js +7 -0
  15. package/dist/cjs/stored/layout.d.ts +55 -0
  16. package/dist/cjs/stored/layout.js +119 -0
  17. package/dist/cjs/version.d.ts +1 -1
  18. package/dist/cjs/version.js +1 -1
  19. package/dist/esm/index.d.ts +1 -1
  20. package/dist/esm/index.js +1 -1
  21. package/dist/esm/pos/index.d.ts +1 -1
  22. package/dist/esm/pos/index.js +1 -1
  23. package/dist/esm/pos/texte.d.ts +95 -2
  24. package/dist/esm/pos/texte.js +108 -2
  25. package/dist/esm/receipt/due.d.ts +33 -0
  26. package/dist/esm/receipt/due.js +58 -15
  27. package/dist/esm/receipt/index.d.ts +1 -1
  28. package/dist/esm/receipt/index.js +1 -1
  29. package/dist/esm/stored/index.d.ts +1 -0
  30. package/dist/esm/stored/index.js +2 -0
  31. package/dist/esm/stored/layout.d.ts +55 -0
  32. package/dist/esm/stored/layout.js +113 -0
  33. package/dist/esm/version.d.ts +1 -1
  34. package/dist/esm/version.js +1 -1
  35. package/fixtures/hobex-hps-codes.json +1 -1
  36. package/fixtures/invoice-api.schema.json +1 -1
  37. package/fixtures/invoice-texts.json +1 -1
  38. package/fixtures/pos-message-cases.json +31 -1
  39. package/fixtures/pos-texts.json +68 -1
  40. package/fixtures/receipt-due-errors.json +56 -0
  41. package/fixtures/surface.json +11 -1
  42. package/fixtures/v3/antworten/belege.json +1 -1
  43. package/fixtures/v3/antworten/belegmail.json +1 -1
  44. package/fixtures/v3/antworten/kasse-belege.json +1 -1
  45. package/fixtures/v3/antworten/kasse.json +2 -2
  46. package/fixtures/v3/antworten/rechnungen.json +1 -1
  47. package/fixtures/v3/antworten/storno.json +1 -1
  48. package/fixtures/v3/stored/belege.json +1 -1
  49. package/fixtures/v3/stored/kasse.json +2 -2
  50. package/fixtures/v3/stored/rechnungen.json +1 -1
  51. package/fixtures/v3/v3-vokabular.json +1 -1
  52. package/fixtures/v3/zahlbetrag-faelle.json +1 -1
  53. package/package.json +1 -1
@@ -20,11 +20,24 @@
20
20
  * `only`), die Platzhalter und die Arten der Fehlerregeln (Abschnitte
21
21
  * `placeholders` und `structure`).
22
22
  */
23
+ import { isOutcomeUnknown, KasseneckApiError, KasseneckHttpError, KasseneckNetworkError } from '../client/errors.js';
23
24
  const MELDUNGEN_ROH = {
24
25
  // --- Transport -----------------------------------------------------------
25
26
  'network.no_connection': { text: 'Keine Verbindung zum Server. Bitte die Internetverbindung prüfen und erneut versuchen.' },
26
27
  'network.timeout': { text: 'Der Server antwortet nicht. Bitte die Internetverbindung prüfen und erneut versuchen.' },
28
+ // Frist oder Netzfehler auf einem Aufruf mit Wirkung (ERROR_OUTCOME_RULES):
29
+ // der Vorgang kann trotzdem gebucht sein, darum nie „erneut versuchen“.
30
+ 'network.outcome_unknown': { text: 'Der Server hat nicht geantwortet, der Vorgang kann trotzdem gebucht sein. Bitte vor einem neuen Versuch prüfen, ob der letzte Vorgang schon gebucht ist.' },
27
31
  'server.unexpected': { text: 'Der Server hat unerwartet geantwortet (HTTP {status}). Bitte den Support verständigen.', placeholders: ['status'] },
32
+ // Rand-Codes, die ein Kassier nicht deuten kann (siehe ERROR_CODE_RULES): der
33
+ // technische Satz bleibt in `error.message`, auf den Schirm kommt dieser.
34
+ // Nur fuer Codes, bei denen der Server nichts ausgefuehrt hat: dann darf
35
+ // der Satz zum neuen Versuch raten.
36
+ 'server.connection_disturbed': { text: 'Die Verbindung zum Kassenserver ist gestört. Bitte kurz warten und erneut versuchen.' },
37
+ // Codes mit unklarem Ausgang (der Vorgang kann gebucht sein): nie zum
38
+ // Wiederholen raten, erst nachsehen. „Kasse neu öffnen“ passt fuer beide
39
+ // Seiten (Web: neu laden, App: neu starten).
40
+ 'server.response_unreadable': { text: 'Die Antwort des Kassenservers war nicht lesbar. Bitte die Kasse neu öffnen und vor einem neuen Versuch prüfen, ob der letzte Vorgang schon gebucht ist.' },
28
41
  // --- Kopplung ------------------------------------------------------------
29
42
  'pairing.code_missing': { text: 'Bitte den Kopplungs-Code eingeben.' },
30
43
  'pairing.failed': { text: 'Die Kopplung ist fehlgeschlagen. Bitte erneut versuchen.' },
@@ -263,8 +276,8 @@ const MELDUNGEN_ROH = {
263
276
  export const MESSAGES = MELDUNGEN_ROH;
264
277
  /**
265
278
  * In welcher Reihenfolge ein Fehler eingeordnet wird, auf beiden Seiten
266
- * dieselbe. Jede Regel nennt ihre Art (`kind`) und entweder ein Verhalten
267
- * (`behavior`) oder den Schluessel des Satzes (`key`). Die Arten:
279
+ * dieselbe. Genau eine Regel je Art (`kind`), mit einem Verhalten
280
+ * (`behavior`) oder dem Schluessel des Satzes (`key`). Die Arten:
268
281
  * api - HTTP 200, `status:'error'`: der Satz des Backends, woertlich
269
282
  * (`server_text`)
270
283
  * plain_text - schon fuer den Bildschirm geschrieben: sein eigener Text
@@ -277,6 +290,11 @@ export const MESSAGES = MELDUNGEN_ROH;
277
290
  * (`fallback`)
278
291
  * Unter 0.x hiessen sie `klartext`, `zeitablauf`, `netz`, `unerwartet`,
279
292
  * `sonst` (Tabelle in `fixtures/renames-1.0.json`).
293
+ *
294
+ * Seit 1.0.0-rc.5 verfeinern [ERROR_CODE_RULES] und [ERROR_OUTCOME_RULES]
295
+ * diese Liste; sie stehen bewusst getrennt, damit ein Leser, der nur nach
296
+ * `kind` sucht, weiter genau den Satz von rc.4 zeigt. [findErrorRule] wendet
297
+ * alle drei in der richtigen Reihenfolge an.
280
298
  */
281
299
  export const ERROR_RULES = [
282
300
  { kind: 'api', behavior: 'server_text' },
@@ -286,6 +304,87 @@ export const ERROR_RULES = [
286
304
  { kind: 'unexpected', key: 'server.unexpected' },
287
305
  { kind: 'other', behavior: 'fallback' },
288
306
  ];
307
+ /**
308
+ * Regeln je Code (`error.code`), vor der Regel der Art: sie ersetzen den
309
+ * technischen Satz des Pakets bzw. des Rands durch einen Menschentext. Der
310
+ * technische Satz bleibt in `error.message` fuers Protokoll.
311
+ * - `dialect_mismatch`, `response_translation_failed`, `response_unreadable`:
312
+ * Ausgang unklar, der Vorgang kann gebucht sein; der Satz raet nie zum
313
+ * Wiederholen, sondern zum Nachsehen.
314
+ * - `route_missing`, `not_found`, `internal_translation_error`: am Server
315
+ * geschah nichts, ein neuer Versuch ist sicher (auch auf den Geldwegen:
316
+ * alle drei stehen in `PAYMENT_CALL_REJECTED_CODES`).
317
+ */
318
+ export const ERROR_CODE_RULES = [
319
+ { kind: 'api', codes: ['dialect_mismatch', 'response_translation_failed', 'response_unreadable'], key: 'server.response_unreadable' },
320
+ { kind: 'api', codes: ['route_missing', 'not_found', 'internal_translation_error'], key: 'server.connection_disturbed' },
321
+ ];
322
+ /**
323
+ * Regeln je Ausgang, nach den Code-Regeln und vor der Regel der Art: eine
324
+ * Frist oder ein Netzfehler auf einem Aufruf mit Wirkung (Ausgang unklar,
325
+ * siehe [messageOutcome]) raet nie zum Wiederholen. Ein Netzfehler auf einem
326
+ * Aufruf ohne Wirkung behaelt den Satz von rc.4 („erneut versuchen“).
327
+ */
328
+ export const ERROR_OUTCOME_RULES = [
329
+ { kind: 'timeout', outcome: 'unknown', key: 'network.outcome_unknown' },
330
+ { kind: 'network', outcome: 'unknown', key: 'network.outcome_unknown' },
331
+ ];
332
+ /**
333
+ * Die Regel fuer einen Fehler: zuerst eine Code-Regel mit passender Art und
334
+ * passendem `code`, dann eine Ausgangs-Regel mit passender Art und
335
+ * passendem `outcome`, sonst die Regel der Art aus [ERROR_RULES]. Beide
336
+ * Kassen ordnen so ein.
337
+ */
338
+ export function findErrorRule(kind, detail = {}) {
339
+ const { code, outcome } = detail;
340
+ if (code !== undefined && code !== null) {
341
+ const treffer = ERROR_CODE_RULES.find((r) => r.kind === kind && r.codes.includes(code));
342
+ if (treffer)
343
+ return treffer;
344
+ }
345
+ if (outcome !== undefined && outcome !== null) {
346
+ const treffer = ERROR_OUTCOME_RULES.find((r) => r.kind === kind && r.outcome === outcome);
347
+ if (treffer)
348
+ return treffer;
349
+ }
350
+ // Jede Art hat genau eine Regel; hierher kommt nur eine fremde Art nicht.
351
+ return ERROR_RULES.find((r) => r.kind === kind) ?? ERROR_RULES[ERROR_RULES.length - 1];
352
+ }
353
+ /**
354
+ * Kassen-Aufrufe mit Wirkung, die sich nach einem Netzfehler nicht gefahrlos
355
+ * wiederholen lassen: die sechs, deren Ausgang das Paket selbst als unklar
356
+ * fuehrt (Beleg, auch Null- und Startbeleg ueber `createReceipt`, Storno,
357
+ * FinanzOnline, die drei Geldwege), dazu der Druckjob (zweiter Bon) und die
358
+ * Belegmail (zweite Mail). Ob die Anfrage vor dem Abriss schon draussen war,
359
+ * sieht das Paket nicht (fetch wirft in beiden Faellen gleich); darum gilt
360
+ * hier immer der vorsichtige Satz.
361
+ */
362
+ export const CALLS_WITH_EFFECT = Object.freeze([
363
+ 'createReceipt',
364
+ 'cancelReceipt',
365
+ 'financeWebService',
366
+ 'hobexPayApi',
367
+ 'hobexRefundApi',
368
+ 'stripeCaptureIntent',
369
+ 'createPrintJob',
370
+ 'sendReceiptEmail',
371
+ ]);
372
+ const MIT_WIRKUNG = new Set(CALLS_WITH_EFFECT);
373
+ /**
374
+ * Der Ausgang, nach dem der Satz gewaehlt wird: `'unknown'`, wenn das Paket
375
+ * ihn als unklar fuehrt ([isOutcomeUnknown]) oder wenn eine Frist bzw. ein
376
+ * Netzfehler einen Aufruf aus [CALLS_WITH_EFFECT] traf; sonst der Ausgang
377
+ * des Fehlers (`'rejected'`) bzw. `undefined` fuer fremde Fehler.
378
+ */
379
+ export function messageOutcome(error) {
380
+ if (isOutcomeUnknown(error))
381
+ return 'unknown';
382
+ if (error instanceof KasseneckNetworkError)
383
+ return MIT_WIRKUNG.has(error.functionName.split('/')[0]) ? 'unknown' : error.outcome;
384
+ if (error instanceof KasseneckApiError || error instanceof KasseneckHttpError)
385
+ return error.outcome;
386
+ return undefined;
387
+ }
289
388
  /**
290
389
  * Beleg per E-Mail senden: welcher `code` des Backends welchen Satz bekommt.
291
390
  *
@@ -369,6 +468,10 @@ const BESCHRIFTUNGEN_ROH = {
369
468
  'message.dismiss_warning': { text: 'Verstanden – Warnung ausblenden: {message}', placeholders: ['message'] },
370
469
  // --- Kopplung und Abmelden -----------------------------------------------
371
470
  'pairing.pair_again': { text: 'Neu koppeln' },
471
+ // Ein Geraet ohne Namen (`deviceLabel: null` unter /v3) in der Auswahl.
472
+ 'register.device_unnamed': { text: 'Kasse' },
473
+ // Restzeit der PIN-Sperre unter dem Satz des Backends.
474
+ 'login.locked_seconds': { text: 'Noch {seconds} s gesperrt', placeholders: ['seconds'] },
372
475
  'logout.question': { text: 'Wirklich abmelden?' },
373
476
  'logout.keep_working': { text: 'Weiter arbeiten' },
374
477
  'device.unpair': { text: 'Gerät entkoppeln' },
@@ -397,6 +500,9 @@ const BESCHRIFTUNGEN_ROH = {
397
500
  'split.payment': { text: 'Zahlung {n}', placeholders: ['n'] },
398
501
  'split.amount': { text: 'Betrag' },
399
502
  'split.remaining': { text: 'Rest' },
503
+ // Letzte Runde bei Getrennt zahlen: der Rest samt Rundungscent;
504
+ // `{cents}` traegt das Vorzeichen (`+1`, `−1`).
505
+ 'split.remaining_with_rounding': { text: 'Rest inkl. Rundung {amount} ({cents} ct)', placeholders: ['amount', 'cents'] },
400
506
  'split.divide': { text: '÷ {n}', placeholders: ['n'] },
401
507
  'split.tip_basis': { text: '% von diesem Betrag' },
402
508
  'split.of_which_tip': { text: 'davon Trinkgeld {amount}', placeholders: ['amount'] },
@@ -33,6 +33,10 @@ import type { Voucher } from '../models/voucher.js';
33
33
  * rabattiert). Das weiss nur die Kasse; darum ist `tipRecipient` Pflicht,
34
34
  * sobald Trinkgeld ohne `recipients` vorkommt. Ohne angemeldeten Benutzer
35
35
  * (Geraete-Schluessel) gilt `'staff'`.
36
+ *
37
+ * **Nicht rechenbar:** jede unbrauchbare Eingabe wirft [ReceiptDueError]
38
+ * (`code: 'receipt_due_unavailable'`, `reason` im Einzelnen), nie still eine
39
+ * falsche Zahl. Dann ist nichts gesendet.
36
40
  */
37
41
  /** Wer das Trinkgeld ohne `recipients` bekommt (Kennzeichen des angemeldeten Kassen-Benutzers). */
38
42
  export type ReceiptDueTipRecipient = 'owner' | 'staff';
@@ -78,6 +82,35 @@ export interface ReceiptDueBreakdown {
78
82
  /** Toepfe je auf Cent gerundet; `null` bei Start- und Nullbeleg. */
79
83
  bucketsCents: ReceiptDueBuckets | null;
80
84
  }
85
+ /**
86
+ * Warum sich der Zahlbetrag nicht rechnen laesst. Fest wie ein Backend-Code;
87
+ * dieselbe Liste im Dart-Zwilling und in `fixtures/receipt-due-errors.json`.
88
+ */
89
+ export declare const RECEIPT_DUE_ERROR_REASONS: readonly ["unknown_receipt_type", "tip_not_allowed", "invalid_item", "invalid_voucher", "invalid_tip", "unknown_payment_method", "tip_conflict", "tip_recipient_missing", "tip_without_goods"];
90
+ export type ReceiptDueErrorReason = (typeof RECEIPT_DUE_ERROR_REASONS)[number];
91
+ /**
92
+ * Der Zahlbetrag laesst sich aus dieser Eingabe nicht rechnen, etwa
93
+ * Trinkgeld mit Betrag ohne Ware (`reason: 'tip_without_goods'`).
94
+ *
95
+ * Eindeutig **nicht gesendet**: die Rechnung laeuft ganz im Paket, vor jedem
96
+ * Aufruf. `outcome` ist darum immer `'rejected'`; nichts ist geschehen, keine
97
+ * Belegnummer verbraucht, keine Karte belastet. Die Kasse sagt das dem
98
+ * Kassier, bevor sie ein Terminal anspricht. Entscheiden am `code` bzw.
99
+ * `reason`, nie an `message` (die ist fuers Protokoll).
100
+ *
101
+ * Bis 1.0.0-rc.4 warf die Rechnung hier einen `RangeError` ohne Code.
102
+ */
103
+ export declare class ReceiptDueError extends Error {
104
+ readonly name = "ReceiptDueError";
105
+ /** Stabiler Code fuer jede Ursache. */
106
+ readonly code: "receipt_due_unavailable";
107
+ /** Die Ursache im Einzelnen. */
108
+ readonly reason: ReceiptDueErrorReason;
109
+ /** Immer `'rejected'`: es ging nichts hinaus. */
110
+ readonly outcome: "rejected";
111
+ constructor(reason: ReceiptDueErrorReason, message: string);
112
+ }
113
+ export declare function isReceiptDueError(error: unknown): error is ReceiptDueError;
81
114
  type BelegArt = ReceiptType | ReceiptTypeKey | string;
82
115
  /** Zahlbetrag in Cent, siehe Modulkommentar. */
83
116
  export declare function receiptDueCents(items: readonly ReceiptItem[], vouchers: readonly Voucher[], receiptType: BelegArt, options?: ReceiptDueOptions): number;
@@ -1,4 +1,47 @@
1
1
  import { ReceiptType, VoucherAction, VoucherType } from '../enums/index.js';
2
+ /**
3
+ * Warum sich der Zahlbetrag nicht rechnen laesst. Fest wie ein Backend-Code;
4
+ * dieselbe Liste im Dart-Zwilling und in `fixtures/receipt-due-errors.json`.
5
+ */
6
+ export const RECEIPT_DUE_ERROR_REASONS = Object.freeze([
7
+ 'unknown_receipt_type',
8
+ 'tip_not_allowed',
9
+ 'invalid_item',
10
+ 'invalid_voucher',
11
+ 'invalid_tip',
12
+ 'unknown_payment_method',
13
+ 'tip_conflict',
14
+ 'tip_recipient_missing',
15
+ 'tip_without_goods',
16
+ ]);
17
+ /**
18
+ * Der Zahlbetrag laesst sich aus dieser Eingabe nicht rechnen, etwa
19
+ * Trinkgeld mit Betrag ohne Ware (`reason: 'tip_without_goods'`).
20
+ *
21
+ * Eindeutig **nicht gesendet**: die Rechnung laeuft ganz im Paket, vor jedem
22
+ * Aufruf. `outcome` ist darum immer `'rejected'`; nichts ist geschehen, keine
23
+ * Belegnummer verbraucht, keine Karte belastet. Die Kasse sagt das dem
24
+ * Kassier, bevor sie ein Terminal anspricht. Entscheiden am `code` bzw.
25
+ * `reason`, nie an `message` (die ist fuers Protokoll).
26
+ *
27
+ * Bis 1.0.0-rc.4 warf die Rechnung hier einen `RangeError` ohne Code.
28
+ */
29
+ export class ReceiptDueError extends Error {
30
+ name = 'ReceiptDueError';
31
+ /** Stabiler Code fuer jede Ursache. */
32
+ code = 'receipt_due_unavailable';
33
+ /** Die Ursache im Einzelnen. */
34
+ reason;
35
+ /** Immer `'rejected'`: es ging nichts hinaus. */
36
+ outcome = 'rejected';
37
+ constructor(reason, message) {
38
+ super(`Zahlbetrag: ${message}`);
39
+ this.reason = reason;
40
+ }
41
+ }
42
+ export function isReceiptDueError(error) {
43
+ return error instanceof ReceiptDueError;
44
+ }
2
45
  const UMSATZ = new Set(['standard', 'cancellation', 'training']);
3
46
  const TRINKGELD_ERLAUBT = new Set(['standard', 'training']);
4
47
  const ZAHLARTEN = new Set(['cash', 'creditCard', 'online', 'uberApp', 'uberCard', 'uberCash', 'boltApp', 'boltCard', 'boltCash']);
@@ -11,7 +54,7 @@ export function receiptDueBreakdown(items, vouchers, receiptType, options = {})
11
54
  const art = belegArt(receiptType);
12
55
  const trinkgelder = trinkgeldAuftraege(options);
13
56
  if (trinkgelder.length > 0 && !TRINKGELD_ERLAUBT.has(art)) {
14
- throw new RangeError(`Zahlbetrag: Trinkgeld gibt es nur bei standard und training, nicht bei "${art}".`);
57
+ throw new ReceiptDueError('tip_not_allowed', `Trinkgeld gibt es nur bei standard und training, nicht bei "${art}".`);
15
58
  }
16
59
  if (!UMSATZ.has(art)) {
17
60
  return { dueCents: 0, counterDeltaCents: 0, valueVoucherFlowCents: 0, bucketsCents: null };
@@ -41,7 +84,7 @@ export function receiptDueBreakdown(items, vouchers, receiptType, options = {})
41
84
  function belegArt(wert) {
42
85
  const text = typeof wert === 'object' && wert !== null ? wert.value : wert;
43
86
  if (typeof text !== 'string' || !Object.prototype.hasOwnProperty.call(ReceiptType, text)) {
44
- throw new RangeError(`Zahlbetrag: unbekannter Belegtyp "${String(text)}".`);
87
+ throw new ReceiptDueError('unknown_receipt_type', `unbekannter Belegtyp "${String(text)}".`);
45
88
  }
46
89
  return text;
47
90
  }
@@ -53,17 +96,17 @@ function euroToCent(euro) {
53
96
  function innen(item, i) {
54
97
  const satz = typeof item.vat === 'number' ? item.vat : item.vat?.rate;
55
98
  if (typeof satz !== 'number' || !Number.isFinite(satz))
56
- throw new RangeError(`Zahlbetrag: Position ${i + 1} ohne Steuersatz.`);
99
+ throw new ReceiptDueError('invalid_item', `Position ${i + 1} ohne Steuersatz.`);
57
100
  if (!Number.isSafeInteger(item.priceCents))
58
- throw new RangeError(`Zahlbetrag: Position ${i + 1} hat keinen ganzen Cent-Preis.`);
101
+ throw new ReceiptDueError('invalid_item', `Position ${i + 1} hat keinen ganzen Cent-Preis.`);
59
102
  if (typeof item.quantity !== 'number' || !Number.isFinite(item.quantity))
60
- throw new RangeError(`Zahlbetrag: Position ${i + 1} hat keine gueltige Menge.`);
103
+ throw new ReceiptDueError('invalid_item', `Position ${i + 1} hat keine gueltige Menge.`);
61
104
  const tip = item.kind === 'tip' ? (item.recipient?.owner === true ? 'owner' : 'staff') : null;
62
105
  return { amount: item.quantity, priceOne: Math.round(item.priceCents) / 100, vat: satz, tip };
63
106
  }
64
107
  function gutscheinInnen(v, i) {
65
108
  if (!Number.isSafeInteger(v.valueCents))
66
- throw new RangeError(`Zahlbetrag: Gutschein ${i + 1} hat keinen ganzen Cent-Wert.`);
109
+ throw new ReceiptDueError('invalid_voucher', `Gutschein ${i + 1} hat keinen ganzen Cent-Wert.`);
67
110
  return { action: String(v.action), type: String(v.type), value: Math.round(v.valueCents) / 100 };
68
111
  }
69
112
  /** Trinkgeld-Auftraege in der Reihenfolge des Servers: `tip`, dann je Zahlart die Summe der `tipCents`. */
@@ -71,7 +114,7 @@ function trinkgeldAuftraege(options) {
71
114
  const raus = [];
72
115
  const tipCents = (options.payments ?? []).some((z) => z != null && z.tipCents !== undefined);
73
116
  if (options.tip != null && tipCents) {
74
- throw new RangeError('Zahlbetrag: tip und payments[].tipCents gehen nicht zugleich (tip_conflict).');
117
+ throw new ReceiptDueError('tip_conflict', 'tip und payments[].tipCents gehen nicht zugleich (tip_conflict).');
75
118
  }
76
119
  if (options.tip != null) {
77
120
  const tip = options.tip;
@@ -80,14 +123,14 @@ function trinkgeldAuftraege(options) {
80
123
  const recipients = typeof tip === 'number' ? undefined : tip.recipients;
81
124
  if (recipients != null) {
82
125
  if (recipients.length === 0)
83
- throw new RangeError('Zahlbetrag: recipients darf nicht leer sein.');
126
+ throw new ReceiptDueError('invalid_tip', 'recipients darf nicht leer sein.');
84
127
  for (const r of recipients) {
85
128
  pruefeCent(r.cents, 'Trinkgeld-Anteil');
86
129
  if (typeof r.owner !== 'boolean')
87
- throw new RangeError('Zahlbetrag: recipients[].owner muss true oder false sein.');
130
+ throw new ReceiptDueError('invalid_tip', 'recipients[].owner muss true oder false sein.');
88
131
  }
89
132
  if (recipients.reduce((s, r) => s + r.cents, 0) !== cents) {
90
- throw new RangeError('Zahlbetrag: die Anteile des Trinkgelds ergeben nicht den Betrag.');
133
+ throw new ReceiptDueError('invalid_tip', 'die Anteile des Trinkgelds ergeben nicht den Betrag.');
91
134
  }
92
135
  raus.push({ empfaenger: recipients.map((r) => ({ cents: r.cents, owner: r.owner })) });
93
136
  }
@@ -103,7 +146,7 @@ function trinkgeldAuftraege(options) {
103
146
  pruefeCent(z.tipCents, 'tipCents');
104
147
  const methode = typeof z.method === 'object' && z.method !== null ? z.method.value : z.method;
105
148
  if (typeof methode !== 'string' || !ZAHLARTEN.has(methode))
106
- throw new RangeError(`Zahlbetrag: unbekannte Zahlart "${String(methode)}".`);
149
+ throw new ReceiptDueError('unknown_payment_method', `unbekannte Zahlart "${String(methode)}".`);
107
150
  je.set(methode, (je.get(methode) ?? 0) + z.tipCents);
108
151
  }
109
152
  for (const cents of je.values())
@@ -112,7 +155,7 @@ function trinkgeldAuftraege(options) {
112
155
  }
113
156
  function pruefeCent(wert, was) {
114
157
  if (typeof wert !== 'number' || !Number.isInteger(wert) || wert <= 0) {
115
- throw new RangeError(`Zahlbetrag: ${was} muss eine ganze Zahl in Cent > 0 sein.`);
158
+ throw new ReceiptDueError('invalid_tip', `${was} muss eine ganze Zahl in Cent > 0 sein.`);
116
159
  }
117
160
  }
118
161
  /** `tip-core.buildTipItems`: Personal in den Null-%-Satz, Inhaber anteilig auf die Saetze der Ware. */
@@ -120,13 +163,13 @@ function trinkgeldPositionen(auftrag, positionen, standard) {
120
163
  const basis = warenCentsJeSatz(positionen);
121
164
  const summe = Object.values(basis).reduce((s, c) => s + c, 0);
122
165
  if (summe <= 0)
123
- throw new RangeError('Zahlbetrag: Trinkgeld braucht mindestens eine Position mit Betrag.');
166
+ throw new ReceiptDueError('tip_without_goods', 'Trinkgeld braucht mindestens eine Position mit Betrag.');
124
167
  const raus = [];
125
168
  for (const e of auftrag.empfaenger) {
126
169
  let owner = e.owner;
127
170
  if (owner == null) {
128
171
  if (standard !== 'owner' && standard !== 'staff') {
129
- throw new RangeError("Zahlbetrag: tipRecipient fehlt ('owner' oder 'staff', wie der angemeldete Kassen-Benutzer).");
172
+ throw new ReceiptDueError('tip_recipient_missing', "tipRecipient fehlt ('owner' oder 'staff', wie der angemeldete Kassen-Benutzer).");
130
173
  }
131
174
  owner = standard === 'owner';
132
175
  }
@@ -136,7 +179,7 @@ function trinkgeldPositionen(auftrag, positionen, standard) {
136
179
  }
137
180
  const teile = splitOwnerTip(e.cents, basis);
138
181
  if (teile.length === 0)
139
- throw new RangeError('Zahlbetrag: Inhaber-Trinkgeld braucht mindestens eine Position mit Betrag.');
182
+ throw new ReceiptDueError('tip_without_goods', 'Inhaber-Trinkgeld braucht mindestens eine Position mit Betrag.');
140
183
  for (const teil of teile) {
141
184
  if (teil.cents !== 0)
142
185
  raus.push({ amount: 1, priceOne: teil.cents / 100, vat: teil.vat, tip: 'owner' });
@@ -6,4 +6,4 @@ export { type SheetLogoSize, type SheetLogo, type LogoDimensions, type SheetBloc
6
6
  export { rasterizeLogo } from './bild.js';
7
7
  export { type BrandMarkRaster, BRAND_MARK_RASTERS, BRAND_MARK_PATHS } from './marke-daten.js';
8
8
  export { brandMarkImage } from './marke.js';
9
- export { type ReceiptDueTip, type ReceiptDueTipRecipient, type ReceiptDueOptions, type ReceiptDueBuckets, type ReceiptDueBreakdown, receiptDueCents, receiptDueBreakdown, } from './due.js';
9
+ export { type ReceiptDueTip, type ReceiptDueTipRecipient, type ReceiptDueOptions, type ReceiptDueBuckets, type ReceiptDueBreakdown, type ReceiptDueErrorReason, receiptDueCents, receiptDueBreakdown, ReceiptDueError, RECEIPT_DUE_ERROR_REASONS, isReceiptDueError, } from './due.js';
@@ -6,4 +6,4 @@ export { SHEET_LOGO_SIZES, LOGO_MAX_PIXELS, DOTS_PER_CHAR, DOTS_PER_LINE, paperS
6
6
  export { rasterizeLogo } from './bild.js';
7
7
  export { BRAND_MARK_RASTERS, BRAND_MARK_PATHS } from './marke-daten.js';
8
8
  export { brandMarkImage } from './marke.js';
9
- export { receiptDueCents, receiptDueBreakdown, } from './due.js';
9
+ export { receiptDueCents, receiptDueBreakdown, ReceiptDueError, RECEIPT_DUE_ERROR_REASONS, isReceiptDueError, } from './due.js';
@@ -107,3 +107,4 @@ export declare function invalidStoredPosSettings(stored: {
107
107
  * weg.
108
108
  */
109
109
  export declare function fromStoredArticle(id: string, stored: unknown): PosArticle;
110
+ export { fromStoredLayout, toStoredLayout, fromStoredPrintLogo, toStoredPrintLogo } from './layout.js';
@@ -215,3 +215,5 @@ export function invalidStoredPosSettings(stored) {
215
215
  export function fromStoredArticle(id, stored) {
216
216
  return fromPosArticlePayload(_storedArticleToWire(id, stored));
217
217
  }
218
+ // ---- Zeilenmodell und Drucklogo ----------------------------------------------
219
+ export { fromStoredLayout, toStoredLayout, fromStoredPrintLogo, toStoredPrintLogo } from './layout.js';
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Zeilenmodell und Drucklogo in der inneren Form (deutsch) <-> Form 1.0.
3
+ *
4
+ * Die innere Form ist die, die das Backend speichert und am Draht `/api`
5
+ * (Kanal intern) spricht, und die, in der 0.x das Zeilenmodell im Browser
6
+ * zwischengespeichert hat:
7
+ *
8
+ * - Zeilenmodell: `regelwerk` statt `ruleset`, Bannerzeilen mit `ton`
9
+ * (`belegart`, `warnung`) statt `tone` (`receipt_type`, `warning`);
10
+ * - Drucklogo (`createPrintJob`, Parameter `logo`): `stufe`, `pxBreite`,
11
+ * `pxHoehe`, `breite`, `hoehe`, `zeilen` (Base64, MSB zuerst) statt
12
+ * [PrintLogo] (`size`, `pixelWidth`, `pixelHeight`, `raster`).
13
+ *
14
+ * Dieselben Namen wie der Rand des Backends unter `/v3` (Vokabular
15
+ * `createPrintJob`/`getReceiptWithCompany`: `layout.ruleset` <-> `regelwerk`,
16
+ * `lines[].tone` <-> `ton`, Katalog `LAYOUT_TON`, `logo.scale` <-> `stufe` ...).
17
+ * Ein unbekannter Ton geht woertlich durch, wie ihn auch der Rand durchreicht.
18
+ */
19
+ import type { ReceiptLayout } from '../receipt/layout.js';
20
+ import type { PrintLogo } from '../receipt/layout-escpos.js';
21
+ /**
22
+ * Ein Zeilenmodell in der inneren Form (0.x-Zwischenspeicher, Antwort unter
23
+ * `/api`) als [ReceiptLayout]. Nimmt auch die Form 1.0 an und laesst sie
24
+ * unveraendert; so liest eine Funktion alte und neue Eintraege. Die
25
+ * Reihenfolge der Schluessel bleibt.
26
+ *
27
+ * `null`, wenn es kein ganzes Zeilenmodell ist: fehlt, kein Objekt, keine
28
+ * oder leere `lines`, eine Zeile, die kein Objekt ist, oder kein `paperSize`.
29
+ * Dann baut der Aufrufer neu (`receiptLayoutFromResult` aus Beleg, Firma und
30
+ * `testCashregister`/`testSignature`), und TESTKASSE und die Warnzeilen
31
+ * stehen wieder da. Ein halb lesbares Zeilenmodell gewaenne sonst immer.
32
+ */
33
+ export declare function fromStoredLayout(stored: unknown): ReceiptLayout | null;
34
+ /**
35
+ * Ein [ReceiptLayout] in der inneren Form, fuer einen Aufruf unter `/api`
36
+ * (`createPrintJob`, `renderBelegPdfAdmin`) oder einen Leser der 0.x-Form,
37
+ * in der Reihenfolge der Schluessel wie 0.31 (`lines`, `paperSize`,
38
+ * `regelwerk`, sofern die Eingabe sie so hat). Ohne diese Uebersetzung liest
39
+ * das Backend den Ton nicht, und der Rahmen der Warnzeilen (TESTKASSE,
40
+ * Sicherheitseinrichtung ausgefallen) und des Belegart-Aufdrucks fiele am
41
+ * Bon und im PDF weg.
42
+ */
43
+ export declare function toStoredLayout(layout: ReceiptLayout): Record<string, unknown>;
44
+ /**
45
+ * Ein [PrintLogo] in der inneren Form des Druckjobs (`createPrintJob` unter
46
+ * `/api`): dieselben Punkte, die das Paket unter `/v3` als `logo` schickt.
47
+ */
48
+ export declare function toStoredPrintLogo(logo: PrintLogo): Record<string, unknown>;
49
+ /**
50
+ * Ein Drucklogo in der inneren Form als [PrintLogo]. Die Zeilen sind die
51
+ * Rasterzeilen als Base64 (MSB zuerst, jede Zeile auf volle Bytes
52
+ * aufgefuellt). Wirft `KasseneckValidationError`, wenn Stufe, Masse oder
53
+ * Zeilen nicht zusammenpassen.
54
+ */
55
+ export declare function fromStoredPrintLogo(stored: unknown): PrintLogo;
@@ -0,0 +1,113 @@
1
+ import { rasterRowsBase64 } from '../printing/escpos.js';
2
+ import { KasseneckValidationError } from '../client/errors.js';
3
+ const istObjekt = (w) => w !== null && typeof w === 'object' && !Array.isArray(w);
4
+ const TON_NACH_1_0 = { belegart: 'receipt_type', warnung: 'warning' };
5
+ const TON_NACH_INNEN = { receipt_type: 'belegart', warning: 'warnung' };
6
+ /**
7
+ * Ein Objekt mit umbenannten Schluesseln, an derselben Stelle: die
8
+ * Reihenfolge bleibt, damit ein gespeicherter Eintrag byte-gleich
9
+ * zurueckkommt (Hash- und Cache-Vergleiche). `werte` ersetzt den Wert eines
10
+ * (alten) Schluessels.
11
+ */
12
+ function umbenannt(o, namen, werte = {}) {
13
+ const raus = {};
14
+ for (const [k, w] of Object.entries(o)) {
15
+ const neu = namen[k] ?? k;
16
+ // Traegt die Eingabe beide Namen, gewinnt der schon neue.
17
+ if (neu !== k && Object.prototype.hasOwnProperty.call(o, neu))
18
+ continue;
19
+ raus[neu] = werte[k] ? werte[k](w) : w;
20
+ }
21
+ return raus;
22
+ }
23
+ const tonNach = (tabelle) => (w) => (typeof w === 'string' ? tabelle[w] ?? w : w);
24
+ /**
25
+ * Ein Zeilenmodell in der inneren Form (0.x-Zwischenspeicher, Antwort unter
26
+ * `/api`) als [ReceiptLayout]. Nimmt auch die Form 1.0 an und laesst sie
27
+ * unveraendert; so liest eine Funktion alte und neue Eintraege. Die
28
+ * Reihenfolge der Schluessel bleibt.
29
+ *
30
+ * `null`, wenn es kein ganzes Zeilenmodell ist: fehlt, kein Objekt, keine
31
+ * oder leere `lines`, eine Zeile, die kein Objekt ist, oder kein `paperSize`.
32
+ * Dann baut der Aufrufer neu (`receiptLayoutFromResult` aus Beleg, Firma und
33
+ * `testCashregister`/`testSignature`), und TESTKASSE und die Warnzeilen
34
+ * stehen wieder da. Ein halb lesbares Zeilenmodell gewaenne sonst immer.
35
+ */
36
+ export function fromStoredLayout(stored) {
37
+ if (!istObjekt(stored) || !Array.isArray(stored.lines) || stored.lines.length === 0)
38
+ return null;
39
+ if (!stored.lines.every(istObjekt))
40
+ return null;
41
+ if (typeof stored.paperSize !== 'string' || stored.paperSize === '')
42
+ return null;
43
+ return umbenannt(stored, { regelwerk: 'ruleset' }, {
44
+ lines: (zeilen) => zeilen.map((z) => umbenannt(z, { ton: 'tone' }, { ton: tonNach(TON_NACH_1_0) })),
45
+ });
46
+ }
47
+ /**
48
+ * Ein [ReceiptLayout] in der inneren Form, fuer einen Aufruf unter `/api`
49
+ * (`createPrintJob`, `renderBelegPdfAdmin`) oder einen Leser der 0.x-Form,
50
+ * in der Reihenfolge der Schluessel wie 0.31 (`lines`, `paperSize`,
51
+ * `regelwerk`, sofern die Eingabe sie so hat). Ohne diese Uebersetzung liest
52
+ * das Backend den Ton nicht, und der Rahmen der Warnzeilen (TESTKASSE,
53
+ * Sicherheitseinrichtung ausgefallen) und des Belegart-Aufdrucks fiele am
54
+ * Bon und im PDF weg.
55
+ */
56
+ export function toStoredLayout(layout) {
57
+ if (!istObjekt(layout) || !Array.isArray(layout.lines)) {
58
+ throw new KasseneckValidationError('toStoredLayout', 'layout ohne lines', 'request');
59
+ }
60
+ return umbenannt(layout, { ruleset: 'regelwerk' }, {
61
+ lines: (zeilen) => zeilen.map((z) => (istObjekt(z) ? umbenannt(z, { tone: 'ton' }, { tone: tonNach(TON_NACH_INNEN) }) : z)),
62
+ });
63
+ }
64
+ /**
65
+ * Ein [PrintLogo] in der inneren Form des Druckjobs (`createPrintJob` unter
66
+ * `/api`): dieselben Punkte, die das Paket unter `/v3` als `logo` schickt.
67
+ */
68
+ export function toStoredPrintLogo(logo) {
69
+ return {
70
+ stufe: logo.size, pxBreite: logo.pixelWidth, pxHoehe: logo.pixelHeight,
71
+ breite: logo.raster.width, hoehe: logo.raster.height, zeilen: rasterRowsBase64(logo.raster),
72
+ };
73
+ }
74
+ const STUFEN = new Set(['S', 'M', 'L', 'XL']);
75
+ const ganz = (w) => typeof w === 'number' && Number.isInteger(w) && w > 0;
76
+ /**
77
+ * Ein Drucklogo in der inneren Form als [PrintLogo]. Die Zeilen sind die
78
+ * Rasterzeilen als Base64 (MSB zuerst, jede Zeile auf volle Bytes
79
+ * aufgefuellt). Wirft `KasseneckValidationError`, wenn Stufe, Masse oder
80
+ * Zeilen nicht zusammenpassen.
81
+ */
82
+ export function fromStoredPrintLogo(stored) {
83
+ const name = 'fromStoredPrintLogo';
84
+ if (!istObjekt(stored))
85
+ throw new KasseneckValidationError(name, 'kein Logo-Objekt', 'request');
86
+ const { stufe, pxBreite, pxHoehe, breite, hoehe, zeilen } = stored;
87
+ if (typeof stufe !== 'string' || !STUFEN.has(stufe))
88
+ throw new KasseneckValidationError(name, 'stufe ist nicht S, M, L oder XL', 'request');
89
+ if (!ganz(pxBreite) || !ganz(pxHoehe) || !ganz(breite) || !ganz(hoehe)) {
90
+ throw new KasseneckValidationError(name, 'pxBreite, pxHoehe, breite und hoehe muessen ganze Zahlen > 0 sein', 'request');
91
+ }
92
+ if (typeof zeilen !== 'string')
93
+ throw new KasseneckValidationError(name, 'zeilen fehlt', 'request');
94
+ let binaer;
95
+ try {
96
+ binaer = atob(zeilen);
97
+ }
98
+ catch {
99
+ throw new KasseneckValidationError(name, 'zeilen ist kein Base64', 'request');
100
+ }
101
+ const byteJeZeile = Math.ceil(breite / 8);
102
+ if (binaer.length !== byteJeZeile * hoehe)
103
+ throw new KasseneckValidationError(name, 'zeilen passen nicht zu breite und hoehe', 'request');
104
+ const dots = new Uint8Array(breite * hoehe);
105
+ for (let y = 0; y < hoehe; y++) {
106
+ for (let x = 0; x < breite; x++) {
107
+ if (binaer.charCodeAt(y * byteJeZeile + (x >> 3)) & (0x80 >> (x & 7)))
108
+ dots[y * breite + x] = 1;
109
+ }
110
+ }
111
+ const raster = { width: breite, height: hoehe, dots };
112
+ return { size: stufe, pixelWidth: pxBreite, pixelHeight: pxHoehe, raster };
113
+ }
@@ -4,4 +4,4 @@
4
4
  * Client-Version, Nachtrag §6). Ein Test haelt sie gleich mit `package.json`;
5
5
  * beim Anheben der Version beide Stellen aendern.
6
6
  */
7
- export declare const PACKAGE_VERSION = "1.0.0-rc.4";
7
+ export declare const PACKAGE_VERSION = "1.0.0";
@@ -4,4 +4,4 @@
4
4
  * Client-Version, Nachtrag §6). Ein Test haelt sie gleich mit `package.json`;
5
5
  * beim Anheben der Version beide Stellen aendern.
6
6
  */
7
- export const PACKAGE_VERSION = '1.0.0-rc.4';
7
+ export const PACKAGE_VERSION = '1.0.0';
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.0.0-rc.4",
2
+ "version": "1.0.0",
3
3
  "measuredOn": {
4
4
  "tid": "3600335",
5
5
  "hpsVersion": "1.10.0",
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "title": "Kasseneck Rechnungs-API",
4
4
  "version": 2,
5
- "package": "1.0.0-rc.4",
5
+ "package": "1.0.0",
6
6
  "endpoints": {
7
7
  "createCustomer": {
8
8
  "request": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.0.0-rc.4",
2
+ "version": "1.0.0",
3
3
  "languages": [
4
4
  "de",
5
5
  "en"