@kreiseck/kasseneck-api 1.16.0 → 1.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/cjs/client/receipts.js +8 -0
  3. package/dist/cjs/client/reports.d.ts +10 -9
  4. package/dist/cjs/client/reports.js +10 -9
  5. package/dist/cjs/index.d.ts +1 -1
  6. package/dist/cjs/models/voucher.d.ts +46 -4
  7. package/dist/cjs/models/voucher.js +58 -11
  8. package/dist/cjs/pos/artikel.d.ts +9 -0
  9. package/dist/cjs/pos/artikel.js +1 -0
  10. package/dist/cjs/pos/gutschein.d.ts +107 -2
  11. package/dist/cjs/pos/gutschein.js +124 -4
  12. package/dist/cjs/pos/index.d.ts +1 -1
  13. package/dist/cjs/pos/index.js +7 -3
  14. package/dist/cjs/pos/texte.d.ts +39 -0
  15. package/dist/cjs/pos/texte.js +22 -0
  16. package/dist/cjs/receipt/due.d.ts +20 -9
  17. package/dist/cjs/receipt/due.js +1 -0
  18. package/dist/cjs/receipt/gutschein-plan.d.ts +23 -7
  19. package/dist/cjs/receipt/gutschein-plan.js +67 -2
  20. package/dist/cjs/receipt/index.d.ts +1 -1
  21. package/dist/cjs/stored/draht.js +10 -2
  22. package/dist/cjs/stored/vokabular.d.ts +1 -1
  23. package/dist/cjs/stored/vokabular.js +84 -3
  24. package/dist/cjs/version.d.ts +1 -1
  25. package/dist/cjs/version.js +1 -1
  26. package/dist/esm/client/receipts.js +8 -0
  27. package/dist/esm/client/reports.d.ts +10 -9
  28. package/dist/esm/client/reports.js +10 -9
  29. package/dist/esm/index.d.ts +1 -1
  30. package/dist/esm/models/voucher.d.ts +46 -4
  31. package/dist/esm/models/voucher.js +56 -11
  32. package/dist/esm/pos/artikel.d.ts +9 -0
  33. package/dist/esm/pos/artikel.js +1 -0
  34. package/dist/esm/pos/gutschein.d.ts +107 -2
  35. package/dist/esm/pos/gutschein.js +123 -3
  36. package/dist/esm/pos/index.d.ts +1 -1
  37. package/dist/esm/pos/index.js +2 -2
  38. package/dist/esm/pos/texte.d.ts +39 -0
  39. package/dist/esm/pos/texte.js +22 -0
  40. package/dist/esm/receipt/due.d.ts +20 -9
  41. package/dist/esm/receipt/due.js +1 -0
  42. package/dist/esm/receipt/gutschein-plan.d.ts +23 -7
  43. package/dist/esm/receipt/gutschein-plan.js +68 -3
  44. package/dist/esm/receipt/index.d.ts +1 -1
  45. package/dist/esm/stored/draht.js +10 -2
  46. package/dist/esm/stored/vokabular.d.ts +1 -1
  47. package/dist/esm/stored/vokabular.js +84 -3
  48. package/dist/esm/version.d.ts +1 -1
  49. package/dist/esm/version.js +1 -1
  50. package/fixtures/hobex-hps-codes.json +1 -1
  51. package/fixtures/invoice-api.schema.json +1 -1
  52. package/fixtures/invoice-texts.json +1 -1
  53. package/fixtures/pos-texts.json +43 -2
  54. package/fixtures/receipt-due-voucher-generated.json +485 -483
  55. package/fixtures/surface.json +17 -1
  56. package/fixtures/v3/README.md +8 -1
  57. package/fixtures/v3/antworten/belege.json +27 -12
  58. package/fixtures/v3/antworten/belegmail.json +4 -1
  59. package/fixtures/v3/antworten/berichte.json +151 -0
  60. package/fixtures/v3/antworten/kasse-belege.json +27 -12
  61. package/fixtures/v3/antworten/kasse.json +366 -9
  62. package/fixtures/v3/antworten/lager.json +1 -1
  63. package/fixtures/v3/antworten/rechnungen.json +4 -1
  64. package/fixtures/v3/antworten/storno.json +4 -1
  65. package/fixtures/v3/stored/belege.json +4 -1
  66. package/fixtures/v3/stored/kasse.json +2 -2
  67. package/fixtures/v3/stored/rechnungen.json +4 -1
  68. package/fixtures/v3/v3-vokabular.json +417 -10
  69. package/fixtures/v3/zahlbetrag-faelle.json +29 -10
  70. package/package.json +1 -1
@@ -15,10 +15,27 @@ export declare const VOUCHER_STATUSES: readonly VoucherStatus[];
15
15
  /** Gueltigkeit eines Gutscheins; Daten als `YYYY-MM-DD` (Wiener Tag). Was der Server nicht nennt, fehlt. */
16
16
  export interface VoucherValidity {
17
17
  pattern: 'unlimited' | '1+3' | 'custom';
18
+ /**
19
+ * Gutschein einer Sorte „N Jahre einloesbar, danach 3 Jahre Rueckzahlung“:
20
+ * die Zahl N (1 bis 30); `pattern` ist dann `1+3` fuer jedes N. Fehlt bei
21
+ * Gutscheinen ohne Sorte und bei unbefristeten. Die Daten stehen trotzdem in
22
+ * `redeemableUntil` (+N) und `refundableUntil` (+N+3) und gelten.
23
+ */
24
+ redeemableYears?: number;
18
25
  redeemableUntil?: string;
19
26
  refundableUntil?: string;
20
27
  validUntil?: string;
21
28
  }
29
+ /** Auszahlung eines Gutscheins bzw. einer Sorte: erlaubt oder nie (nur bei unbefristeter Gueltigkeit). */
30
+ export type VoucherPayout = 'allowed' | 'never';
31
+ /** Art einer Gutschein-Sorte: nur Wertgutscheine (Leistung und Aktion sind keine Sorte). */
32
+ export type VoucherTypeKind = 'multi_purpose' | 'single_purpose';
33
+ export declare const VOUCHER_PAYOUTS: readonly VoucherPayout[];
34
+ export declare const VOUCHER_TYPE_KINDS: readonly VoucherTypeKind[];
35
+ /** Betrag einer Sorte: frei an der Kasse eingegeben oder fest. */
36
+ export declare const VOUCHER_AMOUNT_MODES: readonly ('free' | 'fixed')[];
37
+ /** Gueltigkeit einer Sorte: unbefristet oder N Jahre (das Muster der Sorte, nicht das eines Gutscheins). */
38
+ export declare const VOUCHER_TYPE_VALIDITY_PATTERNS: readonly ('unlimited' | 'years')[];
22
39
  /** Der Artikel eines Dienstleistungsgutscheins. */
