@kreiseck/kasseneck-api 0.7.0 → 0.7.2

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 (122) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +87 -15
  3. package/dist/cjs/client/aufrufe.d.ts +1 -1
  4. package/dist/cjs/client/aufrufe.js +1 -0
  5. package/dist/cjs/client/errors.d.ts +8 -4
  6. package/dist/cjs/client/errors.js +12 -9
  7. package/dist/cjs/client/receipts.d.ts +22 -0
  8. package/dist/cjs/client/receipts.js +16 -0
  9. package/dist/cjs/client/transport.js +5 -4
  10. package/dist/cjs/index.d.ts +1 -1
  11. package/dist/cjs/index.js +4 -2
  12. package/dist/cjs/models/cancellation.d.ts +28 -0
  13. package/dist/cjs/models/cancellation.js +31 -1
  14. package/dist/cjs/models/hobex-receipt.js +9 -2
  15. package/dist/cjs/models/index.d.ts +1 -1
  16. package/dist/cjs/models/index.js +3 -1
  17. package/dist/cjs/models/receipt-summary.d.ts +2 -0
  18. package/dist/cjs/models/receipt-summary.js +4 -1
  19. package/dist/cjs/models/receipt.js +8 -1
  20. package/dist/cjs/models/voucher.js +18 -2
  21. package/dist/cjs/payments/hobex-hps/connect-client.d.ts +104 -0
  22. package/dist/cjs/payments/hobex-hps/connect-client.js +159 -0
  23. package/dist/cjs/payments/hobex-hps/errors.d.ts +122 -0
  24. package/dist/cjs/payments/hobex-hps/errors.js +158 -0
  25. package/dist/cjs/payments/hobex-hps/events.d.ts +22 -0
  26. package/dist/cjs/payments/hobex-hps/events.js +2 -0
  27. package/dist/cjs/payments/hobex-hps/index.d.ts +27 -0
  28. package/dist/cjs/payments/hobex-hps/index.js +67 -0
  29. package/dist/cjs/payments/hobex-hps/outcome.d.ts +46 -0
  30. package/dist/cjs/payments/hobex-hps/outcome.js +7 -0
  31. package/dist/cjs/payments/hobex-hps/payments.d.ts +149 -0
  32. package/dist/cjs/payments/hobex-hps/payments.js +573 -0
  33. package/dist/cjs/payments/hobex-hps/receipt.d.ts +28 -0
  34. package/dist/cjs/payments/hobex-hps/receipt.js +60 -0
  35. package/dist/cjs/payments/hobex-hps/transaction-id.d.ts +65 -0
  36. package/dist/cjs/payments/hobex-hps/transaction-id.js +91 -0
  37. package/dist/cjs/payments/hobex-hps/transaction-response.d.ts +175 -0
  38. package/dist/cjs/payments/hobex-hps/transaction-response.js +266 -0
  39. package/dist/cjs/payments/hobex.js +9 -6
  40. package/dist/cjs/payments/index.d.ts +26 -14
  41. package/dist/cjs/payments/index.js +59 -15
  42. package/dist/cjs/printing/index.d.ts +1 -1
  43. package/dist/cjs/printing/index.js +2 -1
  44. package/dist/cjs/printing/webusb.d.ts +37 -7
  45. package/dist/cjs/printing/webusb.js +76 -18
  46. package/dist/cjs/receipt/epos.d.ts +18 -0
  47. package/dist/cjs/receipt/epos.js +78 -6
  48. package/dist/cjs/receipt/index.d.ts +1 -1
  49. package/dist/cjs/receipt/index.js +2 -1
  50. package/dist/cjs/receipt/layout.js +19 -0
  51. package/dist/cjs/register/index.d.ts +1 -1
  52. package/dist/cjs/register/index.js +2 -1
  53. package/dist/cjs/register/pairing.d.ts +59 -0
  54. package/dist/cjs/register/pairing.js +51 -2
  55. package/dist/esm/client/aufrufe.d.ts +1 -1
  56. package/dist/esm/client/aufrufe.js +1 -0
  57. package/dist/esm/client/errors.d.ts +8 -4
  58. package/dist/esm/client/errors.js +12 -9
  59. package/dist/esm/client/receipts.d.ts +22 -0
  60. package/dist/esm/client/receipts.js +16 -0
  61. package/dist/esm/client/transport.js +5 -4
  62. package/dist/esm/index.d.ts +1 -1
  63. package/dist/esm/index.js +1 -1
  64. package/dist/esm/models/cancellation.d.ts +28 -0
  65. package/dist/esm/models/cancellation.js +29 -0
  66. package/dist/esm/models/hobex-receipt.js +10 -3
  67. package/dist/esm/models/index.d.ts +1 -1
  68. package/dist/esm/models/index.js +1 -1
  69. package/dist/esm/models/receipt-summary.d.ts +2 -0
  70. package/dist/esm/models/receipt-summary.js +4 -1
  71. package/dist/esm/models/receipt.js +8 -1
  72. package/dist/esm/models/voucher.js +18 -2
  73. package/dist/esm/payments/hobex-hps/connect-client.d.ts +104 -0
  74. package/dist/esm/payments/hobex-hps/connect-client.js +156 -0
  75. package/dist/esm/payments/hobex-hps/errors.d.ts +122 -0
  76. package/dist/esm/payments/hobex-hps/errors.js +149 -0
  77. package/dist/esm/payments/hobex-hps/events.d.ts +22 -0
  78. package/dist/esm/payments/hobex-hps/events.js +1 -0
  79. package/dist/esm/payments/hobex-hps/index.d.ts +27 -0
  80. package/dist/esm/payments/hobex-hps/index.js +26 -0
  81. package/dist/esm/payments/hobex-hps/outcome.d.ts +46 -0
  82. package/dist/esm/payments/hobex-hps/outcome.js +4 -0
  83. package/dist/esm/payments/hobex-hps/payments.d.ts +149 -0
  84. package/dist/esm/payments/hobex-hps/payments.js +570 -0
  85. package/dist/esm/payments/hobex-hps/receipt.d.ts +28 -0
  86. package/dist/esm/payments/hobex-hps/receipt.js +57 -0
  87. package/dist/esm/payments/hobex-hps/transaction-id.d.ts +65 -0
  88. package/dist/esm/payments/hobex-hps/transaction-id.js +86 -0
  89. package/dist/esm/payments/hobex-hps/transaction-response.d.ts +175 -0
  90. package/dist/esm/payments/hobex-hps/transaction-response.js +254 -0
  91. package/dist/esm/payments/hobex.js +9 -6
  92. package/dist/esm/payments/index.d.ts +26 -14
  93. package/dist/esm/payments/index.js +26 -14
  94. package/dist/esm/printing/index.d.ts +1 -1
  95. package/dist/esm/printing/index.js +1 -1
  96. package/dist/esm/printing/webusb.d.ts +37 -7
  97. package/dist/esm/printing/webusb.js +74 -17
  98. package/dist/esm/receipt/epos.d.ts +18 -0
  99. package/dist/esm/receipt/epos.js +76 -6
  100. package/dist/esm/receipt/index.d.ts +1 -1
  101. package/dist/esm/receipt/index.js +1 -1
  102. package/dist/esm/receipt/layout.js +19 -0
  103. package/dist/esm/register/index.d.ts +1 -1
  104. package/dist/esm/register/index.js +1 -1
  105. package/dist/esm/register/pairing.d.ts +59 -0
  106. package/dist/esm/register/pairing.js +50 -2
  107. package/fixtures/belege/storno-rabatt.json +3 -2
  108. package/fixtures/belege/storno-teil.json +2 -1
  109. package/fixtures/belege/storno-voll.json +2 -1
  110. package/fixtures/erwartet/storno-rabatt.grid32.txt +1 -0
  111. package/fixtures/erwartet/storno-rabatt.grid48.txt +1 -0
  112. package/fixtures/erwartet/storno-rabatt.lines.json +6 -0
  113. package/fixtures/erwartet/storno-teil.grid32.txt +1 -0
  114. package/fixtures/erwartet/storno-teil.grid48.txt +1 -0
  115. package/fixtures/erwartet/storno-teil.lines.json +6 -0
  116. package/fixtures/erwartet/storno-voll.grid32.txt +1 -0
  117. package/fixtures/erwartet/storno-voll.grid48.txt +1 -0
  118. package/fixtures/erwartet/storno-voll.lines.json +6 -0
  119. package/fixtures/hobex-hps-codes.json +83 -0
  120. package/fixtures/manifest.json +12 -12
  121. package/fixtures/oberflaeche.json +2 -1
  122. package/package.json +3 -2
