@kreiseck/kasseneck-api 1.0.0-rc.2 → 1.0.0-rc.3

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.
package/CHANGELOG.md CHANGED
@@ -126,6 +126,27 @@ an upgrade as long as `/v1` is served.
126
126
  Reason: an app that read `'rejected'` could charge or refund a card twice;
127
127
  a silent resend below the package does the same without any error. Same
128
128
  change as in the Dart twin `kasseneck_api` (no `RetryClient`).
129
+ - **Findings of the Dart twin (1.0.0-rc.3).** `listMyCashregisters` reads
130
+ the start receipt under `onboarding.start_receipt_created`/`_transmitted`
131
+ (`_at`) as `/v3` sends it; rc.1 and rc.2 read the internal
132
+ `startbeleg_*` names and reported every register as having no start
133
+ receipt (`CashregisterOnboardingPayload` changes accordingly). The fields
134
+ of `CashregisterOnboarding` are English now as well: `startbelegCreated`
135
+ is `startReceiptCreated`, `startbelegTransmitted` is
136
+ `startReceiptTransmitted`, `startbelegCreatedAt` is `startReceiptCreatedAt`,
137
+ `startbelegTransmittedAt` is `startReceiptTransmittedAt`; the German-name
138
+ guard no longer exempts `startbeleg`.
139
+ `sendReceiptEmail` reads its success reply leniently: without `to` it
140
+ returns the address you sent, without `at` it returns `at: null`
141
+ (`SendReceiptEmailResult.at` is `string | null`); throwing there would
142
+ invite a second email. `listMyPrinters` without `data.printers` and
143
+ `createPrintJob`/`getPrintJob` without `data.jobId` throw a
144
+ `KasseneckValidationError` (`scope: 'response'`) instead of returning an
145
+ empty list or an empty id: "no printer yet" must not look like a broken
146
+ reply, and a job without an id cannot be polled, so the cashier would print
147
+ again. `setMyRegisterDeviceSettings` takes `shortcuts` only as the whole map
148
+ of known actions (as `posSettingsChanges` produces it): the server checks
149
+ keys bound twice only within the map it receives.
129
150
  - **Strict validation before sending.** The 0.x payment fields
130
151
  (`paymentMethod`, `paymentMethodFromServer`, `creditCardProvider`,
131
152
  `cardPaymentId`, `cardPaymentData`) throw, also from plain JavaScript.
@@ -252,7 +273,7 @@ texts are English: `storno.ergebnis_unklar` is `cancellation.outcome_unknown`,
252
273
  `tax.intra_community_supply.title`. Every part is lower case with underscores,
253
274
  only country codes stay upper case (`country.AT`). Placeholders are English
254
275
  too: `{betrag}` is `{amount}`, `{grund}` is `{reason}`, `{sekunden}` is
255
- `{seconds}`, `{uid}` is `{vatId}`, and so on for 23 of 30 names; pass the
276
+ `{seconds}`, `{uid}` is `{vatId}`, and so on for 25 of 30 names; pass the
256
277
  values under the new names (`messageText('checkout.locked', { reason })`). An
257
278
  old name throws as a missing value. `ERROR_RULES` entries use
258
279
  `kind`/`behavior`/`key` with the kinds `api`, `plain_text`, `timeout`,
package/README.md CHANGED
@@ -615,10 +615,14 @@ const confirmation = await api.sendReceiptEmail({
615
615
  language: 'de', // optional; the backend currently only uses 'de'
616
616
  });
617
617
  confirmation.to; // address as the backend logged it (trimmed, lower case)
618
- confirmation.at; // time, ISO with Vienna offset
618
+ confirmation.at; // time, ISO with Vienna offset, or null
619
619
  confirmation.via; // 'own' | 'platform' | 'platform_fallback' | null