23
40
  export interface VoucherArticle {
24
41
  id: string;
@@ -50,12 +67,92 @@ export interface VoucherCheck {
50
67
  * inzwischen steuerpflichtigen Konto. Fehlt bei aelteren Servern.
51
68
  */
52
69
  exempt?: boolean;
70
+ /** Name der Sorte, mit der der Gutschein verkauft wurde (beim Verkauf eingefroren); fehlt ohne Sorte. */
71
+ typeName?: string;
72
+ /**
73
+ * Auszahlung dieses Gutscheins (beim Verkauf eingefroren); fehlt ohne
74
+ * Sorte. Bei `never` lehnt der Server jede Auszahlung ab
75
+ * (`voucher_payout_not_allowed`); Einloesen geht unveraendert.
76
+ */
77
+ payout?: VoucherPayout;
78
+ }
79
+ /**
80
+ * Etikett-Vorlage in der `/v3`-Form (Groesse, Layout, Drehung, Felder,
81
+ * Freitext). Das Paket zeichnet keine Etiketten: es reicht die Vorlage als
82
+ * Kopie durch, gezeichnet wird in der Kasse (druck-kit, Kassen-App). Weitere
83
+ * Schluessel eines neueren Servers bleiben erhalten.
84
+ */
85
+ export interface VoucherLabelTemplate {
86
+ /** Etikettgroesse `BxH` in mm, z. B. `40x30`. */
87
+ size: string;
88
+ layout: string;
89
+ /** 0 oder 90 (Grad); fehlt = 0. */
90
+ rotation?: number;
91
+ fields?: Record<string, boolean>;
92
+ freeText?: string[];
93
+ [key: string]: unknown;
94
+ }
95
+ /**
96
+ * Gutschein-Sorte (Gutscheine Stufe 4b): eine Kachel im Reiter „Gutscheine“.
97
+ * Die Kasse bekommt nur verkaufbare Sorten (aktiv und gueltig), in
98
+ * Kachelfolge. Verkauft wird ein Wertgutschein nur ueber eine Sorte:
99
+ * `{ action: 'sell', type: kind, typeId: id, amountCents }` (bei `fixed` genau
100
+ * `amount.cents`); Art, Satz, Gueltigkeit und Auszahlung nimmt der Server aus
101
+ * der Sorte und friert sie am Gutschein ein.
102
+ */
103
+ export interface PosVoucherType {
104
+ id: string;
105
+ name: string;
106
+ kind: VoucherTypeKind;
107
+ /** Satz in Basispunkten, nur bei Einzweck (> 0); Mehrzweck `null`. */
108
+ vatRateBp: number | null;
109
+ /** Betrag: frei (Zahlenfeld) oder fest (sofort in den Korb), Cent ganzzahlig. */
110
+ amount: {
111
+ mode: 'free';
112
+ } | {
113
+ mode: 'fixed';
114
+ cents: number;
115
+ };
116
+ /** Unbefristet oder N Jahre einloesbar, danach 3 Jahre Rueckzahlung (N 1 bis 30). */
117
+ validity: {
118
+ pattern: 'unlimited';
119
+ } | {
120
+ pattern: 'years';
121
+ years: number;
122
+ };
123
+ payout: VoucherPayout;
124
+ /** Wirksame Vorlage der Sorte (eigene, sonst die Standardvorlage); `null`, wenn die Antwort keine brauchbare traegt. */
125
+ label: VoucherLabelTemplate | null;
126
+ order: number;
127
+ active: boolean;
128
+ /** Verkaufte Gutscheine dieser Sorte (nur zur Anzeige). */
129
+ soldCount: number;
53
130
  }
54
- /** Gutschein-Einstellungen der Kasse (Block `vouchers` aus `getKasseSettings`). */
131
+ /**
132
+ * Gutschein-Einstellungen der Kasse (Block `vouchers` aus `getKasseSettings`).
133
+ *
134
+ * Seit Gutscheine Stufe 4b verkauft eine Kasse ueber Sorten: Reiter und
135
+ * Kacheln zeigt sie mit `typeSale` (und dem Recht `voucherSell`), je Sorte aus
136
+ * `types` und die feste Kachel „Leistungsgutschein“ mit `serviceArticles`.
137
+ * `sale`, `valueVoucherType` und `vatRateBp` gelten nur Kassen vor 1.17; der
138
+ * Server schickt `sale` immer als `false`.
139
+ *
140
+ * Die vier neuen Felder sind optional, damit bestehende Literale von
141
+ * Verbrauchern weiter uebersetzen; der Leser ([posVoucherSettingsFromWire])
142
+ * setzt sie immer.
143
+ */
55
144
  export interface PosVoucherSettings {
56
145
  sale: boolean;
57
146
  valueVoucherType: 'multi_purpose' | 'single_purpose' | null;
58
147
  vatRateBp: number | null;
148
+ /** Gutscheine verkaufen (Reiter „Gutscheine“): Schalter des Servers und mindestens eine Sorte oder ein Leistungsartikel. */
149
+ typeSale?: boolean;
150
+ /** Verkaufbare Sorten in Kachelfolge. */
151
+ types?: PosVoucherType[];
152
+ /** Es gibt aktive Artikel, die als Leistungsgutschein freigegeben sind (`PosArticle.serviceVoucher`). */
153
+ serviceArticles?: boolean;
154
+ /** Standardvorlage des Etiketts (Leistungsgutschein, Notausgang); `null` ohne brauchbare Vorlage. */
155
+ labelDefault?: VoucherLabelTemplate | null;
59
156
  }
60
157
  /** Ohne Block `vouchers` (alter Server, Konto ohne Schalter): Verkaufen ist aus. */
61
158
  export declare const VOUCHER_SETTINGS_DEFAULTS: Readonly<PosVoucherSettings>;
@@ -67,7 +164,15 @@ export declare const VOUCHER_SETTINGS_DEFAULTS: Readonly<PosVoucherSettings>;
67
164
  * `multi_purpose`/`single_purpose`/`null`, der Satz eine ganze Zahl >= 0 oder
68
165
  * `null`; Mehrzweck traegt keinen Satz, Einzweck braucht einen, und ein
69
166
  * eingeschalteter Verkauf braucht eine Art. Ein ausgeschalteter Verkauf darf
70
- * eine Art behalten.
167
+ * eine Art behalten. `typeSale` und `serviceArticles` sind Wahrheitswerte,
168
+ * `types` eine Liste (fehlen sie, antwortet ein aelterer Server: aus, leer).
169
+ *
170
+ * Sorten (Gutscheine Stufe 4b) liest der Leser einzeln: eine unstimmige Sorte
171
+ * faellt weg ([sorteVomDraht]), die anderen bleiben; ist danach weder eine
172
+ * Sorte noch ein Leistungsartikel da, ist `typeSale` aus (kein leerer Reiter).
173
+ * Eine unbrauchbare Etikett-Vorlage schliesst nichts: sie wird `null`, die
174
+ * Kasse druckt dann ihren Standard (ein Verkauf scheitert nie an der Vorlage).
175
+ * Das alte Feld `label` (Kassen vor 1.17, Ruling R3) liest das Paket nicht.
71
176
  */
72
177
  export declare function posVoucherSettingsFromWire(block: unknown): PosVoucherSettings;
73
178
  /**
@@ -2,10 +2,21 @@ import { VoucherCode } from '@kreiseck/validator';
2
2
  import { KasseneckApiError, KasseneckValidationError } from '../client/errors.js';
3
3
  export const VOUCHER_KINDS = Object.freeze(['multi_purpose', 'single_purpose', 'service', 'promotion']);
4
4
  export const VOUCHER_STATUSES = Object.freeze(['active', 'used_up', 'expired', 'paid_out', 'cancelled', 'blocked']);
5
+ // Nur die Muster, die der Server an Gutscheinen nach aussen gibt: eine Sorte mit
6
+ // „N Jahre“ geht fuer jedes N als `1+3` mit den Daten und `redeemableYears` = N hinaus
7
+ // (Nachtrag E1), damit Kassen bis 1.16 beim Einloesen nicht an einem neuen Muster
8
+ // scheitern und „einloesbar bis <Datum>“ drucken.
5
9
  const VOUCHER_VALIDITY_PATTERNS = ['unlimited', '1+3', 'custom'];
10
+ export const VOUCHER_PAYOUTS = Object.freeze(['allowed', 'never']);
11
+ export const VOUCHER_TYPE_KINDS = Object.freeze(['multi_purpose', 'single_purpose']);
12
+ /** Betrag einer Sorte: frei an der Kasse eingegeben oder fest. */
13
+ export const VOUCHER_AMOUNT_MODES = Object.freeze(['free', 'fixed']);
14
+ /** Gueltigkeit einer Sorte: unbefristet oder N Jahre (das Muster der Sorte, nicht das eines Gutscheins). */
15
+ export const VOUCHER_TYPE_VALIDITY_PATTERNS = Object.freeze(['unlimited', 'years']);
6
16
  /** Ohne Block `vouchers` (alter Server, Konto ohne Schalter): Verkaufen ist aus. */
7
17
  export const VOUCHER_SETTINGS_DEFAULTS = Object.freeze({
8
18
  sale: false, valueVoucherType: null, vatRateBp: null,
19
+ typeSale: false, types: Object.freeze([]), serviceArticles: false, labelDefault: null,
9
20
  });
10
21
  /**
11
22
  * Block `vouchers` einer Einstellungs-Antwort, geschlossen im Zweifel: ist
@@ -15,10 +26,18 @@ export const VOUCHER_SETTINGS_DEFAULTS = Object.freeze({
15
26
  * `multi_purpose`/`single_purpose`/`null`, der Satz eine ganze Zahl >= 0 oder
16
27
  * `null`; Mehrzweck traegt keinen Satz, Einzweck braucht einen, und ein
17
28
  * eingeschalteter Verkauf braucht eine Art. Ein ausgeschalteter Verkauf darf
18
- * eine Art behalten.
29
+ * eine Art behalten. `typeSale` und `serviceArticles` sind Wahrheitswerte,
30
+ * `types` eine Liste (fehlen sie, antwortet ein aelterer Server: aus, leer).
31
+ *
32
+ * Sorten (Gutscheine Stufe 4b) liest der Leser einzeln: eine unstimmige Sorte
33
+ * faellt weg ([sorteVomDraht]), die anderen bleiben; ist danach weder eine
34
+ * Sorte noch ein Leistungsartikel da, ist `typeSale` aus (kein leerer Reiter).
35
+ * Eine unbrauchbare Etikett-Vorlage schliesst nichts: sie wird `null`, die
36
+ * Kasse druckt dann ihren Standard (ein Verkauf scheitert nie an der Vorlage).
37
+ * Das alte Feld `label` (Kassen vor 1.17, Ruling R3) liest das Paket nicht.
19
38
  */
20
39
  export function posVoucherSettingsFromWire(block) {
21
- const zu = () => ({ ...VOUCHER_SETTINGS_DEFAULTS });
40
+ const zu = () => ({ ...VOUCHER_SETTINGS_DEFAULTS, types: [] });
22
41
  if (block === null || typeof block !== 'object' || Array.isArray(block))
23
42
  return zu();
24
43
  const b = block;
@@ -37,7 +56,92 @@ export function posVoucherSettingsFromWire(block) {
37
56
  return zu();
38
57
  if (art === 'multi_purpose' && satz !== null)
39
58
  return zu();
40
- return { sale, valueVoucherType: art, vatRateBp: satz };
59
+ const sortenVerkauf = b['typeSale'] ?? false;
60
+ const rohSorten = b['types'] ?? [];
61
+ const leistung = b['serviceArticles'] ?? false;
62
+ if (typeof sortenVerkauf !== 'boolean' || typeof leistung !== 'boolean' || !Array.isArray(rohSorten))
63
+ return zu();
64
+ const types = [];
65
+ const gesehen = new Set();
66
+ for (const roh of rohSorten) {
67
+ const t = sorteVomDraht(roh);
68
+ if (t === null || gesehen.has(t.id))
69
+ continue;
70
+ gesehen.add(t.id);
71
+ types.push(t);
72
+ }
73
+ return {
74
+ sale, valueVoucherType: art, vatRateBp: satz,
75
+ typeSale: sortenVerkauf && (types.length > 0 || leistung),
76
+ types,
77
+ serviceArticles: leistung,
78
+ labelDefault: vorlageVomDraht(b['labelDefault']),
79
+ };
80
+ }
81
+ /** Kennung wie am Server (gutschein-core.ID_MUSTER): reservierte `__…__` gibt es nie als Sorte. */
82
+ const SORTE_ID = /^(?!__.*__$)[A-Za-z0-9_-]{1,128}$/;
83
+ /** Grenzen des Servers (gutschein-core.pruefeSorte): Fixbetrag 1,00 bis 10.000,00 €, Gueltigkeit 1 bis 30 Jahre. */
84
+ const MIN_FIX_CENTS = 100;
85
+ const MAX_BETRAG_CENTS = 1_000_000;
86
+ const MAX_JAHRE = 30;
87
+ const istObjekt = (w) => w !== null && typeof w === 'object' && !Array.isArray(w);
88
+ const ganzeZahl = (w, min, max = Number.MAX_SAFE_INTEGER) => typeof w === 'number' && Number.isSafeInteger(w) && w >= min && w <= max;
89
+ /** Etikett-Vorlage als Kopie, wenn sie Groesse und Layout als Text traegt; sonst `null`. */
90
+ function vorlageVomDraht(w) {
91
+ if (!istObjekt(w) || typeof w['size'] !== 'string' || typeof w['layout'] !== 'string')
92
+ return null;
93
+ return JSON.parse(JSON.stringify(w));
94
+ }
95
+ /**
96
+ * Eine Sorte aus `vouchers.types[]`, oder `null`, wenn sie nicht so ist, wie
97
+ * der Server eine verkaufbare Sorte liefert (dann bietet die Kasse sie nicht
98
+ * an): Kennung, Name, Art mit passendem Satz, Betrag, Gueltigkeit, Auszahlung
99
+ * (`never` nur unbefristet) und aktiv. Fehlende Reihenfolge oder Verkaufszahl
100
+ * gelten als 0; eine unbrauchbare Vorlage wird `null`.
101
+ */
102
+ function sorteVomDraht(w) {
103
+ if (!istObjekt(w))
104
+ return null;
105
+ const { id, name, kind, amount, validity, payout } = w;
106
+ const satz = w['vatRateBp'] ?? null;
107
+ const reihenfolge = w['order'] ?? 0;
108
+ const verkauft = w['soldCount'] ?? 0;
109
+ if (typeof id !== 'string' || !SORTE_ID.test(id))
110
+ return null;
111
+ if (typeof name !== 'string' || name.trim() === '')
112
+ return null;
113
+ if (kind !== 'multi_purpose' && kind !== 'single_purpose')
114
+ return null;
115
+ if (kind === 'single_purpose' ? !ganzeZahl(satz, 1) : satz !== null)
116
+ return null;
117
+ if (!istObjekt(amount) || !istObjekt(validity))
118
+ return null;
119
+ let betrag;
120
+ if (amount['mode'] === 'free')
121
+ betrag = { mode: 'free' };
122
+ else if (amount['mode'] === 'fixed' && ganzeZahl(amount['cents'], MIN_FIX_CENTS, MAX_BETRAG_CENTS))
123
+ betrag = { mode: 'fixed', cents: amount['cents'] };
124
+ else
125
+ return null;
126
+ let gueltig;
127
+ if (validity['pattern'] === 'unlimited')
128
+ gueltig = { pattern: 'unlimited' };
129
+ else if (validity['pattern'] === 'years' && ganzeZahl(validity['years'], 1, MAX_JAHRE))
130
+ gueltig = { pattern: 'years', years: validity['years'] };
131
+ else
132
+ return null;
133
+ if (payout !== 'allowed' && payout !== 'never')
134
+ return null;
135
+ if (payout === 'never' && gueltig.pattern !== 'unlimited')
136
+ return null;
137
+ if (!ganzeZahl(reihenfolge, Number.MIN_SAFE_INTEGER) || !ganzeZahl(verkauft, 0))
138
+ return null;
139
+ if (w['active'] !== true)
140
+ return null;
141
+ return {
142
+ id, name, kind, vatRateBp: satz, amount: betrag, validity: gueltig, payout,
143
+ label: vorlageVomDraht(w['label']), order: reihenfolge, active: true, soldCount: verkauft,
144
+ };
41
145
  }
42
146
  function fehler(name, grund) {
43
147
  return new KasseneckValidationError(name, `Antwort: ${grund}`, 'response');
@@ -71,6 +175,12 @@ function wahl(name, pfad, o, feld, erlaubt) {
71
175
  function gueltigkeit(name, w) {
72
176
  const o = objekt(name, 'validity', w);
73
177
  const raus = { pattern: wahl(name, 'validity', o, 'pattern', VOUCHER_VALIDITY_PATTERNS) };
178
+ if (!fehlt(o.redeemableYears)) {
179
+ const jahre = o.redeemableYears;
180
+ if (typeof jahre !== 'number' || !Number.isSafeInteger(jahre) || jahre < 1)
181
+ throw fehler(name, 'validity.redeemableYears ist keine Zahl von Jahren');
182
+ raus.redeemableYears = jahre;
183
+ }
74
184
  for (const feld of ['redeemableUntil', 'refundableUntil', 'validUntil']) {
75
185
  const t = o[feld];
76
186
  if (fehlt(t))
@@ -111,6 +221,16 @@ function leseCheck(name, w) {
111
221
  throw fehler(name, 'data.exempt ist kein Wahrheitswert');
112
222
  raus.exempt = o.exempt;
113
223
  }
224
+ if (!fehlt(o.typeName)) {
225
+ if (typeof o.typeName !== 'string' || o.typeName === '')
226
+ throw fehler(name, 'data.typeName ist kein Text');
227
+ raus.typeName = o.typeName;
228
+ }
229
+ // Geschlossen im Zweifel, ohne Wurf: ein unbekannter Wert (neuerer Server)
230
+ // gilt als `never`. Einloesen haengt nicht daran und darf nicht an einem
231
+ // neuen Wert scheitern (die Lehre aus dem Gueltigkeitsmuster, Nachtrag E1).
232
+ if (!fehlt(o.payout))
233
+ raus.payout = o.payout === 'allowed' ? 'allowed' : 'never';
114
234
  return raus;
115
235
  }
116
236
  /**
@@ -18,7 +18,7 @@ export { STOCK_LOCATION_TYPES, type StockLocationType, type StockLocation, type
18
18
  export { type ListMyStocktakesOptions, type ListMyStocktakeItemsOptions, type ListMyStocktakeCountsOptions, type RecordMyStocktakeCountOptions, type VoidMyStocktakeCountOptions, listMyStocktakes, listMyStocktakeItems, listMyStocktakeCounts, recordMyStocktakeCount, voidMyStocktakeCount, parseQuantityMilli, } from './inventur.js';
19
19
  export type { Stocktake, StocktakeItem, StocktakeCount, StocktakeCountResult, StocktakeItemPage, StocktakeCountPage, StocktakeActor, StocktakeProgress, StocktakeReview, StocktakeRecount, StocktakeScope, } from '../inventory/typen.js';
20
20
  export type { StocktakeStatus } from '../inventory/vertrag.js';
21
- export { VOUCHER_KINDS, VOUCHER_STATUSES, checkVoucher, reprintVoucherLabel, VOUCHER_SETTINGS_DEFAULTS, posVoucherSettingsFromWire, type VoucherKind, type VoucherStatus, type VoucherValidity, type VoucherArticle, type VoucherCheck, type PosVoucherSettings, } from './gutschein.js';
21
+ export { VOUCHER_KINDS, VOUCHER_STATUSES, checkVoucher, reprintVoucherLabel, VOUCHER_SETTINGS_DEFAULTS, posVoucherSettingsFromWire, VOUCHER_PAYOUTS, VOUCHER_TYPE_KINDS, VOUCHER_AMOUNT_MODES, VOUCHER_TYPE_VALIDITY_PATTERNS, type VoucherKind, type VoucherStatus, type VoucherValidity, type VoucherArticle, type VoucherCheck, type PosVoucherSettings, type VoucherPayout, type VoucherTypeKind, type PosVoucherType, type VoucherLabelTemplate, } from './gutschein.js';
22
22
  export { type GetMyDrawerStateOptions, type OpenMyShiftOptions, type RecordMyCashMovementOptions, type DrawerOpeningEntry, type LogMyDrawerOpeningsOptions, type GetMyShiftReportOptions, type TipPayout, type CloseMyShiftOptions, type AfterCountOption, getMyDrawerState, openMyShift, recordMyCashMovement, logMyDrawerOpenings, getMyShiftReport, closeMyShift, sumDenominations, parseCentsAllowZero, newIdempotencyKey, shiftTipLines, durationText, type ShiftTipLine, type ShiftTipLineKey, } from './schicht.js';
23
23
  export { EUR_DENOMINATIONS, MOVEMENT_TYPES, MANUAL_MOVEMENT_TYPES, CASH_LOCATIONS, OPENING_REASONS, MANUAL_OPENING_REASONS, SHIFT_RECEIPT_KINDS, AFTER_COUNT_TYPES, type AfterCountType, type AfterCount, type EurDenomination, type MovementType, type ManualMovementType, type CashLocation, type OpeningReason, type ManualOpeningReason, type ShiftReceiptKind, type ShiftStatus, type ShiftActorRole, type Denominations, type ShiftActor, type DrawerInfo, type DrawerRights, type NonCashAmount, type ShiftTipTotals, type ShiftTip, type ShiftRunningTotals, type ShiftReconciliation, type Shift, type TipClaim, type DrawerOpening, type DrawerState, type CashMovement, type ShiftReceiptHeader, type ShiftReceiptMovement, type ShiftReceiptTip, type ShiftSummaryReceipt, type CashMovementReceipt, type ShiftReceipt, type OpenShiftResult, type CashMovementResult, type DrawerOpeningsResult, type ShiftReport, type CloseShiftResult, } from './schicht-typen.js';
24
24
  export { type ShiftReceiptLayoutOptions, shiftReceiptLayout, cashMovementLayout } from '../receipt/shift-sheet.js';
@@ -20,8 +20,8 @@ export { PRINT_JOB_STATUSES, PRINT_JOB_SOURCES, isPrintJobFinished, listMyPrinte
20
20
  export { listMyTipRecipients } from './trinkgeld.js';
21
21
  export { STOCK_LOCATION_TYPES, listMyStockLocations, listMyStock, setMyCashregisterStockLocation, } from './lager.js';
22
22
  export { listMyStocktakes, listMyStocktakeItems, listMyStocktakeCounts, recordMyStocktakeCount, voidMyStocktakeCount, parseQuantityMilli, } from './inventur.js';
23
- // Gutschein pruefen und Etikett nachdrucken an der Kasse.
24
- export { VOUCHER_KINDS, VOUCHER_STATUSES, checkVoucher, reprintVoucherLabel, VOUCHER_SETTINGS_DEFAULTS, posVoucherSettingsFromWire, } from './gutschein.js';
23
+ // Gutschein pruefen und Etikett nachdrucken an der Kasse; Gutschein-Sorten (Stufe 4b, 1.17.0).
24
+ export { VOUCHER_KINDS, VOUCHER_STATUSES, checkVoucher, reprintVoucherLabel, VOUCHER_SETTINGS_DEFAULTS, posVoucherSettingsFromWire, VOUCHER_PAYOUTS, VOUCHER_TYPE_KINDS, VOUCHER_AMOUNT_MODES, VOUCHER_TYPE_VALIDITY_PATTERNS, } from './gutschein.js';
25
25
  // Schicht, Kassenstand und Lade-Protokoll (1.9.0): Aufrufe, Kataloge, Helfer, Bons.
26
26
  export { getMyDrawerState, openMyShift, recordMyCashMovement, logMyDrawerOpenings, getMyShiftReport, closeMyShift, sumDenominations, parseCentsAllowZero, newIdempotencyKey, shiftTipLines, durationText, } from './schicht.js';
27
27
  export { EUR_DENOMINATIONS, MOVEMENT_TYPES, MANUAL_MOVEMENT_TYPES, CASH_LOCATIONS, OPENING_REASONS, MANUAL_OPENING_REASONS, SHIFT_RECEIPT_KINDS, AFTER_COUNT_TYPES, } from './schicht-typen.js';
@@ -371,6 +371,24 @@ declare const MELDUNGEN_ROH: {
371
371
  readonly 'voucher_error.sale_off': {
372
372
  readonly text: "Gutscheine verkaufen geht hier noch nicht.";
373
373
  };
374
+ readonly 'voucher_error.type_required': {
375
+ readonly text: "Bitte eine Gutschein-Sorte wählen.";
376
+ };
377
+ readonly 'voucher_error.type_not_found': {
378
+ readonly text: "Diese Gutschein-Sorte gibt es nicht mehr. Die Kasse lädt die Sorten neu.";
379
+ };
380
+ readonly 'voucher_error.amount_mismatch': {
381
+ readonly text: "Die Gutschein-Sorte hat sich geändert. Die Kasse lädt die Sorten neu.";
382
+ };
383
+ readonly 'voucher_error.service_not_allowed': {
384
+ readonly text: "Dieser Artikel ist nicht als Leistungsgutschein freigegeben.";
385
+ };
386
+ readonly 'voucher_error.payout_not_allowed': {
387
+ readonly text: "Dieser Gutschein kann nicht ausgezahlt werden.";
388
+ };
389
+ readonly 'voucher.type_changed': {
390
+ readonly text: "Diese Gutschein-Sorte hat sich geändert. Bitte die Zeile entfernen und die Kachel neu antippen.";
391
+ };
374
392
  readonly 'voucher.reentry_needed': {
375
393
  readonly text: "Die Karte ist belastet, der Beleg fehlt noch: zu jedem Gutschein den Code erneut eingeben, dann den Beleg abschließen.";
376
394
  };
@@ -1048,6 +1066,11 @@ export declare const VOUCHER_ERROR_MESSAGES: {
1048
1066
  readonly voucher_partial_cancel: "voucher_error.partial_cancel";
1049
1067
  readonly voucher_settings_missing: "voucher_error.settings_missing";
1050
1068
  readonly too_many_voucher_checks: "voucher_error.too_many_checks";
1069
+ readonly voucher_type_required: "voucher_error.type_required";
1070
+ readonly voucher_type_not_found: "voucher_error.type_not_found";
1071
+ readonly voucher_amount_mismatch: "voucher_error.amount_mismatch";
1072
+ readonly voucher_service_not_allowed: "voucher_error.service_not_allowed";
1073
+ readonly voucher_payout_not_allowed: "voucher_error.payout_not_allowed";
1051
1074
  };
1052
1075
  export type VoucherMessageCode = keyof typeof VOUCHER_ERROR_MESSAGES;
1053
1076
  /** Der Satz zu einem Gutschein-`code`; `undefined` für jeden anderen Code. */
@@ -1896,6 +1919,22 @@ declare const BESCHRIFTUNGEN_ROH: {
1896
1919
  readonly 'voucher.add': {
1897
1920
  readonly text: "Hinzufügen";
1898
1921
  };
1922
+ readonly 'voucher.tab': {
1923
+ readonly text: "Gutscheine";
1924
+ };
1925
+ readonly 'voucher.enter_amount': {
1926
+ readonly text: "Betrag eingeben";
1927
+ };
1928
+ readonly 'voucher.service_tile': {
1929
+ readonly text: "Leistungsgutschein";
1930
+ };
1931
+ readonly 'voucher.kind_multi_short': {
1932
+ readonly text: "Mehrzweck";
1933
+ };
1934
+ readonly 'voucher.kind_single_short': {
1935
+ readonly text: "Einzweck {rate} %";
1936
+ readonly placeholders: readonly ["rate"];
1937
+ };
1899
1938
  readonly 'shift.all_payments': {
1900
1939
  readonly text: "Alle Zahlungen dieser Schicht (bar + unbar)";
1901
1940
  };
@@ -195,6 +195,16 @@ const MELDUNGEN_ROH = {
195
195
  'voucher_error.settings_missing': { text: 'Die Gutschein-Einstellungen fehlen. Bitte die Kasse neu laden.' },
196
196
  'voucher_error.too_many_checks': { text: 'Zu viele Versuche. Bitte in einer Stunde wieder.' },
197
197
  'voucher_error.sale_off': { text: 'Gutscheine verkaufen geht hier noch nicht.' },
198
+ // Gutschein-Sorten (Stufe 4b, 1.17.0): Verkauf nur ueber eine Sorte; bei den beiden Satzpaaren laedt die Kasse die Sorten neu.
199
+ 'voucher_error.type_required': { text: 'Bitte eine Gutschein-Sorte wählen.' },
200
+ 'voucher_error.type_not_found': { text: 'Diese Gutschein-Sorte gibt es nicht mehr. Die Kasse lädt die Sorten neu.' },
201
+ 'voucher_error.amount_mismatch': { text: 'Die Gutschein-Sorte hat sich geändert. Die Kasse lädt die Sorten neu.' },
202
+ 'voucher_error.service_not_allowed': { text: 'Dieser Artikel ist nicht als Leistungsgutschein freigegeben.' },
203
+ 'voucher_error.payout_not_allowed': { text: 'Dieser Gutschein kann nicht ausgezahlt werden.' },
204
+ // Satz der Kasse selbst (1.17.0): eine Verkaufszeile im Korb passt nach dem Neuladen der Sorten nicht mehr (Sorte
205
+ // geaendert, deaktiviert oder geloescht). Die Zeile sperrt das Kassieren, bis sie entfernt und neu gewaehlt ist;
206
+ // der Satz nach einer Absage des Servers bleibt voucher_error.type_not_found bzw. voucher_error.amount_mismatch.
207
+ 'voucher.type_changed': { text: 'Diese Gutschein-Sorte hat sich geändert. Bitte die Zeile entfernen und die Kachel neu antippen.' },
198
208
  // Gutscheine an der Kasse, Stufe 3 (1.13.0): Sätze der Kasse selbst (nicht des Servers).
199
209
  'voucher.reentry_needed': { text: 'Die Karte ist belastet, der Beleg fehlt noch: zu jedem Gutschein den Code erneut eingeben, dann den Beleg abschließen.' },
200
210
  'voucher.reentry_mismatch': { text: 'Mit diesen Gutscheinen ergibt der Beleg nicht mehr den belasteten Betrag – die Karte zurückbuchen.' },
@@ -599,6 +609,11 @@ export const VOUCHER_ERROR_MESSAGES = {
599
609
  voucher_partial_cancel: 'voucher_error.partial_cancel',
600
610
  voucher_settings_missing: 'voucher_error.settings_missing',
601
611
  too_many_voucher_checks: 'voucher_error.too_many_checks',
612
+ voucher_type_required: 'voucher_error.type_required',
613
+ voucher_type_not_found: 'voucher_error.type_not_found',
614
+ voucher_amount_mismatch: 'voucher_error.amount_mismatch',
615
+ voucher_service_not_allowed: 'voucher_error.service_not_allowed',
616
+ voucher_payout_not_allowed: 'voucher_error.payout_not_allowed',
602
617
  };
603
618
  /** Der Satz zu einem Gutschein-`code`; `undefined` für jeden anderen Code. */
604
619
  export function voucherErrorMessage(code) {
@@ -940,6 +955,13 @@ const BESCHRIFTUNGEN_ROH = {
940
955
  'voucher.code_done': { text: 'Fertig' },
941
956
  'voucher.article': { text: 'Artikel' },
942
957
  'voucher.add': { text: 'Hinzufügen' },
958
+ // Gutschein-Sorten als Kacheln im Reiter „Gutscheine“ (Stufe 4b, 1.17.0): Kachel je Sorte mit Betrag
959
+ // oder „Betrag eingeben“, darunter klein die Art; dazu die feste Kachel „Leistungsgutschein“.
960
+ 'voucher.tab': { text: 'Gutscheine' },
961
+ 'voucher.enter_amount': { text: 'Betrag eingeben' },
962
+ 'voucher.service_tile': { text: 'Leistungsgutschein' },
963
+ 'voucher.kind_multi_short': { text: 'Mehrzweck' },
964
+ 'voucher.kind_single_short': { text: 'Einzweck {rate} %', placeholders: ['rate'] },
943
965
  'shift.all_payments': { text: 'Alle Zahlungen dieser Schicht (bar + unbar)' },
944
966
  'shift.receipt_count_one': { text: '1 Beleg' },
945
967
  'shift.receipt_count_other': { text: '{count} Belege', placeholders: ['count'] },
@@ -2,8 +2,8 @@ import { ReceiptType, type ReceiptTypeKey, KeckPaymentMethod } from '../enums/in
2
2
  import type { ReceiptItem } from '../models/receipt-item.js';
3
3
  import type { Voucher } from '../models/voucher.js';
4
4
  import type { PosVoucherSettings, VoucherCheck } from '../pos/gutschein.js';
5
- import { type VoucherDueArticle, type VoucherDueInfo } from './gutschein-plan.js';
6
- export { VOUCHER_ZERO_RATE_BUCKET_FROM, type VoucherDueArticle, type VoucherDueInfo } from './gutschein-plan.js';
5
+ import { type VoucherDueArticle, type VoucherDueInfo, type VoucherDueType } from './gutschein-plan.js';
6
+ export { VOUCHER_ZERO_RATE_BUCKET_FROM, type VoucherDueArticle, type VoucherDueInfo, type VoucherDueType } from './gutschein-plan.js';
7
7
  /**
8
8
  * Zahlbetrag eines Belegs in ganzen Cent: Zwilling von `createReceipt` im
9
9
  * Backend (Nachtrag §8), also `beleg-toepfe.belegToepfe(...).zaehlerDeltaCents`
@@ -42,8 +42,8 @@ export { VOUCHER_ZERO_RATE_BUCKET_FROM, type VoucherDueArticle, type VoucherDueI
42
42
  * (`gutschein-beleg.plane`, Zwilling in `gutschein-plan.ts`). Was er dafuer
43
43
  * aus seinen Daten liest, gibt die Kasse mit: je Einloesung/Auszahlung die
44
44
  * Pruefung aus `checkVoucher` (`voucherInfo`, ueber den Code zugeordnet), fuer
45
- * einen Einzweck-Verkauf die Gutschein-Einstellung der Kasse
46
- * (`voucherSettings`, Satz), fuer einen Leistungsgutschein-Verkauf den Artikel
45
+ * einen Wertgutschein-Verkauf die Gutschein-Sorten (`voucherTypes`, Art, Satz,
46
+ * Betrag; seit 1.17.0), fuer einen Leistungsgutschein-Verkauf den Artikel
47
47
  * (`voucherArticles`, Preis und Satz) und `smallBusiness` fuer ein
48
48
  * Kleinunternehmer-Konto. Was fehlt oder was der Server abwiese, wirft
49
49
  * `invalid_voucher` mit dem erwarteten Server-Code in `voucherErrorCode`.
@@ -86,11 +86,22 @@ export interface ReceiptDueOptions {
86
86
  */
87
87
  voucherInfo?: readonly VoucherDueInfo[];
88
88
  /**
89
- * Gutschein-Einstellung der Kasse (`getKasseSettings`, Block `vouchers`):
90
- * Pflicht fuer jeden Verkauf eines Mehrzweck- oder Einzweckgutscheins (der
91
- * Server verkauft nur mit Einstellung). Fehlt sie oder ist keine Art
92
- * gewaehlt: `voucher_settings_missing`; eine andere Art als die
93
- * eingestellte: `voucher_invalid`.
89
+ * Gutschein-Sorten der Kasse (`getKasseSettings`, `vouchers.types`, oder
90
+ * je Verkauf die gewaehlte): ein Wertgutschein wird nur ueber eine Sorte
91
+ * verkauft (`typeId` am Wunsch), Art und Satz kommen aus ihr. Fehlt die
92
+ * Sorte des Wunschs hier: `voucher_type_not_found` (wie beim Server fuer
93
+ * eine unbekannte oder inaktive); ein anderer Fixbetrag oder eine andere
94
+ * Art am Wunsch (die Sorte wurde umgestellt): `voucher_amount_mismatch`.
95
+ */
96
+ voucherTypes?: readonly VoucherDueType[];
97
+ /**
98
+ * Alte Gutschein-Einstellung der Kasse (bis 1.16, Block `vouchers` ohne
99
+ * Sorten). Nur noch fuer einen Wert-Verkauf ohne `typeId`: nennt sie eine
100
+ * Art (`valueVoucherType`), rechnet der Zahlbetrag wie bisher (eine andere
101
+ * Art: `voucher_invalid`); ohne sie oder ohne Art – so liest sich der Block
102
+ * jedes Servers ab Gutscheine Stufe 4b – `voucher_type_required`. Der
103
+ * Server verkauft seitdem nur ueber eine Sorte, und `createReceipt` sendet
104
+ * einen Wert-Verkauf ohne `typeId` nicht mehr.
94
105
  */
95
106
  voucherSettings?: PosVoucherSettings;
96
107
  /**
@@ -83,6 +83,7 @@ function rechne(items, vouchers, receiptType, options) {
83
83
  const plan = planeGutscheine(positionen, vouchers, art, {
84
84
  voucherInfo: options.voucherInfo,
85
85
  voucherSettings: options.voucherSettings,
86
+ voucherTypes: options.voucherTypes,
86
87
  voucherArticles: options.voucherArticles,
87
88
  smallBusiness: options.smallBusiness,
88
89
  zeroRateBucketFrom: options.zeroRateBucketFrom,
@@ -1,5 +1,5 @@
1
1
  import type { Voucher } from '../models/voucher.js';
2
- import type { PosVoucherSettings, VoucherArticle, VoucherCheck } from '../pos/gutschein.js';
2
+ import type { PosVoucherSettings, PosVoucherType, VoucherArticle, VoucherCheck } from '../pos/gutschein.js';
3
3
  /**
4
4
  * Gutscheine der neuen Form im Zahlbetrag: Zwilling von
5
5
  * `gutschein-beleg.plane` im Backend, soweit er den Zahlbetrag bestimmt.
@@ -14,10 +14,16 @@ import type { PosVoucherSettings, VoucherArticle, VoucherCheck } from '../pos/gu
14
14
  * - Mehrzweck einloesen: Eintrag `value`, mindert den Zahlbetrag als Fluss.
15
15
  * - Mehrzweck verkaufen: Eintrag `value` (+Fluss); ab dem Stichtag des
16
16
  * 0-%-Topfs stattdessen Position im 0-%-Satz ohne Fluss.
17
- * - Mehrzweck auszahlen: immer Position im 0-%-Satz (minus), kein Fluss.
18
- * - Einzweck: Position im Satz (Verkauf aus der Einstellung der Kasse,
19
- * Einloesung und Auszahlung aus der Pruefung des Gutscheins); als
20
- * Kleinunternehmer eingeloest anteilig auf alle Saetze des Korbs.
17
+ * - Mehrzweck auszahlen: immer Position im 0-%-Satz (minus), kein Fluss;
18
+ * nie bei einem Gutschein mit Auszahlung `never`.
19
+ * - Einzweck: Position im Satz (Verkauf aus der Sorte, Einloesung und
20
+ * Auszahlung aus der Pruefung des Gutscheins); als Kleinunternehmer
21
+ * eingeloest anteilig auf alle Saetze des Korbs.
22
+ * - Wertgutschein verkaufen (Stufe 4b): nur ueber eine Sorte (`typeId`); Art
23
+ * und Satz kommen aus der Sorte, ein Fixbetrag muss stimmen. Ohne Sorte
24
+ * rechnet nur noch ein Aufrufer mit einer alten Gutschein-Einstellung mit
25
+ * Art (`voucherSettings.valueVoucherType`) wie bisher; sonst und beim
26
+ * Server immer `voucher_type_required`.
21
27
  * - Leistung verkaufen: Stueck mal Artikelpreis im Artikelsatz; einloesen: je
22
28
  * Satz eine Minuszeile ueber die gewaehlten Positionen.
23
29
  * - Aktion: Rabatt wie `promo`, Betrag aus der Pruefung, hoechstens die Summe.
@@ -50,8 +56,17 @@ export interface VoucherDueInfo {
50
56
  code: string;
51
57
  check: VoucherCheck;
52
58
  }
53
- /** Artikel fuer den Verkauf eines Dienstleistungsgutscheins: Preis und Satz wie im Artikelstamm. */
54
- export type VoucherDueArticle = Pick<VoucherArticle, 'id' | 'priceCents' | 'vatRateBp'>;
59
+ /**
60
+ * Artikel fuer den Verkauf eines Dienstleistungsgutscheins: Preis und Satz wie
61
+ * im Artikelstamm. `serviceVoucher` (`PosArticle.serviceVoucher`): bei `false`
62
+ * verkauft der Server ihn nicht (`voucher_service_not_allowed`); ohne Angabe
63
+ * rechnet der Zahlbetrag wie bisher.
64
+ */
65
+ export type VoucherDueArticle = Pick<VoucherArticle, 'id' | 'priceCents' | 'vatRateBp'> & {
66
+ serviceVoucher?: boolean;
67
+ };
68
+ /** Gutschein-Sorte fuer den Verkauf: Kennung, Art, Satz und Betrag wie in `getKasseSettings` (`vouchers.types[]`). */
69
+ export type VoucherDueType = Pick<PosVoucherType, 'id' | 'kind' | 'vatRateBp' | 'amount'>;
55
70
  /**
56
71
  * Stichtag des 0-%-Topfs fuer Mehrzweck-Verkaeufe (Backend
57
72
  * `gutschein-core.NULLTOPF_AB`). Solange er in der Zukunft liegt, zaehlt ein
@@ -62,6 +77,7 @@ export declare const VOUCHER_ZERO_RATE_BUCKET_FROM = "2099-12-31";
62
77
  export interface PlanOptionen {
63
78
  voucherInfo?: readonly VoucherDueInfo[];
64
79
  voucherSettings?: PosVoucherSettings;
80
+ voucherTypes?: readonly VoucherDueType[];
65
81
  voucherArticles?: readonly VoucherDueArticle[];
66
82
  smallBusiness?: boolean;
67
83
  zeroRateBucketFrom?: string;