@@ -27,6 +27,14 @@ export interface RegisterClientInfo {
27
27
  w: number;
28
28
  h: number;
29
29
  };
30
+ /**
31
+ * Welcher Build der Anwendung gerade laeuft (frei, z. B. "0.6.46+3120").
32
+ * Bei einer Stoerungsmeldung die entscheidende Angabe: ohne sie laesst sich
33
+ * ein alter Build im Browser-Cache weder belegen noch ausschliessen -- und
34
+ * genau der steckt oft hinter "bei mir geht es aber". Das Backend kuerzt auf
35
+ * 80 Zeichen (register-geraetedaten.js, clientAusParams).
36
+ */
37
+ app?: string;
30
38
  }
31
39
  /** Standort aus der Browser-Ortung (freiwillig); Grundlage der Standortsperre. */
32
40
  export interface RegisterGeo {
@@ -214,6 +222,15 @@ export interface RegisterUserLoginOptions extends RegisterDeviceConnection, Regi
214
222
  * es bei der Abweisung "Kasse wird gerade auf … verwendet".
215
223
  */
216
224
  takeover?: boolean;
225
+ /**
226
+ * Welche Sitzung soll weichen? Kennung aus [listRegisterSessionsForDevice].
227
+ * Ohne Angabe verdraengt das Backend die aelteste -- so wie bisher.
228
+ *
229
+ * Eine Kennung, die nicht mehr laeuft, wird ABGEWIESEN statt still durch die
230
+ * aelteste ersetzt: Wer waehlt, bekommt seine Wahl oder eine Fehlermeldung,
231
+ * nie einen Unbeteiligten.
232
+ */
233
+ takeoverSessionId?: string;
217
234
  }
218
235
  export interface RegisterPinLoginOptions extends RegisterDeviceConnection, RegisterDeviceCredentials, RegisterGeraeteAngaben {
219
236
  /**
@@ -227,6 +244,8 @@ export interface RegisterPinLoginOptions extends RegisterDeviceConnection, Regis
227
244
  cashregisterId: string;
228
245
  /** Wie [RegisterUserLoginOptions.takeover]. */
229
246
  takeover?: boolean;
247
+ /** Wie [RegisterUserLoginOptions.takeoverSessionId]. */
248
+ takeoverSessionId?: string;
230
249
  }
231
250
  /**
232
251
  * Geraet koppeln: der Kopplungs-Code wird gegen den dauerhaften Ausweis dieses
@@ -252,6 +271,46 @@ export declare function pairRegisterDevice(options: PairRegisterDeviceOptions):
252
271
  * Geraet meldet Erfolg. Danach das Geraet lokal vergessen.
253
272
  */
254
273
  export declare function unpairRegisterDevice(options: ListRegisterUsersForDeviceOptions): Promise<void>;
274
+ /** Eine laufende Sitzung an einer Kasse -- Zeile in der Auswahl. */
275
+ export interface RegisterSession {
276
+ id: string;
277
+ deviceId: string | null;
278
+ /** Name des Geraets; „Kasse", wenn keiner gesetzt wurde. */
279
+ deviceLabel: string;
280
+ startedAt: number | null;
281
+ expiresAt: number | null;
282
+ /** Laeuft diese Sitzung auf DIESEM Geraet? */
283
+ selbst: boolean;
284
+ /**
285
+ * Wer angemeldet ist. Fehlt an einem nur-PIN-Geraet: Dort zeigt schon die
286
+ * Benutzerliste keine Namen, und was dort nicht steht, darf hier nicht
287
+ * durch die Hintertuer kommen. `undefined` heisst „wird nicht genannt",
288
+ * nicht „hat keinen Namen".
289
+ */
290
+ userName?: string;
291
+ }
292
+ /** Was eine Kasse gerade haelt -- Lizenzzahl und laufende Sitzungen. */
293
+ export interface RegisterSessionsStand {
294
+ /** Wie viele Sitzungen gleichzeitig laufen duerfen. */
295
+ licenses: number;
296
+ /** Aelteste zuerst: oben steht, was das Backend ohne Wahl verdraengen wuerde. */
297
+ sessions: RegisterSession[];
298
+ }
299
+ export interface ListRegisterSessionsForDeviceOptions extends RegisterDeviceConnection, RegisterDeviceCredentials {
300
+ }
301
+ /**
302
+ * Welche Sitzungen haelt diese Kasse gerade?
303
+ *
304
+ * Fuer den Anmeldebildschirm: Sind alle Lizenzplaetze belegt, muss eine
305
+ * weichen -- und der Kassier soll sehen, WELCHE, statt dass das Backend still
306
+ * die aelteste nimmt. Die Wahl geht dann als
307
+ * [RegisterUserLoginOptions.takeoverSessionId] mit.
308
+ *
309
+ * Ausgewiesen wird sich ueber das Geraete-Geheimnis, wie bei
310
+ * [listRegisterUsersForDevice]: An diesem Punkt gibt es weder Sitzung noch
311
+ * Token. Die Kasse bestimmt das Backend aus dem Geraet, nicht aus dem Aufruf.
312
+ */
313
+ export declare function listRegisterSessionsForDevice(options: ListRegisterSessionsForDeviceOptions): Promise<RegisterSessionsStand>;
255
314
  export declare function listRegisterUsersForDevice(options: ListRegisterUsersForDeviceOptions): Promise<RegisterDeviceUsers>;