620
620
  ```
621
621
 
622
+ A success response means the email has gone out. If it lacks `to` or `at`,
623
+ the call still succeeds: `to` falls back to the address you sent and `at` is
624
+ `null`. Throwing there would invite sending the email a second time.
625
+
622
626
  The email contains a **link to the public receipt page**, not a PDF
623
627
  attachment: the receipt page uses the same line model as screen and printed
624
628
  receipt and offers a PDF there. The receipt itself stays untouched (BAO § 131,
@@ -315,10 +315,17 @@ export interface SendReceiptEmailOptions {
315
315
  }
316
316
  /** Was der Versand bestaetigt (Backend: beleg-mail-endpoints.js). */
317
317
  export interface SendReceiptEmailResult {
318
- /** Adresse in der Form, in der das Backend sie protokolliert hat (getrimmt, klein). */
318
+ /**
319
+ * Adresse in der Form, in der das Backend sie protokolliert hat (getrimmt,
320
+ * klein). Nennt die Antwort keine, die gesendete (getrimmte) Adresse.
321
+ */
319
322
  to: string;
320
- /** Zeitpunkt des Versands, ISO mit Wiener Zonenoffset (`2026-09-11T14:05:00+02:00`). */
321
- at: string;
323
+ /**
324
+ * Zeitpunkt des Versands, ISO mit Wiener Zonenoffset
325
+ * (`2026-09-11T14:05:00+02:00`). `null`, wenn die Antwort keinen nennt: der
326
+ * Versand ist trotzdem bestaetigt.
327
+ */
328
+ at: string | null;
322
329
  /**
323
330
  * Versandweg: `own` (Postfach des Betriebs), `platform` oder
324
331
  * `platform_fallback` ([RECEIPT_EMAIL_VIAS]). `null`, wenn die Antwort ihn
@@ -395,14 +395,17 @@ async function sendReceiptEmail(transport, options) {
395
395
  if (options.language !== undefined && options.language !== '')
396
396
  params.language = options.language;
397
397
  const daten = await transport('sendReceiptEmail', params);
398
- if (typeof daten?.to !== 'string' || daten.to === '') {
399
- throw antwortfehler('sendReceiptEmail', 'Antwort nennt keine Empfaengeradresse (data.to fehlt)');
400
- }
401
- if (typeof daten.at !== 'string' || daten.at === '') {
402
- throw antwortfehler('sendReceiptEmail', 'Antwort nennt keinen Zeitpunkt (data.at fehlt)');
403
- }
404
- const via = index_js_2.RECEIPT_EMAIL_VIAS.includes(daten.via) ? daten.via : null;
405
- return { to: daten.to, at: daten.at, via };
398
+ // Nachsichtig gelesen (wie der Dart-Zwilling): eine Erfolgsantwort heisst,
399
+ // die Mail ist schon verschickt. Ein Fehler wegen eines fehlenden
400
+ // Antwortfelds luede zum zweiten Versand ein; also zurueck, was da ist.
401
+ const gemeldet = daten?.to;
402
+ const zeit = daten?.at;
403
+ const via = index_js_2.RECEIPT_EMAIL_VIAS.includes(daten?.via) ? daten.via : null;
404
+ return {
405
+ to: typeof gemeldet === 'string' && gemeldet.trim() !== '' ? gemeldet : to,
406
+ at: typeof zeit === 'string' && zeit !== '' ? zeit : null,
407
+ via,
408
+ };
406
409
  }
407
410
  /**
408
411
  * Prueft die Gutschein-Kombination eines Belegs und liefert den ersten
@@ -45,11 +45,11 @@ export interface Cashregister {
45
45
  */
46
46
  export interface CashregisterOnboarding {
47
47
  cashboxRegistered: boolean;
48
- startbelegCreated: boolean;
49
- startbelegTransmitted: boolean;
48
+ startReceiptCreated: boolean;
49
+ startReceiptTransmitted: boolean;
50
50
  cashboxRegisteredAt?: Date;
51
- startbelegCreatedAt?: Date;
52
- startbelegTransmittedAt?: Date;
51
+ startReceiptCreatedAt?: Date;
52
+ startReceiptTransmittedAt?: Date;
53
53
  }
54
54
  /** Nutzlast-Form, die dieses Paket liest — die Feldnamen von `listMyCashregisters`. */
55
55
  export interface CashregisterPayload {
@@ -61,13 +61,19 @@ export interface CashregisterPayload {
61
61
  token?: string | null;
62
62
  onboarding?: CashregisterOnboardingPayload | null;
63
63
  }
64
+ /**
65
+ * `onboarding` am Draht `/v3` (fixtures/v3/antworten/kasse.json): der
66
+ * Startbeleg heisst dort `start_receipt_*`. Die inneren Namen
67
+ * `startbeleg_*` sendet nur `/v1`; wer sie hier laese, meldete jede Kasse
68
+ * als „Startbeleg fehlt“.
69
+ */
64
70
  export interface CashregisterOnboardingPayload {
65
71
  cashbox_registered?: boolean | null;
66
- startbeleg_created?: boolean | null;
67
- startbeleg_transmitted?: boolean | null;
72
+ start_receipt_created?: boolean | null;
73
+ start_receipt_transmitted?: boolean | null;
68
74
  cashbox_registered_at?: string | null;
69
- startbeleg_created_at?: string | null;
70
- startbeleg_transmitted_at?: string | null;
75
+ start_receipt_created_at?: string | null;
76
+ start_receipt_transmitted_at?: string | null;
71
77
  }
72
78
  /**
73
79
  * Liest eine Kasse aus der Antwort. `id` gewinnt aus der Nutzlast, faellt aber
@@ -18,11 +18,11 @@ function fromCashregisterPayload(payload, id) {
18
18
  ...(payload.signature_id ? { signatureId: payload.signature_id } : {}),
19
19
  onboarding: {
20
20
  cashboxRegistered: ob.cashbox_registered === true,
21
- startbelegCreated: ob.startbeleg_created === true,
22
- startbelegTransmitted: ob.startbeleg_transmitted === true,
21
+ startReceiptCreated: ob.start_receipt_created === true,
22
+ startReceiptTransmitted: ob.start_receipt_transmitted === true,
23
23
  ...zeitfeld('cashboxRegisteredAt', ob.cashbox_registered_at),
24
- ...zeitfeld('startbelegCreatedAt', ob.startbeleg_created_at),
25
- ...zeitfeld('startbelegTransmittedAt', ob.startbeleg_transmitted_at),
24
+ ...zeitfeld('startReceiptCreatedAt', ob.start_receipt_created_at),
25
+ ...zeitfeld('startReceiptTransmittedAt', ob.start_receipt_transmitted_at),
26
26
  },
27
27
  };
28
28
  }
@@ -11,7 +11,12 @@ export declare function posSettingsFromWire(data: {
11
11
  } | null | undefined): PosSettings;
12
12
  /** Betriebsweite Einstellungen schreiben (Recht `layout`); liefert den gemischten Stand. */
13
13
  export declare function setMyPosSettings(transport: InternerTransport, business: Partial<PosBusinessSettings>): Promise<PosBusinessSettings>;
14
- /** Geraete-Einstellungen schreiben (Recht `layout`); liefert den gemischten Stand. */
14
+ /**
15
+ * Geraete-Einstellungen schreiben (Recht `layout`); liefert den gemischten
16
+ * Stand. `shortcuts` nur als ganze Karte aller bekannten Aktionen
17
+ * ([posSettingsChanges] liefert sie bei jeder Tastenaenderung), sonst wirft
18
+ * der Aufruf vor dem Senden.
19
+ */
15
20
  export declare function setMyRegisterDeviceSettings(transport: InternerTransport, deviceId: string, device: Partial<PosDeviceSettings>): Promise<PosDeviceSettings>;
16
21
  /** Eingabe von [setMyPosLogo]: ein Bild als Data-URL (PNG, JPEG, SVG) oder das Entfernen. */
17
22
  export type SetMyPosLogoOptions = {
@@ -57,7 +57,12 @@ async function setMyPosSettings(transport, business) {
57
57
  const daten = await transport(name, { business });
58
58
  return (0, settings_js_1.mergePosSettings)(settings_js_1.POS_BUSINESS_DEFAULTS, objekt(daten?.business));
59
59
  }
60
- /** Geraete-Einstellungen schreiben (Recht `layout`); liefert den gemischten Stand. */
60
+ /**
61
+ * Geraete-Einstellungen schreiben (Recht `layout`); liefert den gemischten
62
+ * Stand. `shortcuts` nur als ganze Karte aller bekannten Aktionen
63
+ * ([posSettingsChanges] liefert sie bei jeder Tastenaenderung), sonst wirft
64
+ * der Aufruf vor dem Senden.
65
+ */
61
66
  async function setMyRegisterDeviceSettings(transport, deviceId, device) {
62
67
  const name = 'setMyRegisterDeviceSettings';
63
68
  if (typeof deviceId !== 'string' || deviceId.trim() === '') {
@@ -77,8 +82,15 @@ async function setMyRegisterDeviceSettings(transport, deviceId, device) {
77
82
  throw new errors_js_1.KasseneckValidationError(name, `device.shortcuts.${aktion}: unbekannte Aktion`, 'request');
78
83
  }
79
84
  }
80
- // Doppelbelegung innerhalb der gesendeten Karte; posSettingsChanges sendet
81
- // bei jeder Tastenaenderung die ganze Karte, so ist das die ganze Belegung.
85
+ // Nur die ganze Karte: der Server prueft Doppelbelegungen nur in der
86
+ // gesendeten Karte, eine halbe (`{cash: ['Mod+K']}`) liesse eine
87
+ // Doppelbelegung mit einer gespeicherten Taste durch. posSettingsChanges
88
+ // liefert bei jeder Tastenaenderung die ganze Karte.
89
+ const fehlt = settings_js_1.POS_SHORTCUT_ACTIONS.filter((a) => karte[a] === undefined);
90
+ if (fehlt.length > 0) {
91
+ throw new errors_js_1.KasseneckValidationError(name, `device.shortcuts: ganze Karte senden (posSettingsChanges), es fehlen ${fehlt.join(', ')}`, 'request');
92
+ }
93
+ // Doppelbelegung in der ganzen Karte, also in der ganzen Belegung.
82
94
  const konflikt = (0, settings_js_1.posShortcutConflict)(karte);
83
95
  if (konflikt) {
84
96
  throw new errors_js_1.KasseneckValidationError(name, `device.shortcuts.${konflikt.action}: Taste ${konflikt.key} schon belegt (${konflikt.heldBy})`, 'request');
@@ -5,6 +5,7 @@ exports.isPrintJobFinished = isPrintJobFinished;
5
5
  exports.listMyPrinters = listMyPrinters;
6
6
  exports.createPrintJob = createPrintJob;
7
7
  exports.getPrintJob = getPrintJob;
8
+ const errors_js_1 = require("../client/errors.js");
8
9
  const index_js_1 = require("../printing/index.js");
9
10
  /** Stand eines Druckjobs (Katalog `DRUCKJOB`). */
10
11
  exports.PRINT_JOB_STATUSES = ['pending', 'sent', 'printed', 'failed', 'expired'];
@@ -29,9 +30,27 @@ const STATUS = new Set(exports.PRINT_JOB_STATUSES);
29
30
  function status(v) {
30
31
  return typeof v === 'string' && STATUS.has(v) ? v : 'unknown';
31
32
  }
33
+ /**
34
+ * Die Kennung eines Jobs aus der Antwort. Ohne sie kann niemand den Job
35
+ * abfragen; ein leerer Text waere ein angeblich angelegter Job, und der
36
+ * Kassier druckte ein zweites Mal.
37
+ */
38
+ function jobKennung(name, d) {
39
+ const id = d?.jobId;
40
+ if (typeof id !== 'string' || id === '') {
41
+ throw new errors_js_1.KasseneckValidationError(name, 'Antwort enthaelt keine Kennung (data.jobId fehlt)', 'response');
42
+ }
43
+ return id;
44
+ }
32
45
  async function listMyPrinters(transport) {
33
46
  const daten = await transport('listMyPrinters', {});
34
- return (Array.isArray(daten?.printers) ? daten.printers : []).map((r) => {
47
+ const liste = daten?.printers;
48
+ if (!Array.isArray(liste)) {
49
+ // Keine Liste ist etwas anderes als eine leere Liste: „noch kein Drucker“
50
+ // darf nicht aussehen wie „Antwort kaputt“.
51
+ throw new errors_js_1.KasseneckValidationError('listMyPrinters', 'Antwort enthaelt keine Liste (data.printers fehlt)', 'response');
52
+ }
53
+ return liste.map((r) => {
35
54
  const d = (r ?? {});
36
55
  const e = d.lastResult && typeof d.lastResult === 'object' ? d.lastResult : null;
37
56
  return {
@@ -60,7 +79,7 @@ async function createPrintJob(transport, o) {
60
79
  if (o.brandMark === true)
61
80
  params.brand = true;
62
81
  const daten = await transport('createPrintJob', params);
63
- return { jobId: String(daten?.jobId ?? ''), status: status(daten?.status), result: null };
82
+ return { jobId: jobKennung('createPrintJob', daten), status: status(daten?.status), result: null };
64
83
  }
65
84
  /**
66
85
  * Stand eines Druckjobs abfragen. Der Aufrufer fragt, bis [isPrintJobFinished]
@@ -71,7 +90,7 @@ async function getPrintJob(transport, o) {
71
90
  const d = await transport('getPrintJob', { printerId: o.printerId, jobId: o.jobId });
72
91
  const e = d?.result && typeof d.result === 'object' ? d.result : null;
73
92
  return {
74
- jobId: String(d?.jobId ?? o.jobId), status: status(d?.status),
93
+ jobId: jobKennung('getPrintJob', d), status: status(d?.status),
75
94
  createdAt: zahl(d?.createdAt), sentAt: zahl(d?.sentAt),
76
95
  result: e ? { success: e.success === true, code: text(e.code), status: text(e.status), at: zahl(e.at) ?? undefined } : null,
77
96
  };
@@ -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.2";
7
+ export declare const PACKAGE_VERSION = "1.0.0-rc.3";
@@ -7,4 +7,4 @@ exports.PACKAGE_VERSION = void 0;
7
7
  * Client-Version, Nachtrag §6). Ein Test haelt sie gleich mit `package.json`;
8
8
  * beim Anheben der Version beide Stellen aendern.
9
9
  */
10
- exports.PACKAGE_VERSION = '1.0.0-rc.2';
10
+ exports.PACKAGE_VERSION = '1.0.0-rc.3';
@@ -315,10 +315,17 @@ export interface SendReceiptEmailOptions {
315
315
  }
316
316
  /** Was der Versand bestaetigt (Backend: beleg-mail-endpoints.js). */
317
317
  export interface SendReceiptEmailResult {
318
- /** Adresse in der Form, in der das Backend sie protokolliert hat (getrimmt, klein). */
318
+ /**
319
+ * Adresse in der Form, in der das Backend sie protokolliert hat (getrimmt,
320
+ * klein). Nennt die Antwort keine, die gesendete (getrimmte) Adresse.
321
+ */
319
322
  to: string;
320
- /** Zeitpunkt des Versands, ISO mit Wiener Zonenoffset (`2026-09-11T14:05:00+02:00`). */
321
- at: string;
323
+ /**
324
+ * Zeitpunkt des Versands, ISO mit Wiener Zonenoffset
325
+ * (`2026-09-11T14:05:00+02:00`). `null`, wenn die Antwort keinen nennt: der
326
+ * Versand ist trotzdem bestaetigt.
327
+ */
328
+ at: string | null;
322
329
  /**
323
330
  * Versandweg: `own` (Postfach des Betriebs), `platform` oder
324
331
  * `platform_fallback` ([RECEIPT_EMAIL_VIAS]). `null`, wenn die Antwort ihn
@@ -376,14 +376,17 @@ export async function sendReceiptEmail(transport, options) {
376
376
  if (options.language !== undefined && options.language !== '')
377
377
  params.language = options.language;
378
378
  const daten = await transport('sendReceiptEmail', params);
379
- if (typeof daten?.to !== 'string' || daten.to === '') {
380
- throw antwortfehler('sendReceiptEmail', 'Antwort nennt keine Empfaengeradresse (data.to fehlt)');
381
- }
382
- if (typeof daten.at !== 'string' || daten.at === '') {
383
- throw antwortfehler('sendReceiptEmail', 'Antwort nennt keinen Zeitpunkt (data.at fehlt)');
384
- }
385
- const via = RECEIPT_EMAIL_VIAS.includes(daten.via) ? daten.via : null;
386
- return { to: daten.to, at: daten.at, via };
379
+ // Nachsichtig gelesen (wie der Dart-Zwilling): eine Erfolgsantwort heisst,
380
+ // die Mail ist schon verschickt. Ein Fehler wegen eines fehlenden
381
+ // Antwortfelds luede zum zweiten Versand ein; also zurueck, was da ist.
382
+ const gemeldet = daten?.to;
383
+ const zeit = daten?.at;
384
+ const via = RECEIPT_EMAIL_VIAS.includes(daten?.via) ? daten.via : null;
385
+ return {
386
+ to: typeof gemeldet === 'string' && gemeldet.trim() !== '' ? gemeldet : to,
387
+ at: typeof zeit === 'string' && zeit !== '' ? zeit : null,
388
+ via,
389
+ };
387
390
  }
388
391
  /**
389
392
  * Prueft die Gutschein-Kombination eines Belegs und liefert den ersten
@@ -45,11 +45,11 @@ export interface Cashregister {
45
45
  */
46
46
  export interface CashregisterOnboarding {
47
47
  cashboxRegistered: boolean;
48
- startbelegCreated: boolean;
49
- startbelegTransmitted: boolean;
48
+ startReceiptCreated: boolean;
49
+ startReceiptTransmitted: boolean;
50
50
  cashboxRegisteredAt?: Date;
51
- startbelegCreatedAt?: Date;
52
- startbelegTransmittedAt?: Date;
51
+ startReceiptCreatedAt?: Date;
52
+ startReceiptTransmittedAt?: Date;
53
53
  }
54
54
  /** Nutzlast-Form, die dieses Paket liest — die Feldnamen von `listMyCashregisters`. */
55
55
  export interface CashregisterPayload {
@@ -61,13 +61,19 @@ export interface CashregisterPayload {
61
61
  token?: string | null;
62
62
  onboarding?: CashregisterOnboardingPayload | null;
63
63
  }
64
+ /**
65
+ * `onboarding` am Draht `/v3` (fixtures/v3/antworten/kasse.json): der
66
+ * Startbeleg heisst dort `start_receipt_*`. Die inneren Namen
67
+ * `startbeleg_*` sendet nur `/v1`; wer sie hier laese, meldete jede Kasse
68
+ * als „Startbeleg fehlt“.
69
+ */
64
70
  export interface CashregisterOnboardingPayload {
65
71
  cashbox_registered?: boolean | null;
66
- startbeleg_created?: boolean | null;
67
- startbeleg_transmitted?: boolean | null;
72
+ start_receipt_created?: boolean | null;
73
+ start_receipt_transmitted?: boolean | null;
68
74
  cashbox_registered_at?: string | null;
69
- startbeleg_created_at?: string | null;
70
- startbeleg_transmitted_at?: string | null;
75
+ start_receipt_created_at?: string | null;
76
+ start_receipt_transmitted_at?: string | null;
71
77
  }
72
78
  /**
73
79
  * Liest eine Kasse aus der Antwort. `id` gewinnt aus der Nutzlast, faellt aber
@@ -15,11 +15,11 @@ export function fromCashregisterPayload(payload, id) {
15
15
  ...(payload.signature_id ? { signatureId: payload.signature_id } : {}),
16
16
  onboarding: {
17
17
  cashboxRegistered: ob.cashbox_registered === true,
18
- startbelegCreated: ob.startbeleg_created === true,
19
- startbelegTransmitted: ob.startbeleg_transmitted === true,
18
+ startReceiptCreated: ob.start_receipt_created === true,
19
+ startReceiptTransmitted: ob.start_receipt_transmitted === true,
20
20
  ...zeitfeld('cashboxRegisteredAt', ob.cashbox_registered_at),
21
- ...zeitfeld('startbelegCreatedAt', ob.startbeleg_created_at),
22
- ...zeitfeld('startbelegTransmittedAt', ob.startbeleg_transmitted_at),
21
+ ...zeitfeld('startReceiptCreatedAt', ob.start_receipt_created_at),
22
+ ...zeitfeld('startReceiptTransmittedAt', ob.start_receipt_transmitted_at),
23
23
  },
24
24
  };
25
25
  }
@@ -11,7 +11,12 @@ export declare function posSettingsFromWire(data: {
11
11
  } | null | undefined): PosSettings;
12
12
  /** Betriebsweite Einstellungen schreiben (Recht `layout`); liefert den gemischten Stand. */
13
13
  export declare function setMyPosSettings(transport: InternerTransport, business: Partial<PosBusinessSettings>): Promise<PosBusinessSettings>;
14
- /** Geraete-Einstellungen schreiben (Recht `layout`); liefert den gemischten Stand. */
14
+ /**
15
+ * Geraete-Einstellungen schreiben (Recht `layout`); liefert den gemischten
16
+ * Stand. `shortcuts` nur als ganze Karte aller bekannten Aktionen
17
+ * ([posSettingsChanges] liefert sie bei jeder Tastenaenderung), sonst wirft
18
+ * der Aufruf vor dem Senden.
19
+ */
15
20
  export declare function setMyRegisterDeviceSettings(transport: InternerTransport, deviceId: string, device: Partial<PosDeviceSettings>): Promise<PosDeviceSettings>;
16
21
  /** Eingabe von [setMyPosLogo]: ein Bild als Data-URL (PNG, JPEG, SVG) oder das Entfernen. */
17
22
  export type SetMyPosLogoOptions = {
@@ -50,7 +50,12 @@ export async function setMyPosSettings(transport, business) {
50
50
  const daten = await transport(name, { business });
51
51
  return mergePosSettings(POS_BUSINESS_DEFAULTS, objekt(daten?.business));
52
52
  }
53
- /** Geraete-Einstellungen schreiben (Recht `layout`); liefert den gemischten Stand. */
53
+ /**
54
+ * Geraete-Einstellungen schreiben (Recht `layout`); liefert den gemischten
55
+ * Stand. `shortcuts` nur als ganze Karte aller bekannten Aktionen
56
+ * ([posSettingsChanges] liefert sie bei jeder Tastenaenderung), sonst wirft
57
+ * der Aufruf vor dem Senden.
58
+ */
54
59
  export async function setMyRegisterDeviceSettings(transport, deviceId, device) {
55
60
  const name = 'setMyRegisterDeviceSettings';
56
61
  if (typeof deviceId !== 'string' || deviceId.trim() === '') {
@@ -70,8 +75,15 @@ export async function setMyRegisterDeviceSettings(transport, deviceId, device) {
70
75
  throw new KasseneckValidationError(name, `device.shortcuts.${aktion}: unbekannte Aktion`, 'request');
71
76
  }
72
77
  }
73
- // Doppelbelegung innerhalb der gesendeten Karte; posSettingsChanges sendet
74
- // bei jeder Tastenaenderung die ganze Karte, so ist das die ganze Belegung.
78
+ // Nur die ganze Karte: der Server prueft Doppelbelegungen nur in der
79
+ // gesendeten Karte, eine halbe (`{cash: ['Mod+K']}`) liesse eine
80
+ // Doppelbelegung mit einer gespeicherten Taste durch. posSettingsChanges
81
+ // liefert bei jeder Tastenaenderung die ganze Karte.
82
+ const fehlt = POS_SHORTCUT_ACTIONS.filter((a) => karte[a] === undefined);
83
+ if (fehlt.length > 0) {
84
+ throw new KasseneckValidationError(name, `device.shortcuts: ganze Karte senden (posSettingsChanges), es fehlen ${fehlt.join(', ')}`, 'request');
85
+ }
86
+ // Doppelbelegung in der ganzen Karte, also in der ganzen Belegung.
75
87
  const konflikt = posShortcutConflict(karte);
76
88
  if (konflikt) {
77
89
  throw new KasseneckValidationError(name, `device.shortcuts.${konflikt.action}: Taste ${konflikt.key} schon belegt (${konflikt.heldBy})`, 'request');
@@ -1,3 +1,4 @@
1
+ import { KasseneckValidationError } from '../client/errors.js';
1
2
  import { rasterRowsBase64 } from '../printing/index.js';
2
3
  /** Stand eines Druckjobs (Katalog `DRUCKJOB`). */
3
4
  export const PRINT_JOB_STATUSES = ['pending', 'sent', 'printed', 'failed', 'expired'];
@@ -22,9 +23,27 @@ const STATUS = new Set(PRINT_JOB_STATUSES);
22
23
  function status(v) {
23
24
  return typeof v === 'string' && STATUS.has(v) ? v : 'unknown';
24
25
  }
26
+ /**
27
+ * Die Kennung eines Jobs aus der Antwort. Ohne sie kann niemand den Job
28
+ * abfragen; ein leerer Text waere ein angeblich angelegter Job, und der
29
+ * Kassier druckte ein zweites Mal.
30
+ */
31
+ function jobKennung(name, d) {
32
+ const id = d?.jobId;
33
+ if (typeof id !== 'string' || id === '') {
34
+ throw new KasseneckValidationError(name, 'Antwort enthaelt keine Kennung (data.jobId fehlt)', 'response');
35
+ }
36
+ return id;
37
+ }
25
38
  export async function listMyPrinters(transport) {
26
39
  const daten = await transport('listMyPrinters', {});
27
- return (Array.isArray(daten?.printers) ? daten.printers : []).map((r) => {
40
+ const liste = daten?.printers;
41
+ if (!Array.isArray(liste)) {
42
+ // Keine Liste ist etwas anderes als eine leere Liste: „noch kein Drucker“
43
+ // darf nicht aussehen wie „Antwort kaputt“.
44
+ throw new KasseneckValidationError('listMyPrinters', 'Antwort enthaelt keine Liste (data.printers fehlt)', 'response');
45
+ }
46
+ return liste.map((r) => {
28
47
  const d = (r ?? {});
29
48
  const e = d.lastResult && typeof d.lastResult === 'object' ? d.lastResult : null;
30
49
  return {
@@ -53,7 +72,7 @@ export async function createPrintJob(transport, o) {
53
72
  if (o.brandMark === true)
54
73
  params.brand = true;
55
74
  const daten = await transport('createPrintJob', params);
56
- return { jobId: String(daten?.jobId ?? ''), status: status(daten?.status), result: null };
75
+ return { jobId: jobKennung('createPrintJob', daten), status: status(daten?.status), result: null };
57
76
  }
58
77
  /**
59
78
  * Stand eines Druckjobs abfragen. Der Aufrufer fragt, bis [isPrintJobFinished]
@@ -64,7 +83,7 @@ export async function getPrintJob(transport, o) {
64
83
  const d = await transport('getPrintJob', { printerId: o.printerId, jobId: o.jobId });
65
84
  const e = d?.result && typeof d.result === 'object' ? d.result : null;
66
85
  return {
67
- jobId: String(d?.jobId ?? o.jobId), status: status(d?.status),
86
+ jobId: jobKennung('getPrintJob', d), status: status(d?.status),
68
87
  createdAt: zahl(d?.createdAt), sentAt: zahl(d?.sentAt),
69
88
  result: e ? { success: e.success === true, code: text(e.code), status: text(e.status), at: zahl(e.at) ?? undefined } : null,
70
89
  };
@@ -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.2";
7
+ export declare const PACKAGE_VERSION = "1.0.0-rc.3";
@@ -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.2';
7
+ export const PACKAGE_VERSION = '1.0.0-rc.3';
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.0.0-rc.2",
2
+ "version": "1.0.0-rc.3",
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.2",
5
+ "package": "1.0.0-rc.3",
6
6
  "endpoints": {
7
7
  "createCustomer": {
8
8
  "request": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.0.0-rc.2",
2
+ "version": "1.0.0-rc.3",
3
3
  "languages": [
4
4
  "de",
5
5
  "en"
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.0.0-rc.2",
2
+ "version": "1.0.0-rc.3",
3
3
  "messages": {
4
4
  "network.no_connection": {
5
5
  "text": "Keine Verbindung zum Server. Bitte die Internetverbindung prüfen und erneut versuchen."
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.0.0-rc.2",
2
+ "version": "1.0.0-rc.3",
3
3
  "baseUrls": {
4
4
  "public": "https://api.kasseneck.at/v3",
5
5
  "pos": "https://kasse.kasseneck.at/api/v3"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kreiseck/kasseneck-api",
3
- "version": "1.0.0-rc.2",
3
+ "version": "1.0.0-rc.3",
4
4
  "description": "Austrian fiscal cash register (RKSV) client for JavaScript and TypeScript: signed receipts, cancellations, card payments, receipt printing (ESC/POS), invoices. RKSV-Registrierkasse für JavaScript und TypeScript.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Kreiseck",