256
315
  /**
257
316
  * Kassen-Benutzer per PIN anmelden. **Ohne Anmeldung** — dieser Aufruf
@@ -5,6 +5,7 @@ exports.cancelScopeOf = cancelScopeOf;
5
5
  exports.receiptsScopeOf = receiptsScopeOf;
6
6
  exports.pairRegisterDevice = pairRegisterDevice;
7
7
  exports.unpairRegisterDevice = unpairRegisterDevice;
8
+ exports.listRegisterSessionsForDevice = listRegisterSessionsForDevice;
8
9
  exports.listRegisterUsersForDevice = listRegisterUsersForDevice;
9
10
  exports.registerUserLogin = registerUserLogin;
10
11
  exports.registerPinLogin = registerPinLogin;
@@ -119,6 +120,48 @@ async function unpairRegisterDevice(options) {
119
120
  pflicht(name, 'deviceSecret', deviceSecret);
120
121
  await transportFuer(verbindung)(name, { ownerUid, deviceId, deviceSecret }, undefined, [deviceSecret]);
121
122
  }
123
+ /**
124
+ * Welche Sitzungen haelt diese Kasse gerade?
125
+ *
126
+ * Fuer den Anmeldebildschirm: Sind alle Lizenzplaetze belegt, muss eine
127
+ * weichen -- und der Kassier soll sehen, WELCHE, statt dass das Backend still
128
+ * die aelteste nimmt. Die Wahl geht dann als
129
+ * [RegisterUserLoginOptions.takeoverSessionId] mit.
130
+ *
131
+ * Ausgewiesen wird sich ueber das Geraete-Geheimnis, wie bei
132
+ * [listRegisterUsersForDevice]: An diesem Punkt gibt es weder Sitzung noch
133
+ * Token. Die Kasse bestimmt das Backend aus dem Geraet, nicht aus dem Aufruf.
134
+ */
135
+ async function listRegisterSessionsForDevice(options) {
136
+ const { ownerUid, deviceId, deviceSecret, ...verbindung } = options;
137
+ const name = 'listRegisterSessionsForDevice';
138
+ pflicht(name, 'ownerUid', ownerUid);
139
+ pflicht(name, 'deviceId', deviceId);
140
+ pflicht(name, 'deviceSecret', deviceSecret);
141
+ const daten = await transportFuer(verbindung)(name, { ownerUid, deviceId, deviceSecret }, undefined, [deviceSecret]);
142
+ const liste = daten?.sessions;
143
+ if (!Array.isArray(liste)) {
144
+ throw antwortfehler(name, 'Antwort enthaelt keine Sitzungsliste (data.sessions fehlt)');
145
+ }
146
+ const sessions = liste.map((eintrag) => {
147
+ const roh = (typeof eintrag === 'object' && eintrag !== null ? eintrag : {});
148
+ return {
149
+ // Ohne Kennung waere die Zeile nicht waehlbar -- eine Schaltflaeche, die
150
+ // nichts tun kann, ist schlimmer als keine.
151
+ id: pflichtfeld(name, roh, 'id'),
152
+ deviceId: typeof roh['deviceId'] === 'string' && roh['deviceId'] ? roh['deviceId'] : null,
153
+ deviceLabel: text(roh['deviceLabel']) || 'Kasse',
154
+ startedAt: typeof roh['startedAt'] === 'number' ? roh['startedAt'] : null,
155
+ expiresAt: typeof roh['expiresAt'] === 'number' ? roh['expiresAt'] : null,
156
+ selbst: roh['selbst'] === true,
157
+ // Nur uebernehmen, wenn wirklich einer kam: ein leerer String stuende
158
+ // in der Oberflaeche als namenlose Zeile, statt die Spalte wegzulassen.
159
+ ...(typeof roh['userName'] === 'string' && roh['userName'] ? { userName: roh['userName'] } : {}),
160
+ };
161
+ });
162
+ const licenses = typeof daten?.licenses === 'number' && daten.licenses > 0 ? daten.licenses : 1;
163
+ return { licenses, sessions };
164
+ }
122
165
  async function listRegisterUsersForDevice(options) {
123
166
  const { ownerUid, deviceId, deviceSecret, ...verbindung } = options;
124
167
  const name = 'listRegisterUsersForDevice';
@@ -192,7 +235,7 @@ function regel(wert) {
192
235
  * sperren gestaffelt (ab dem fuenften 30 Sekunden, ab dem neunten 15 Minuten).
193
236
  */
194
237
  async function registerUserLogin(options) {
195
- const { ownerUid, deviceId, deviceSecret, userId, pin, cashregisterId, takeover, client, geo, ...verbindung } = options;
238
+ const { ownerUid, deviceId, deviceSecret, userId, pin, cashregisterId, takeover, takeoverSessionId, client, geo, ...verbindung } = options;
196
239
  const name = 'registerUserLogin';
197
240
  pflicht(name, 'ownerUid', ownerUid);
198
241
  pflicht(name, 'deviceId', deviceId);
@@ -210,6 +253,9 @@ async function registerUserLogin(options) {
210
253
  // Nur die ausdrueckliche Uebernahme geht mit: das Backend prueft auf
211
254
  // `=== true`, und ein mitgesendetes `false` waere nur Rauschen.
212
255
  takeover: takeover === true ? true : undefined,
256
+ // Leerer String heisst „nichts gewaehlt" und gehoert nicht in die
257
+ // Nutzlast -- das Backend wiese ihn sonst als unbekannte Sitzung ab.
258
+ takeoverSessionId: takeoverSessionId ? takeoverSessionId : undefined,
213
259
  client, geo: geo ?? undefined,
214
260
  }, undefined, [pin, deviceSecret]);
215
261
  return sitzungAusAntwort(name, daten);
@@ -221,7 +267,7 @@ async function registerUserLogin(options) {
221
267
  * zaehlen und sperren dort am **Geraet**, nicht an einem Benutzer.
222
268
  */
223
269
  async function registerPinLogin(options) {
224
- const { ownerUid, deviceId, deviceSecret, pin, cashregisterId, takeover, client, geo, ...verbindung } = options;
270
+ const { ownerUid, deviceId, deviceSecret, pin, cashregisterId, takeover, takeoverSessionId, client, geo, ...verbindung } = options;
225
271
  const name = 'registerPinLogin';
226
272
  pflicht(name, 'ownerUid', ownerUid);
227
273
  pflicht(name, 'deviceId', deviceId);
@@ -236,6 +282,9 @@ async function registerPinLogin(options) {
236
282
  cashregisterId,
237
283
  // Nur die ausdrueckliche Uebernahme geht mit — wie bei registerUserLogin.
238
284
  takeover: takeover === true ? true : undefined,
285
+ // Leerer String heisst „nichts gewaehlt" und gehoert nicht in die
286
+ // Nutzlast -- das Backend wiese ihn sonst als unbekannte Sitzung ab.
287
+ takeoverSessionId: takeoverSessionId ? takeoverSessionId : undefined,
239
288
  client, geo: geo ?? undefined,
240
289
  }, undefined, [pin, deviceSecret]);
241
290
  return sitzungAusAntwort(name, daten);
@@ -13,7 +13,7 @@
13
13
  * umhuellt, muss ihn weiterhin absetzen koennen.
14
14
  */
15
15
  import type { TransportBodyFields } from './transport.js';
16
- export declare const AUFRUFE: readonly ["activateCashregister", "cancelReceipt", "createCustomerCashregister", "createPartnerCustomer", "checkPartnerCustomerEmail", "createPartnerWebhook", "createPaymentLinkStripe", "createPrintJob", "createReceipt", "deletePartnerWebhook", "downloadDailyReport", "downloadReport", "endRegisterSession", "financeWebService", "generateFullReceiptId", "getCustomerCredentials", "getCustomerSignatureStatus", "getFirstReceiptDate", "getKasseSettings", "getPartnerCustomer", "getPartnerInfo", "getPrintJob", "getReceipt", "hobexPayApi", "hobexRefundApi", "listCustomerCashregisters", "listMyArticleGroups", "listMyArticles", "listMyCashregisters", "listMyPrinters", "listMyReceipts", "listMyTipRecipients", "listPartnerCustomers", "listPartnerWebhookDeliveries", "listPartnerWebhooks", "listRegisterUsersForDevice", "pairRegisterDevice", "registerPinLogin", "registerUserLogin", "renewRegisterSession", "requestCustomerSignature", "sendPartnerCustomerFonLink", "sendPartnerWebhookTest", "setMyKasseSettings", "setMyRegisterDeviceSettings", "stripeCaptureIntent", "unpairRegisterDevice", "rotatePartnerWebhookSecret", "updatePartnerWebhook"];
16
+ export declare const AUFRUFE: readonly ["activateCashregister", "cancelReceipt", "createCustomerCashregister", "createPartnerCustomer", "checkPartnerCustomerEmail", "createPartnerWebhook", "createPaymentLinkStripe", "createPrintJob", "createReceipt", "deletePartnerWebhook", "downloadDailyReport", "downloadReport", "endRegisterSession", "financeWebService", "generateFullReceiptId", "getCustomerCredentials", "getCustomerSignatureStatus", "getFirstReceiptDate", "getKasseSettings", "getPartnerCustomer", "getPartnerInfo", "getPrintJob", "getReceipt", "hobexPayApi", "hobexRefundApi", "listCustomerCashregisters", "listMyArticleGroups", "listMyArticles", "listMyCashregisters", "listMyPrinters", "listMyReceipts", "listMyTipRecipients", "listPartnerCustomers", "listPartnerWebhookDeliveries", "listPartnerWebhooks", "listRegisterUsersForDevice", "listRegisterSessionsForDevice", "pairRegisterDevice", "registerPinLogin", "registerUserLogin", "renewRegisterSession", "requestCustomerSignature", "sendPartnerCustomerFonLink", "sendPartnerWebhookTest", "setMyKasseSettings", "setMyRegisterDeviceSettings", "stripeCaptureIntent", "unpairRegisterDevice", "rotatePartnerWebhookSecret", "updatePartnerWebhook"];
17
17
  export type Aufruf = typeof AUFRUFE[number];
18
18
  /** Wie [KasseneckTransport], nur mit bekanntem Aufrufnamen. Nicht exportiert nach aussen. */
19
19
  export type InternerTransport = <T = unknown>(functionName: Aufruf, params?: Record<string, unknown>, extraBodyFields?: TransportBodyFields, secretParams?: readonly string[]) => Promise<T>;
@@ -35,6 +35,7 @@ export const AUFRUFE = [
35
35
  'listPartnerWebhookDeliveries',
36
36
  'listPartnerWebhooks',
37
37
  'listRegisterUsersForDevice',
38
+ 'listRegisterSessionsForDevice',
38
39
  'pairRegisterDevice',
39
40
  'registerPinLogin',
40
41
  'registerUserLogin',
@@ -95,9 +95,13 @@ export declare class KasseneckApiError extends Error {
95
95
  /** Meldung des Backends, unveraendert (`message` aus der Huelle). */
96
96
  readonly serverMessage: string;
97
97
  /**
98
- * Maschinenlesbarer Fehlercode aus `data.code`, sofern die Antwort einen
99
- * fuehrt (`vertrag_offen`, `rate_limited`, …). Die aelteren Endpunkte des
100
- * Backends antworten ohne Code dann `undefined`.
98
+ * Stabiler, maschinenlesbarer Fehlercode des Backends, wenn der Endpunkt
99
+ * einen legt. Zwei Ablageorte kommen vor und beide zaehlen: das Feld `code`
100
+ * neben `message` (cancelReceipt, siehe CANCELLATION_ERROR_CODES) und
101
+ * `data.code` (Partner-API: `vertrag_offen`, `rate_limited`, …). Nur
102
+ * Bezeichner gelten — ein Freitext waere kein Code. Die aelteren Endpunkte
103
+ * antworten ohne Code — dann `undefined`. **Daran entscheiden, nie an
104
+ * [serverMessage]:** der Text darf sich aendern, der Code nicht.
101
105
  */
102
106
  readonly code: string | undefined;
103
107
  /**
@@ -106,7 +110,7 @@ export declare class KasseneckApiError extends Error {
106
110
  * beilegt. Immer ein Objekt, notfalls ein leeres.
107
111
  */
108
112
  readonly details: Record<string, unknown>;
109
- constructor(functionName: string, serverMessage: string, details?: Record<string, unknown>);
113
+ constructor(functionName: string, serverMessage: string, details?: Record<string, unknown>, code?: string);
110
114
  }
111
115
  /** Warum die Antwort keine verwertbare Huelle war. */
112
116
  export type HttpFailureReason = 'server-error' | 'empty-body' | 'not-json' | 'missing-status';
@@ -171,9 +171,13 @@ export class KasseneckApiError extends Error {
171
171
  /** Meldung des Backends, unveraendert (`message` aus der Huelle). */
172
172
  serverMessage;
173
173
  /**
174
- * Maschinenlesbarer Fehlercode aus `data.code`, sofern die Antwort einen
175
- * fuehrt (`vertrag_offen`, `rate_limited`, …). Die aelteren Endpunkte des
176
- * Backends antworten ohne Code dann `undefined`.
174
+ * Stabiler, maschinenlesbarer Fehlercode des Backends, wenn der Endpunkt
175
+ * einen legt. Zwei Ablageorte kommen vor und beide zaehlen: das Feld `code`
176
+ * neben `message` (cancelReceipt, siehe CANCELLATION_ERROR_CODES) und
177
+ * `data.code` (Partner-API: `vertrag_offen`, `rate_limited`, …). Nur
178
+ * Bezeichner gelten — ein Freitext waere kein Code. Die aelteren Endpunkte
179
+ * antworten ohne Code — dann `undefined`. **Daran entscheiden, nie an
180
+ * [serverMessage]:** der Text darf sich aendern, der Code nicht.
177
181
  */
178
182
  code;
179
183
  /**
@@ -182,16 +186,15 @@ export class KasseneckApiError extends Error {
182
186
  * beilegt. Immer ein Objekt, notfalls ein leeres.
183
187
  */
184
188
  details;
185
- constructor(functionName, serverMessage, details = {}) {
189
+ constructor(functionName, serverMessage, details = {}, code) {
186
190
  super(`${functionName} fehlgeschlagen: ${serverMessage}`);
187
191
  this.functionName = functionName;
188
192
  this.serverMessage = serverMessage;
189
193
  this.details = details;
190
- // Der Code steht in `details` und zusaetzlich als eigenes Feld: das eine
191
- // ist die Nutzlast, das andere die Frage, die ein Aufrufer wirklich
192
- // stellt. Nur Bezeichner gelten ein Freitext waere kein Code.
193
- const code = details['code'];
194
- this.code = typeof code === 'string' && BEZEICHNER.test(code) ? code : undefined;
194
+ // Der Code neben `message` hat Vorrang; fehlt er, gilt `data.code` aus
195
+ // den Details. So bleibt die Klasse fuer beide Ablageorte dieselbe.
196
+ const kandidat = code !== undefined ? code : details['code'];
197
+ this.code = typeof kandidat === 'string' && BEZEICHNER.test(kandidat) ? kandidat : undefined;
195
198
  }
196
199
  }
197
200
  const GRUND_TEXT = {
@@ -54,6 +54,22 @@ export interface TipOptions {
54
54
  paymentMethod?: KeckPaymentMethod | KeckPaymentMethodKey;
55
55
  /** Empfaenger; ohne Angabe der angemeldete Kassen-Benutzer. Summe der cents = cents. */
56
56
  recipients?: TipRecipientShare[];
57
+ /**
58
+ * Hat der Empfaenger das Geld schon? `true` = ja (Bargeld mitgenommen,
59
+ * Kartentrinkgeld sofort aus der Lade ausgezahlt), `false` = der Betrieb
60
+ * behaelt es und schuldet es.
61
+ *
62
+ * Entscheidend ist NICHT die Zahlart, sondern der Besitz -- § 2j Abs 2 AVRAG
63
+ * kennt beide Faelle. Ohne Angabe gilt die Voreinstellung des Betriebs (bar:
64
+ * schon erhalten, bargeldlos: einbehalten); diese Angabe braucht es nur fuer
65
+ * die Ausnahme.
66
+ *
67
+ * NUR mit dem Recht `tipAssign` bei persoenlicher Anmeldung: das Merkmal
68
+ * entscheidet, ob der Betrieb Geld schuldet, und gewoehnliches Personal soll
69
+ * das nicht am Geraet umstellen koennen. Ueber einen Geraete-API-Schluessel
70
+ * (ohne angemeldeten Kassen-Benutzer) gilt die Einschraenkung nicht.
71
+ */
72
+ sofortErhalten?: boolean;
57
73
  }
58
74
  export interface TipRecipientShare {
59
75
  registerUserId: string;
@@ -169,6 +185,12 @@ export declare function cancelReceipt(rufen: InternerTransport, options: CancelR
169
185
  * Stornobeleg aus frei uebergebenen Positionen — fuer den Fall, dass der
170
186
  * Originalbeleg nicht als Objekt vorliegt. Die Positionen gehen **unveraendert**
171
187
  * hinaus; das Vorzeichen setzt der Aufrufer.
188
+ *
189
+ * @deprecated Alter Storno-Weg ohne Bezug: kein Verweis auf das Original,
190
+ * keine Restmengen, kein Schutz vor doppeltem Storno, Gutscheine werden nicht
191
+ * zurueckgenommen. Das Backend nimmt ihn weiter an und legt `deprecation` in
192
+ * die Antwort. Stattdessen [cancelReceipt] (Endpunkt `cancelReceipt`): Bezug,
193
+ * Grund, Teilstorno, Fehlercodes.
172
194
  */
173
195
  export declare function createCancelReceipt(rufen: InternerTransport, options: CreateCancelReceiptOptions): Promise<Receipt>;
174
196
  /** Nullbeleg (RKSV-Pruefbeleg) — ohne Positionen und ohne Zahlungsart. */
@@ -160,6 +160,12 @@ export async function cancelReceipt(rufen, options) {
160
160
  * Stornobeleg aus frei uebergebenen Positionen — fuer den Fall, dass der
161
161
  * Originalbeleg nicht als Objekt vorliegt. Die Positionen gehen **unveraendert**
162
162
  * hinaus; das Vorzeichen setzt der Aufrufer.
163
+ *
164
+ * @deprecated Alter Storno-Weg ohne Bezug: kein Verweis auf das Original,
165
+ * keine Restmengen, kein Schutz vor doppeltem Storno, Gutscheine werden nicht
166
+ * zurueckgenommen. Das Backend nimmt ihn weiter an und legt `deprecation` in
167
+ * die Antwort. Stattdessen [cancelReceipt] (Endpunkt `cancelReceipt`): Bezug,
168
+ * Grund, Teilstorno, Fehlercodes.
163
169
  */
164
170
  export function createCancelReceipt(rufen, options) {
165
171
  return createReceipt(rufen, { ...options, receiptType: ReceiptType.cancellation });
@@ -385,6 +391,16 @@ function gepruefterTip(tip) {
385
391
  const nutzlast = { cents: tip.cents };
386
392
  if (tip.paymentMethod != null)
387
393
  nutzlast['paymentMethod'] = gepruefteZahlungsart(tip.paymentMethod);
394
+ // Nur mitschicken, wenn gesetzt: fehlt das Feld, entscheidet die
395
+ // Voreinstellung des Betriebs. Ein `false` waere dort eine Aussage, kein
396
+ // Weglassen. Der Wortlaut des Fehlers ist der des Backends
397
+ // (tip-core.normalizeTip) -- wer ihn hier sieht, sieht denselben Satz.
398
+ if (tip.sofortErhalten != null) {
399
+ if (typeof tip.sofortErhalten !== 'boolean') {
400
+ throw eingabefehler('Trinkgeld: sofortErhalten muss true oder false sein.');
401
+ }
402
+ nutzlast['sofortErhalten'] = tip.sofortErhalten;
403
+ }
388
404
  if (tip.recipients != null) {
389
405
  if (!Array.isArray(tip.recipients) || tip.recipients.length === 0) {
390
406
  throw eingabefehler('Trinkgeld: recipients darf nicht leer sein.');
@@ -176,7 +176,7 @@ function jsonAuswerten(text, functionName, statusCode, inhaltstyp, geheimnisse)
176
176
  }
177
177
  // Alles, was nicht ausdruecklich Erfolg ist, gilt als fachlicher Fehler —
178
178
  // ein unbekannter Statuswert darf nie stillschweigend als Erfolg durchgehen.
179
- throw fachfehler(functionName, huelle.message, huelle.data, geheimnisse);
179
+ throw fachfehler(functionName, huelle.message, huelle.data, geheimnisse, huelle.code);
180
180
  }
181
181
  /**
182
182
  * Auswertung des Binaerwegs. Der Kern der Zusage: **ein Aufrufer bekommt nie
@@ -222,7 +222,7 @@ function pdfAuswerten(bytes, functionName, statusCode, inhaltstyp, geheimnisse)
222
222
  }
223
223
  // Derselbe fachliche Fehler wie auf dem JSON-Weg — fuer den Aufrufer macht
224
224
  // es keinen Unterschied, ob er ein PDF oder eine Nutzlast erwartet hat.
225
- throw fachfehler(functionName, huelle.message, huelle.data, geheimnisse);
225
+ throw fachfehler(functionName, huelle.message, huelle.data, geheimnisse, huelle.code);
226
226
  }
227
227
  /** `%PDF` am Anfang — die Kennung jeder PDF-Datei. */
228
228
  function istPdf(bytes) {
@@ -235,9 +235,10 @@ function alsHuelle(wert) {
235
235
  }
236
236
  return wert;
237
237
  }
238
- function fachfehler(functionName, message, daten, geheimnisse) {
238
+ function fachfehler(functionName, message, daten, geheimnisse, code) {
239
239
  const meldung = typeof message === 'string' && message.trim() ? message : 'Unbekannter Fehler';
240
- return new KasseneckApiError(functionName, meldung, fehlerDetails(daten, geheimnisse));
240
+ // Nur ein Text zaehlt als Code -- alles andere waere ein geratener Vertrag.
241
+ return new KasseneckApiError(functionName, meldung, fehlerDetails(daten, geheimnisse), typeof code === 'string' && code ? code : undefined);
241
242
  }
242
243
  /**
243
244
  * Nutzlast aus Auth- und Aufruferparametern. Auth-Parameter bilden die
@@ -1,5 +1,5 @@
1
1
  export { ReceiptType, type ReceiptTypeKey, VatRate, type VatRateKey, KeckPaymentMethod, type KeckPaymentMethodKey, CreditCardProvider, type CreditCardProviderKey, VoucherType, type VoucherTypeKey, VoucherAction, type VoucherActionKey, StripeLinkMode, type StripeLinkModeKey, } from './enums/index.js';
2
- export { type ReceiptItem, type ReceiptItemPayload, type ReceiptItemPayloadRead, toReceiptItemPayload, fromReceiptItemPayload, receiptItemTotalCents, receiptItemIsValid, negateReceiptItem, type TipRecipient, isTipItem, type Voucher, type VoucherPayload, toVoucherPayload, fromVoucherPayload, voucherIsValid, type Receipt, type ReceiptPayload, type ReceiptPayloadRead, toReceiptPayload, fromReceiptPayload, receiptSubSumCents, receiptSumCents, type CancellationOf, type CancellationItem, type Cancellation, type CancellationReason, CANCELLATION_REASONS, CANCELLATION_RESERVATION_MS, isCancellationReason, remainingQuantities, type ReceiptCompany, type ReceiptCompanyPayload, fromReceiptCompanyPayload, receiptCompanyTaxInfo, type Cashregister, type CashregisterOnboarding, type CashregisterPayload, type CashregisterOnboardingPayload, fromCashregisterPayload, type ReceiptSummary, type ReceiptSummaryPayload, fromReceiptSummaryPayload, type ZeroKind, ZERO_KINDS, istZeroKind, type ReportMonth, reportMonthFromDate, previousReportMonth, nextReportMonth, reportMonthKey, reportMonthReadable, type StripeUrlSession, type StripeUrlSessionPayload, toStripeUrlSessionPayload, fromStripeUrlSessionPayload, type HobexReceipt, type HobexReceiptPayload, toHobexReceiptPayload, fromHobexReceiptPayload, hobexReceiptToCardPaymentData, hobexReceiptNeedsSignature, } from './models/index.js';
2
+ export { type ReceiptItem, type ReceiptItemPayload, type ReceiptItemPayloadRead, toReceiptItemPayload, fromReceiptItemPayload, receiptItemTotalCents, receiptItemIsValid, negateReceiptItem, type TipRecipient, isTipItem, type Voucher, type VoucherPayload, toVoucherPayload, fromVoucherPayload, voucherIsValid, type Receipt, type ReceiptPayload, type ReceiptPayloadRead, toReceiptPayload, fromReceiptPayload, receiptSubSumCents, receiptSumCents, type CancellationOf, type CancellationItem, type Cancellation, type CancellationReason, type CancellationErrorCode, CANCELLATION_REASONS, CANCELLATION_ERROR_CODES, CANCELLATION_RESERVATION_MS, isCancellationReason, isCancellationErrorCode, remainingQuantities, type ReceiptCompany, type ReceiptCompanyPayload, fromReceiptCompanyPayload, receiptCompanyTaxInfo, type Cashregister, type CashregisterOnboarding, type CashregisterPayload, type CashregisterOnboardingPayload, fromCashregisterPayload, type ReceiptSummary, type ReceiptSummaryPayload, fromReceiptSummaryPayload, type ZeroKind, ZERO_KINDS, istZeroKind, type ReportMonth, reportMonthFromDate, previousReportMonth, nextReportMonth, reportMonthKey, reportMonthReadable, type StripeUrlSession, type StripeUrlSessionPayload, toStripeUrlSessionPayload, fromStripeUrlSessionPayload, type HobexReceipt, type HobexReceiptPayload, toHobexReceiptPayload, fromHobexReceiptPayload, hobexReceiptToCardPaymentData, hobexReceiptNeedsSignature, } from './models/index.js';
3
3
  export { type AuthCredentials, type KasseneckAuth, type ApiKeyAuthOptions, type RegisterUserAuthOptions, apiKeyAuth, registerUserAuth, DEFAULT_BASE_URL, DEFAULT_TIMEOUT_MS, type HttpResponseLike, type HttpRequestInit, type FetchLike, type TransportOptions, type TransportBodyFields, type KasseneckTransport, type KasseneckBinaryTransport, AUFRUFE, type Aufruf, createTransport, createBinaryTransport, KasseneckApiError, KasseneckAuthError, KasseneckHttpError, KasseneckNetworkError, KasseneckValidationError, type ValidationScope, type KasseneckError, type HttpFailureReason, type CauseDigest, isKasseneckApiError, isKasseneckAuthError, isKasseneckHttpError, isKasseneckNetworkError, isKasseneckValidationError, type ReceiptCommonOptions, type SellReceiptOptions, type TipOptions, type TipRecipientShare, type CancelReceiptOptions, type CancelReceiptResult, type CreateCancelReceiptOptions, type ReceiptWithCompany, sellReceipt, sellReceiptWithCompany, cancelReceipt, createCancelReceipt, zeroReceipt, getReceipt, getReceiptWithCompany, generateFullReceiptId, getFirstReceiptDate, listMyReceipts, type ListMyReceiptsOptions, type ReceiptList, type ReceiptListStats, listMyCashregisters, checkVoucherCombinationError, downloadDailyReport, downloadMonthlyReport, type CashboxStatus, type SignatureStatus, getCashboxStatus, getSignatureStatus, type KasseneckApi, createKasseneckApi, } from './client/index.js';
4
4
  export { parseServerTimeStamp, toViennaWallClock, type ViennaWallClock } from './vienna-time.js';
5
5
  export { type KasseSettingsBetrieb, type KasseSettingsGeraet, type KasseSettings, type ArticleGroup, type KasseArtikel, KASSE_BETRIEB_STANDARD, KASSE_GERAET_STANDARD, mergeKasseSettings, fromArticleGroupPayload, fromKasseArtikelPayload, listMyArticleGroups, listMyArticles, getKasseSettings, setMyKasseSettings, setMyRegisterDeviceSettings, verteileRabatt, cancelScopeOf, receiptsScopeOf, } from './kasse/index.js';
package/dist/esm/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export { ReceiptType, VatRate, KeckPaymentMethod, CreditCardProvider, VoucherType, VoucherAction, StripeLinkMode, } from './enums/index.js';
2
- export { toReceiptItemPayload, fromReceiptItemPayload, receiptItemTotalCents, receiptItemIsValid, negateReceiptItem, isTipItem, toVoucherPayload, fromVoucherPayload, voucherIsValid, toReceiptPayload, fromReceiptPayload, receiptSubSumCents, receiptSumCents, CANCELLATION_REASONS, CANCELLATION_RESERVATION_MS, isCancellationReason, remainingQuantities, fromReceiptCompanyPayload, receiptCompanyTaxInfo, fromCashregisterPayload, fromReceiptSummaryPayload, ZERO_KINDS, istZeroKind, reportMonthFromDate, previousReportMonth, nextReportMonth, reportMonthKey, reportMonthReadable, toStripeUrlSessionPayload, fromStripeUrlSessionPayload, toHobexReceiptPayload, fromHobexReceiptPayload, hobexReceiptToCardPaymentData, hobexReceiptNeedsSignature, } from './models/index.js';
2
+ export { toReceiptItemPayload, fromReceiptItemPayload, receiptItemTotalCents, receiptItemIsValid, negateReceiptItem, isTipItem, toVoucherPayload, fromVoucherPayload, voucherIsValid, toReceiptPayload, fromReceiptPayload, receiptSubSumCents, receiptSumCents, CANCELLATION_REASONS, CANCELLATION_ERROR_CODES, CANCELLATION_RESERVATION_MS, isCancellationReason, isCancellationErrorCode, remainingQuantities, fromReceiptCompanyPayload, receiptCompanyTaxInfo, fromCashregisterPayload, fromReceiptSummaryPayload, ZERO_KINDS, istZeroKind, reportMonthFromDate, previousReportMonth, nextReportMonth, reportMonthKey, reportMonthReadable, toStripeUrlSessionPayload, fromStripeUrlSessionPayload, toHobexReceiptPayload, fromHobexReceiptPayload, hobexReceiptToCardPaymentData, hobexReceiptNeedsSignature, } from './models/index.js';
3
3
  export { apiKeyAuth, registerUserAuth, DEFAULT_BASE_URL, DEFAULT_TIMEOUT_MS, AUFRUFE, createTransport, createBinaryTransport, KasseneckApiError, KasseneckAuthError, KasseneckHttpError, KasseneckNetworkError, KasseneckValidationError, isKasseneckApiError, isKasseneckAuthError, isKasseneckHttpError, isKasseneckNetworkError, isKasseneckValidationError, sellReceipt, sellReceiptWithCompany, cancelReceipt, createCancelReceipt, zeroReceipt, getReceipt, getReceiptWithCompany, generateFullReceiptId, getFirstReceiptDate, listMyReceipts, listMyCashregisters, checkVoucherCombinationError, downloadDailyReport, downloadMonthlyReport, getCashboxStatus, getSignatureStatus, createKasseneckApi, } from './client/index.js';
4
4
  export { parseServerTimeStamp, toViennaWallClock } from './vienna-time.js';
5
5
  export { KASSE_BETRIEB_STANDARD, KASSE_GERAET_STANDARD, mergeKasseSettings, fromArticleGroupPayload, fromKasseArtikelPayload, listMyArticleGroups, listMyArticles, getKasseSettings, setMyKasseSettings, setMyRegisterDeviceSettings, verteileRabatt, cancelScopeOf, receiptsScopeOf, } from './kasse/index.js';
@@ -18,10 +18,29 @@ export declare const CANCELLATION_REASONS: Readonly<{
18
18
  }>;
19
19
  export type CancellationReason = keyof typeof CANCELLATION_REASONS;
20
20
  export declare function isCancellationReason(value: unknown): value is CancellationReason;
21
+ /**
22
+ * Stabile Fehlercodes von `cancelReceipt` — Zwilling von
23
+ * `functions/storno-core.js` STORNO_FEHLERCODES. Das Backend legt sie bei
24
+ * jedem fachlichen Fehler als `code` neben die Meldung; das Paket reicht sie
25
+ * als [KasseneckApiError.code] durch. **Entscheide am Code, nie am Text** —
26
+ * die deutsche Meldung darf sich aendern, der Code nicht.
27
+ *
28
+ * Nur Auth-/Parameterfehler (Sitzung abgelaufen, Pflichtfeld fehlt) kommen
29
+ * ohne Code; dort bleibt `code` undefined.
30
+ */
31
+ export declare const CANCELLATION_ERROR_CODES: readonly ["beleg_nicht_gefunden", "belegart_nicht_stornierbar", "trainingsbeleg", "bereits_storniert", "position_ungueltig", "menge_ueber_rest", "grund_unbekannt", "anmerkung_zu_lang", "items_ungueltig", "kasse_nicht_zugewiesen", "keine_berechtigung", "nur_eigene_belege", "kasse_unvollstaendig", "storno_fehlgeschlagen"];
32
+ export type CancellationErrorCode = (typeof CANCELLATION_ERROR_CODES)[number];
33
+ export declare function isCancellationErrorCode(value: unknown): value is CancellationErrorCode;
21
34
  /** Bezug eines Storno-Belegs auf sein Original. */
22
35
  export interface CancellationOf {
23
36
  receiptId: string;
24
37
  fullReceiptId: string | null;
38
+ /**
39
+ * Zeitstempel des Originals (Server-Format, Wiener Wanduhr) — das Layout
40
+ * nennt ihn im Kopfblock des Storno-Bons („vom 11.08.2026, 09:02 Uhr").
41
+ * Fehlt bei Altbelegen; dann bleibt die Zeile weg.
42
+ */
43
+ timeStamp?: string;
25
44
  }
26
45
  /** Eine stornierte Position: Index im Original und Menge. */
27
46
  export interface CancellationItem {
@@ -40,6 +59,15 @@ export interface Cancellation {
40
59
  by: string | null;
41
60
  note: string | null;
42
61
  items: CancellationItem[];
62
+ /**
63
+ * Rabattgutschein-Ausgleich, den DIESER Storno gewaehrt hat — Cent je
64
+ * Steuertopf des Backends (`amountRateStandard`, `amountRateReduced1`, …).
65
+ * Ein Rabattgutschein ist am Original schon in den Umsatz eingerechnet;
66
+ * jeder Storno nimmt ihn anteilig der stornierten Menge zurueck (bei 3 Stueck
67
+ * a 10 EUR mit 6 EUR Rabatt: 2,00 je Stueck). Fehlt das Feld, hat der Eintrag
68
+ * nichts gewaehrt (Altbestand vor dieser Regel) — der naechste Storno holt nach.
69
+ */
70
+ promoAdjustmentCents?: Record<string, number>;
43
71
  }
44
72
  /** Ab wann eine liegengebliebene Reservierung nicht mehr zaehlt (wie im Backend). */
45
73
  export declare const CANCELLATION_RESERVATION_MS = 120000;
@@ -18,6 +18,35 @@ export const CANCELLATION_REASONS = Object.freeze({
18
18
  export function isCancellationReason(value) {
19
19
  return typeof value === 'string' && Object.prototype.hasOwnProperty.call(CANCELLATION_REASONS, value);
20
20
  }
21
+ /**
22
+ * Stabile Fehlercodes von `cancelReceipt` — Zwilling von
23
+ * `functions/storno-core.js` STORNO_FEHLERCODES. Das Backend legt sie bei
24
+ * jedem fachlichen Fehler als `code` neben die Meldung; das Paket reicht sie
25
+ * als [KasseneckApiError.code] durch. **Entscheide am Code, nie am Text** —
26
+ * die deutsche Meldung darf sich aendern, der Code nicht.
27
+ *
28
+ * Nur Auth-/Parameterfehler (Sitzung abgelaufen, Pflichtfeld fehlt) kommen
29
+ * ohne Code; dort bleibt `code` undefined.
30
+ */
31
+ export const CANCELLATION_ERROR_CODES = [
32
+ 'beleg_nicht_gefunden', // Original fehlt oder gehoert nicht zu dieser Kasse
33
+ 'belegart_nicht_stornierbar', // Original ist selbst Storno-, Null- oder Startbeleg
34
+ 'trainingsbeleg', // Trainingsbelege werden nicht storniert
35
+ 'bereits_storniert', // keine Restmenge mehr (auch beim positionslosen Beleg)
36
+ 'position_ungueltig', // Index unbekannt oder doppelt
37
+ 'menge_ueber_rest', // Menge nicht ganzzahlig >= 1 oder groesser als der Rest
38
+ 'grund_unbekannt', // reason fehlt oder nicht im Katalog
39
+ 'anmerkung_zu_lang', // note laenger als 200 Zeichen
40
+ 'items_ungueltig', // items ist keine Liste
41
+ 'kasse_nicht_zugewiesen', // Register-Benutzer darf diese Kasse nicht
42
+ 'keine_berechtigung', // Register-Benutzer ohne Storno-Recht
43
+ 'nur_eigene_belege', // Recht "eigene", fremder Beleg
44
+ 'kasse_unvollstaendig', // api_key/token fehlen am Konto bzw. an der Kasse
45
+ 'storno_fehlgeschlagen', // der Storno-Beleg selbst wurde abgelehnt (z. B. Signatur)
46
+ ];
47
+ export function isCancellationErrorCode(value) {
48
+ return typeof value === 'string' && CANCELLATION_ERROR_CODES.includes(value);
49
+ }
21
50
  /** Ab wann eine liegengebliebene Reservierung nicht mehr zaehlt (wie im Backend). */
22
51
  export const CANCELLATION_RESERVATION_MS = 120_000;
23
52
  /**
@@ -1,5 +1,5 @@
1
1
  import { CreditCardProvider } from '../enums/index.js';
2
- import { centsToEuro } from '../money.js';
2
+ import { centsToEuro, euroToCents } from '../money.js';
3
3
  export function toHobexReceiptPayload(beleg) {
4
4
  return {
5
5
  transactionId: beleg.transactionId,
@@ -38,8 +38,15 @@ export function fromHobexReceiptPayload(payload) {
38
38
  responseCode: payload.responseCode,
39
39
  transactionType: payload.transactionType,
40
40
  currency: payload.currency,
41
- amountCents: Math.round((payload.amount ?? 0) * 100),
42
- tipCents: Math.round((payload.tip ?? 0) * 100),
41
+ // Euro->Cent ueber die eine gehaertete Stelle im Paket ([euroToCents],
42
+ // money.ts) statt einer eigenen `Math.round(x * 100)` -- die Hobex-Antwort
43
+ // ist eine fremde Antwort und wird hier nur auf transactionId/
44
+ // transactionDate geprueft (siehe payments/hobex.ts, belegAusNutzlast);
45
+ // amount/tip bleiben ungeprueft. Ein nicht-numerischer oder
46
+ // nicht-endlicher Wert ergab bis zu dieser Fassung `NaN` -- lautlos, ohne
47
+ // dass irgendwo geworfen wurde. [euroToCents] liefert dafuer `0`.
48
+ amountCents: euroToCents(payload.amount),
49
+ tipCents: euroToCents(payload.tip),
43
50
  cvm: String(payload.cvm),
44
51
  // Die Cloud-API-Nutzlast traegt keinen eigenen Provider-Schluessel — der
45
52
  // Provider ergibt sich aus dem Transportweg (Cloud vs. HPS), nicht aus
@@ -1,7 +1,7 @@
1
1
  export { type ReceiptItem, type ReceiptItemPayload, type ReceiptItemPayloadRead, toReceiptItemPayload, fromReceiptItemPayload, receiptItemTotalCents, receiptItemIsValid, negateReceiptItem, type TipRecipient, isTipItem, isDiscountItem, } from './receipt-item.js';
2
2
  export { type Voucher, type VoucherPayload, toVoucherPayload, fromVoucherPayload, voucherIsValid } from './voucher.js';
3
3
  export { type Receipt, type ReceiptPayload, type ReceiptPayloadRead, toReceiptPayload, fromReceiptPayload, receiptSubSumCents, receiptSumCents, } from './receipt.js';
4
- export { type CancellationOf, type CancellationItem, type Cancellation, type CancellationReason, CANCELLATION_REASONS, CANCELLATION_RESERVATION_MS, isCancellationReason, remainingQuantities, } from './cancellation.js';
4
+ export { type CancellationOf, type CancellationItem, type Cancellation, type CancellationReason, type CancellationErrorCode, CANCELLATION_REASONS, CANCELLATION_ERROR_CODES, CANCELLATION_RESERVATION_MS, isCancellationReason, isCancellationErrorCode, remainingQuantities, } from './cancellation.js';
5
5
  export { type ReceiptCompany, type ReceiptCompanyPayload, fromReceiptCompanyPayload, receiptCompanyTaxInfo, } from './receipt-company.js';
6
6
  export { type Cashregister, type CashregisterOnboarding, type CashregisterPayload, type CashregisterOnboardingPayload, fromCashregisterPayload, } from './cashregister.js';
7
7
  export { type ReceiptSummary, type ReceiptSummaryPayload, fromReceiptSummaryPayload, type ZeroKind, ZERO_KINDS, istZeroKind } from './receipt-summary.js';
@@ -1,7 +1,7 @@
1
1
  export { toReceiptItemPayload, fromReceiptItemPayload, receiptItemTotalCents, receiptItemIsValid, negateReceiptItem, isTipItem, isDiscountItem, } from './receipt-item.js';
2
2
  export { toVoucherPayload, fromVoucherPayload, voucherIsValid } from './voucher.js';
3
3
  export { toReceiptPayload, fromReceiptPayload, receiptSubSumCents, receiptSumCents, } from './receipt.js';
4
- export { CANCELLATION_REASONS, CANCELLATION_RESERVATION_MS, isCancellationReason, remainingQuantities, } from './cancellation.js';
4
+ export { CANCELLATION_REASONS, CANCELLATION_ERROR_CODES, CANCELLATION_RESERVATION_MS, isCancellationReason, isCancellationErrorCode, remainingQuantities, } from './cancellation.js';
5
5
  export { fromReceiptCompanyPayload, receiptCompanyTaxInfo, } from './receipt-company.js';
6
6
  export { fromCashregisterPayload, } from './cashregister.js';
7
7
  export { fromReceiptSummaryPayload, ZERO_KINDS, istZeroKind } from './receipt-summary.js';
@@ -56,6 +56,7 @@ export interface ReceiptSummary {
56
56
  /** Nur am Storno-Beleg: das Original. */
57
57
  cancellationOf?: {
58
58
  receiptId: string | null;
59
+ timeStamp?: string;
59
60
  };
60
61
  cancellationReason?: string;
61
62
  /** offen | teil | voll -- Storno-Stand des Originals. */
@@ -84,6 +85,7 @@ export interface ReceiptSummaryPayload {
84
85
  } | null;
85
86
  cancellationOf?: {
86
87
  receiptId?: string | null;
88
+ timeStamp?: string | null;
87
89
  } | null;
88
90
  cancellationReason?: string | null;
89
91
  stornoStand?: string | null;
@@ -41,7 +41,10 @@ export function fromReceiptSummaryPayload(payload) {
41
41
  signatureOk: payload.signature_ok !== false,
42
42
  items: Array.isArray(payload.items) ? payload.items.map((i) => ({ name: i?.name ?? '', quantity: typeof i?.quantity === 'number' ? i.quantity : 0 })) : [],
43
43
  ...(payload.operator ? { operator: { uid: payload.operator.uid ?? null, name: payload.operator.name ?? '' } } : {}),
44
- ...(payload.cancellationOf ? { cancellationOf: { receiptId: payload.cancellationOf.receiptId ?? null } } : {}),
44
+ ...(payload.cancellationOf ? { cancellationOf: {
45
+ receiptId: payload.cancellationOf.receiptId ?? null,
46
+ ...(typeof payload.cancellationOf.timeStamp === 'string' && payload.cancellationOf.timeStamp ? { timeStamp: payload.cancellationOf.timeStamp } : {}),
47
+ } } : {}),
45
48
  ...(payload.cancellationReason ? { cancellationReason: payload.cancellationReason } : {}),
46
49
  stornoStand: payload.stornoStand === 'teil' || payload.stornoStand === 'voll' ? payload.stornoStand : 'offen',
47
50
  ...(istZeroKind(payload.zeroKind) ? { zeroKind: payload.zeroKind } : {}),
@@ -70,7 +70,11 @@ export function fromReceiptPayload(payload) {
70
70
  signatureSuccess: payload.signatureSuccess ?? undefined,
71
71
  customProjectId: payload.customProjectId ?? undefined,
72
72
  ...(typeof payload.tipCents === 'number' ? { tipCents: payload.tipCents } : {}),
73
- ...(payload.cancellationOf ? { cancellationOf: { receiptId: payload.cancellationOf.receiptId, fullReceiptId: payload.cancellationOf.fullReceiptId ?? null } } : {}),
73
+ ...(payload.cancellationOf ? { cancellationOf: {
74
+ receiptId: payload.cancellationOf.receiptId,
75
+ fullReceiptId: payload.cancellationOf.fullReceiptId ?? null,
76
+ ...(typeof payload.cancellationOf.timeStamp === 'string' && payload.cancellationOf.timeStamp ? { timeStamp: payload.cancellationOf.timeStamp } : {}),
77
+ } } : {}),
74
78
  ...(payload.cancellationReason ? { cancellationReason: payload.cancellationReason } : {}),
75
79
  ...(istZeroKind(payload.zeroKind) ? { zeroKind: payload.zeroKind } : {}),
76
80
  ...(payload.cancellations ? { cancellations: payload.cancellations.map(leseStorno) } : {}),
@@ -84,6 +88,9 @@ function leseStorno(eintrag) {
84
88
  by: eintrag.by ?? null,
85
89
  note: eintrag.note ?? null,
86
90
  items: (eintrag.items ?? []).map((p) => ({ index: Number(p.index), quantity: Number(p.quantity) })),
91
+ ...(eintrag.promoAdjustmentCents && typeof eintrag.promoAdjustmentCents === 'object'
92
+ ? { promoAdjustmentCents: Object.fromEntries(Object.entries(eintrag.promoAdjustmentCents).map(([k, v]) => [k, Number(v)])) }
93
+ : {}),
87
94
  };
88
95
  }
89
96
  /**
@@ -1,4 +1,5 @@
1
1
  import { VoucherAction, VoucherType } from '../enums/index.js';
2
+ import { euroToCents } from '../money.js';
2
3
  import { readEnumKey, requireEnumKey } from './enum-payload.js';
3
4
  export function toVoucherPayload(voucher) {
4
5
  return {
@@ -30,16 +31,31 @@ export function voucherIsValid(voucher) {
30
31
  if (voucher.type === VoucherType.promo && voucher.valueCents == null) {
31
32
  return false;
32
33
  }
33
- if (voucher.valueCents != null && voucher.valueCents <= 0) {
34
+ // `!Number.isFinite` VOR den Vergleich, nicht dahinter: `NaN <= 0` und
35
+ // `Infinity <= 0` sind beide `false`, ein Vergleich mit einem
36
+ // nicht-endlichen Wert ist also IMMER falsch und liesse ihn hier
37
+ // durchrutschen. Gemessen (28.08.2026): ein Gutschein mit `valueCents: NaN`
38
+ // (aus einem kaputten `value` in der Nutzlast, siehe [fromVoucherPayload])
39
+ // oder `valueCents: Infinity` galt bis zu dieser Fassung als GUELTIG und
40
+ // waere still in die Belegsummen gewandert. Diese Pruefung greift
41
+ // unabhaengig davon, ob `valueCents` ueber [fromVoucherPayload] oder direkt
42
+ // gesetzt wurde -- die gehaertete Umrechnung dort allein reicht nicht.
43
+ if (voucher.valueCents != null && (!Number.isFinite(voucher.valueCents) || voucher.valueCents <= 0)) {
34
44
  return false;
35
45
  }
36
46
  return true;
37
47
  }
38
48
  export function fromVoucherPayload(payload) {
49
+ // Euro->Cent ueber die eine gehaertete Stelle im Paket ([euroToCents],
50
+ // money.ts) statt einer eigenen `Math.round(payload.value * 100)` --
51
+ // [euroToCents] prueft `Number.isFinite` und liefert fuer einen
52
+ // nicht-numerischen oder nicht-endlichen Wert `0` statt eines lautlosen
53
+ // `NaN`. Siehe auch [voucherIsValid]: `0` faellt dort ueber die
54
+ // "<= 0"-Pruefung, ein `NaN` waere durchgerutscht.
39
55
  const valueCents = payload.valueCents != null
40
56
  ? payload.valueCents
41
57
  : payload.value != null
42
- ? Math.round(payload.value * 100)
58
+ ? euroToCents(payload.value)
43
59
  : undefined;
44
60
  return {
45
61
  name: payload.name ?? undefined,