@kreiseck/kasseneck-api 0.6.7

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 (294) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +2 -0
  3. package/README.md +206 -0
  4. package/dist/cjs/client/api.d.ts +75 -0
  5. package/dist/cjs/client/api.js +40 -0
  6. package/dist/cjs/client/auth.d.ts +52 -0
  7. package/dist/cjs/client/auth.js +72 -0
  8. package/dist/cjs/client/cashregisters.d.ts +21 -0
  9. package/dist/cjs/client/cashregisters.js +36 -0
  10. package/dist/cjs/client/errors.d.ts +159 -0
  11. package/dist/cjs/client/errors.js +226 -0
  12. package/dist/cjs/client/index.d.ts +8 -0
  13. package/dist/cjs/client/index.js +44 -0
  14. package/dist/cjs/client/receipts.d.ts +245 -0
  15. package/dist/cjs/client/receipts.js +452 -0
  16. package/dist/cjs/client/reports.d.ts +43 -0
  17. package/dist/cjs/client/reports.js +73 -0
  18. package/dist/cjs/client/status.d.ts +43 -0
  19. package/dist/cjs/client/status.js +47 -0
  20. package/dist/cjs/client/transport.d.ts +117 -0
  21. package/dist/cjs/client/transport.js +312 -0
  22. package/dist/cjs/enums/credit-card-provider.d.ts +22 -0
  23. package/dist/cjs/enums/credit-card-provider.js +23 -0
  24. package/dist/cjs/enums/define-enum.d.ts +15 -0
  25. package/dist/cjs/enums/define-enum.js +16 -0
  26. package/dist/cjs/enums/index.d.ts +7 -0
  27. package/dist/cjs/enums/index.js +17 -0
  28. package/dist/cjs/enums/keck-payment-method.d.ts +57 -0
  29. package/dist/cjs/enums/keck-payment-method.js +23 -0
  30. package/dist/cjs/enums/receipt-type.d.ts +41 -0
  31. package/dist/cjs/enums/receipt-type.js +18 -0
  32. package/dist/cjs/enums/stripe-link-mode.d.ts +17 -0
  33. package/dist/cjs/enums/stripe-link-mode.js +18 -0
  34. package/dist/cjs/enums/vat-rate.d.ts +40 -0
  35. package/dist/cjs/enums/vat-rate.js +19 -0
  36. package/dist/cjs/enums/voucher-action.d.ts +10 -0
  37. package/dist/cjs/enums/voucher-action.js +11 -0
  38. package/dist/cjs/enums/voucher-type.d.ts +9 -0
  39. package/dist/cjs/enums/voucher-type.js +10 -0
  40. package/dist/cjs/index.d.ts +5 -0
  41. package/dist/cjs/index.js +97 -0
  42. package/dist/cjs/kasse/artikel.d.ts +83 -0
  43. package/dist/cjs/kasse/artikel.js +79 -0
  44. package/dist/cjs/kasse/client.d.ts +10 -0
  45. package/dist/cjs/kasse/client.js +38 -0
  46. package/dist/cjs/kasse/drucker.d.ts +54 -0
  47. package/dist/cjs/kasse/drucker.js +41 -0
  48. package/dist/cjs/kasse/index.d.ts +11 -0
  49. package/dist/cjs/kasse/index.js +38 -0
  50. package/dist/cjs/kasse/settings.d.ts +115 -0
  51. package/dist/cjs/kasse/settings.js +62 -0
  52. package/dist/cjs/models/cancellation.d.ts +51 -0
  53. package/dist/cjs/models/cancellation.js +45 -0
  54. package/dist/cjs/models/cashregister.d.ts +77 -0
  55. package/dist/cjs/models/cashregister.js +53 -0
  56. package/dist/cjs/models/enum-payload.d.ts +26 -0
  57. package/dist/cjs/models/enum-payload.js +50 -0
  58. package/dist/cjs/models/hobex-receipt.d.ts +63 -0
  59. package/dist/cjs/models/hobex-receipt.js +86 -0
  60. package/dist/cjs/models/index.d.ts +10 -0
  61. package/dist/cjs/models/index.js +46 -0
  62. package/dist/cjs/models/receipt-company.d.ts +71 -0
  63. package/dist/cjs/models/receipt-company.js +52 -0
  64. package/dist/cjs/models/receipt-item.d.ts +100 -0
  65. package/dist/cjs/models/receipt-item.js +108 -0
  66. package/dist/cjs/models/receipt-summary.d.ts +92 -0
  67. package/dist/cjs/models/receipt-summary.js +54 -0
  68. package/dist/cjs/models/receipt.d.ts +108 -0
  69. package/dist/cjs/models/receipt.js +119 -0
  70. package/dist/cjs/models/report-month.d.ts +32 -0
  71. package/dist/cjs/models/report-month.js +65 -0
  72. package/dist/cjs/models/stripe-url-session.d.ts +18 -0
  73. package/dist/cjs/models/stripe-url-session.js +20 -0
  74. package/dist/cjs/models/voucher.d.ts +41 -0
  75. package/dist/cjs/models/voucher.js +56 -0
  76. package/dist/cjs/money.d.ts +23 -0
  77. package/dist/cjs/money.js +35 -0
  78. package/dist/cjs/package.json +1 -0
  79. package/dist/cjs/payments/hobex.d.ts +87 -0
  80. package/dist/cjs/payments/hobex.js +167 -0
  81. package/dist/cjs/payments/index.d.ts +33 -0
  82. package/dist/cjs/payments/index.js +42 -0
  83. package/dist/cjs/payments/stripe.d.ts +61 -0
  84. package/dist/cjs/payments/stripe.js +157 -0
  85. package/dist/cjs/printing/escpos.d.ts +219 -0
  86. package/dist/cjs/printing/escpos.js +636 -0
  87. package/dist/cjs/printing/index.d.ts +3 -0
  88. package/dist/cjs/printing/index.js +33 -0
  89. package/dist/cjs/printing/printable.d.ts +27 -0
  90. package/dist/cjs/printing/printable.js +90 -0
  91. package/dist/cjs/printing/webusb.d.ts +89 -0
  92. package/dist/cjs/printing/webusb.js +110 -0
  93. package/dist/cjs/react/index.d.ts +46 -0
  94. package/dist/cjs/react/index.js +51 -0
  95. package/dist/cjs/receipt/discount.d.ts +12 -0
  96. package/dist/cjs/receipt/discount.js +54 -0
  97. package/dist/cjs/receipt/epos.d.ts +53 -0
  98. package/dist/cjs/receipt/epos.js +95 -0
  99. package/dist/cjs/receipt/grid.d.ts +47 -0
  100. package/dist/cjs/receipt/grid.js +122 -0
  101. package/dist/cjs/receipt/index.d.ts +4 -0
  102. package/dist/cjs/receipt/index.js +29 -0
  103. package/dist/cjs/receipt/layout-escpos.d.ts +35 -0
  104. package/dist/cjs/receipt/layout-escpos.js +67 -0
  105. package/dist/cjs/receipt/layout.d.ts +180 -0
  106. package/dist/cjs/receipt/layout.js +594 -0
  107. package/dist/cjs/register/index.d.ts +51 -0
  108. package/dist/cjs/register/index.js +61 -0
  109. package/dist/cjs/register/pairing.d.ts +248 -0
  110. package/dist/cjs/register/pairing.js +279 -0
  111. package/dist/cjs/register/session.d.ts +37 -0
  112. package/dist/cjs/register/session.js +52 -0
  113. package/dist/cjs/vienna-time.d.ts +23 -0
  114. package/dist/cjs/vienna-time.js +114 -0
  115. package/dist/esm/client/api.d.ts +75 -0
  116. package/dist/esm/client/api.js +37 -0
  117. package/dist/esm/client/auth.d.ts +52 -0
  118. package/dist/esm/client/auth.js +68 -0
  119. package/dist/esm/client/cashregisters.d.ts +21 -0
  120. package/dist/esm/client/cashregisters.js +33 -0
  121. package/dist/esm/client/errors.d.ts +159 -0
  122. package/dist/esm/client/errors.js +212 -0
  123. package/dist/esm/client/index.d.ts +8 -0
  124. package/dist/esm/client/index.js +8 -0
  125. package/dist/esm/client/receipts.d.ts +245 -0
  126. package/dist/esm/client/receipts.js +438 -0
  127. package/dist/esm/client/reports.d.ts +43 -0
  128. package/dist/esm/client/reports.js +69 -0
  129. package/dist/esm/client/status.d.ts +43 -0
  130. package/dist/esm/client/status.js +43 -0
  131. package/dist/esm/client/transport.d.ts +117 -0
  132. package/dist/esm/client/transport.js +307 -0
  133. package/dist/esm/enums/credit-card-provider.d.ts +22 -0
  134. package/dist/esm/enums/credit-card-provider.js +20 -0
  135. package/dist/esm/enums/define-enum.d.ts +15 -0
  136. package/dist/esm/enums/define-enum.js +13 -0
  137. package/dist/esm/enums/index.d.ts +7 -0
  138. package/dist/esm/enums/index.js +7 -0
  139. package/dist/esm/enums/keck-payment-method.d.ts +57 -0
  140. package/dist/esm/enums/keck-payment-method.js +20 -0
  141. package/dist/esm/enums/receipt-type.d.ts +41 -0
  142. package/dist/esm/enums/receipt-type.js +15 -0
  143. package/dist/esm/enums/stripe-link-mode.d.ts +17 -0
  144. package/dist/esm/enums/stripe-link-mode.js +15 -0
  145. package/dist/esm/enums/vat-rate.d.ts +40 -0
  146. package/dist/esm/enums/vat-rate.js +16 -0
  147. package/dist/esm/enums/voucher-action.d.ts +10 -0
  148. package/dist/esm/enums/voucher-action.js +8 -0
  149. package/dist/esm/enums/voucher-type.d.ts +9 -0
  150. package/dist/esm/enums/voucher-type.js +7 -0
  151. package/dist/esm/index.d.ts +5 -0
  152. package/dist/esm/index.js +5 -0
  153. package/dist/esm/kasse/artikel.d.ts +83 -0
  154. package/dist/esm/kasse/artikel.js +70 -0
  155. package/dist/esm/kasse/client.d.ts +10 -0
  156. package/dist/esm/kasse/client.js +33 -0
  157. package/dist/esm/kasse/drucker.d.ts +54 -0
  158. package/dist/esm/kasse/drucker.js +36 -0
  159. package/dist/esm/kasse/index.d.ts +11 -0
  160. package/dist/esm/kasse/index.js +13 -0
  161. package/dist/esm/kasse/settings.d.ts +115 -0
  162. package/dist/esm/kasse/settings.js +58 -0
  163. package/dist/esm/models/cancellation.d.ts +51 -0
  164. package/dist/esm/models/cancellation.js +40 -0
  165. package/dist/esm/models/cashregister.d.ts +77 -0
  166. package/dist/esm/models/cashregister.js +50 -0
  167. package/dist/esm/models/enum-payload.d.ts +26 -0
  168. package/dist/esm/models/enum-payload.js +44 -0
  169. package/dist/esm/models/hobex-receipt.d.ts +63 -0
  170. package/dist/esm/models/hobex-receipt.js +80 -0
  171. package/dist/esm/models/index.d.ts +10 -0
  172. package/dist/esm/models/index.js +10 -0
  173. package/dist/esm/models/receipt-company.d.ts +71 -0
  174. package/dist/esm/models/receipt-company.js +48 -0
  175. package/dist/esm/models/receipt-item.d.ts +100 -0
  176. package/dist/esm/models/receipt-item.js +101 -0
  177. package/dist/esm/models/receipt-summary.d.ts +92 -0
  178. package/dist/esm/models/receipt-summary.js +49 -0
  179. package/dist/esm/models/receipt.d.ts +108 -0
  180. package/dist/esm/models/receipt.js +113 -0
  181. package/dist/esm/models/report-month.d.ts +32 -0
  182. package/dist/esm/models/report-month.js +58 -0
  183. package/dist/esm/models/stripe-url-session.d.ts +18 -0
  184. package/dist/esm/models/stripe-url-session.js +16 -0
  185. package/dist/esm/models/voucher.d.ts +41 -0
  186. package/dist/esm/models/voucher.js +51 -0
  187. package/dist/esm/money.d.ts +23 -0
  188. package/dist/esm/money.js +31 -0
  189. package/dist/esm/payments/hobex.d.ts +87 -0
  190. package/dist/esm/payments/hobex.js +162 -0
  191. package/dist/esm/payments/index.d.ts +33 -0
  192. package/dist/esm/payments/index.js +33 -0
  193. package/dist/esm/payments/stripe.d.ts +61 -0
  194. package/dist/esm/payments/stripe.js +153 -0
  195. package/dist/esm/printing/escpos.d.ts +219 -0
  196. package/dist/esm/printing/escpos.js +616 -0
  197. package/dist/esm/printing/index.d.ts +3 -0
  198. package/dist/esm/printing/index.js +3 -0
  199. package/dist/esm/printing/printable.d.ts +27 -0
  200. package/dist/esm/printing/printable.js +87 -0
  201. package/dist/esm/printing/webusb.d.ts +89 -0
  202. package/dist/esm/printing/webusb.js +101 -0
  203. package/dist/esm/react/index.d.ts +46 -0
  204. package/dist/esm/react/index.js +47 -0
  205. package/dist/esm/receipt/discount.d.ts +12 -0
  206. package/dist/esm/receipt/discount.js +51 -0
  207. package/dist/esm/receipt/epos.d.ts +53 -0
  208. package/dist/esm/receipt/epos.js +86 -0
  209. package/dist/esm/receipt/grid.d.ts +47 -0
  210. package/dist/esm/receipt/grid.js +116 -0
  211. package/dist/esm/receipt/index.d.ts +4 -0
  212. package/dist/esm/receipt/index.js +4 -0
  213. package/dist/esm/receipt/layout-escpos.d.ts +35 -0
  214. package/dist/esm/receipt/layout-escpos.js +64 -0
  215. package/dist/esm/receipt/layout.d.ts +180 -0
  216. package/dist/esm/receipt/layout.js +583 -0
  217. package/dist/esm/register/index.d.ts +51 -0
  218. package/dist/esm/register/index.js +51 -0
  219. package/dist/esm/register/pairing.d.ts +248 -0
  220. package/dist/esm/register/pairing.js +270 -0
  221. package/dist/esm/register/session.d.ts +37 -0
  222. package/dist/esm/register/session.js +48 -0
  223. package/dist/esm/vienna-time.d.ts +23 -0
  224. package/dist/esm/vienna-time.js +110 -0
  225. package/fixtures/belege/langer-artikelname.json +43 -0
  226. package/fixtures/belege/null-ausfall.json +41 -0
  227. package/fixtures/belege/null-jahr.json +41 -0
  228. package/fixtures/belege/null-monat.json +41 -0
  229. package/fixtures/belege/null-pruef.json +41 -0
  230. package/fixtures/belege/null-schluss.json +41 -0
  231. package/fixtures/belege/null-start.json +40 -0
  232. package/fixtures/belege/rabattzeilen.json +61 -0
  233. package/fixtures/belege/signaturausfall-verkauf.json +49 -0
  234. package/fixtures/belege/storno-teil.json +48 -0
  235. package/fixtures/belege/storno-voll.json +54 -0
  236. package/fixtures/belege/testkasse-verkauf.json +50 -0
  237. package/fixtures/belege/testsignatur-verkauf.json +50 -0
  238. package/fixtures/belege/training.json +49 -0
  239. package/fixtures/belege/verkauf-bar.json +49 -0
  240. package/fixtures/belege/verkauf-karte.json +49 -0
  241. package/fixtures/belege/verkauf-kleinunternehmer.json +42 -0
  242. package/fixtures/erwartet/langer-artikelname.grid32.txt +25 -0
  243. package/fixtures/erwartet/langer-artikelname.grid48.txt +24 -0
  244. package/fixtures/erwartet/langer-artikelname.lines.json +216 -0
  245. package/fixtures/erwartet/null-ausfall.grid32.txt +28 -0
  246. package/fixtures/erwartet/null-ausfall.grid48.txt +25 -0
  247. package/fixtures/erwartet/null-ausfall.lines.json +220 -0
  248. package/fixtures/erwartet/null-jahr.grid32.txt +29 -0
  249. package/fixtures/erwartet/null-jahr.grid48.txt +25 -0
  250. package/fixtures/erwartet/null-jahr.lines.json +220 -0
  251. package/fixtures/erwartet/null-monat.grid32.txt +28 -0
  252. package/fixtures/erwartet/null-monat.grid48.txt +25 -0
  253. package/fixtures/erwartet/null-monat.lines.json +220 -0
  254. package/fixtures/erwartet/null-pruef.grid32.txt +28 -0
  255. package/fixtures/erwartet/null-pruef.grid48.txt +25 -0
  256. package/fixtures/erwartet/null-pruef.lines.json +220 -0
  257. package/fixtures/erwartet/null-schluss.grid32.txt +28 -0
  258. package/fixtures/erwartet/null-schluss.grid48.txt +25 -0
  259. package/fixtures/erwartet/null-schluss.lines.json +220 -0
  260. package/fixtures/erwartet/null-start.grid32.txt +28 -0
  261. package/fixtures/erwartet/null-start.grid48.txt +25 -0
  262. package/fixtures/erwartet/null-start.lines.json +220 -0
  263. package/fixtures/erwartet/rabattzeilen.grid32.txt +27 -0
  264. package/fixtures/erwartet/rabattzeilen.grid48.txt +26 -0
  265. package/fixtures/erwartet/rabattzeilen.lines.json +286 -0
  266. package/fixtures/erwartet/signaturausfall-verkauf.grid32.txt +28 -0
  267. package/fixtures/erwartet/signaturausfall-verkauf.grid48.txt +26 -0
  268. package/fixtures/erwartet/signaturausfall-verkauf.lines.json +266 -0
  269. package/fixtures/erwartet/storno-teil.grid32.txt +27 -0
  270. package/fixtures/erwartet/storno-teil.grid48.txt +26 -0
  271. package/fixtures/erwartet/storno-teil.lines.json +237 -0
  272. package/fixtures/erwartet/storno-voll.grid32.txt +29 -0
  273. package/fixtures/erwartet/storno-voll.grid48.txt +28 -0
  274. package/fixtures/erwartet/storno-voll.lines.json +277 -0
  275. package/fixtures/erwartet/testkasse-verkauf.grid32.txt +28 -0
  276. package/fixtures/erwartet/testkasse-verkauf.grid48.txt +27 -0
  277. package/fixtures/erwartet/testkasse-verkauf.lines.json +270 -0
  278. package/fixtures/erwartet/testsignatur-verkauf.grid32.txt +30 -0
  279. package/fixtures/erwartet/testsignatur-verkauf.grid48.txt +27 -0
  280. package/fixtures/erwartet/testsignatur-verkauf.lines.json +270 -0
  281. package/fixtures/erwartet/training.grid32.txt +33 -0
  282. package/fixtures/erwartet/training.grid48.txt +30 -0
  283. package/fixtures/erwartet/training.lines.json +277 -0
  284. package/fixtures/erwartet/verkauf-bar.grid32.txt +25 -0
  285. package/fixtures/erwartet/verkauf-bar.grid48.txt +24 -0
  286. package/fixtures/erwartet/verkauf-bar.lines.json +256 -0
  287. package/fixtures/erwartet/verkauf-karte.grid32.txt +25 -0
  288. package/fixtures/erwartet/verkauf-karte.grid48.txt +24 -0
  289. package/fixtures/erwartet/verkauf-karte.lines.json +256 -0
  290. package/fixtures/erwartet/verkauf-kleinunternehmer.grid32.txt +26 -0
  291. package/fixtures/erwartet/verkauf-kleinunternehmer.grid48.txt +25 -0
  292. package/fixtures/erwartet/verkauf-kleinunternehmer.lines.json +226 -0
  293. package/fixtures/manifest.json +107 -0
  294. package/package.json +135 -0
@@ -0,0 +1,583 @@
1
+ import { VatRate, VoucherAction, VoucherType } from '../enums/index.js';
2
+ import { CANCELLATION_REASONS, isCancellationReason, receiptCompanyTaxInfo, receiptItemTotalCents, receiptSubSumCents, receiptSumCents, voucherIsValid, } from '../models/index.js';
3
+ import { parseServerTimeStamp, toViennaWallClock } from '../vienna-time.js';
4
+ import { KasseneckValidationError } from '../client/errors.js';
5
+ export const AKTUELLES_REGELWERK = 2;
6
+ // -------------------------------------------------------------- Formatierung
7
+ /**
8
+ * Cent-Betrag als Anzeigetext: `1234` -> `"12,34"`. Die **einzige** Stelle, an
9
+ * der aus Cent eine Zeichenkette wird. Erwartet ganze Cent (Geld wird in
10
+ * diesem Paket ausnahmslos ganzzahlig gerechnet).
11
+ */
12
+ export function formatCents(cents) {
13
+ return (cents / 100).toFixed(2).replace('.', ',');
14
+ }
15
+ /**
16
+ * Belegzeit als `DD.MM.YYYY HH:mm:ss` in **Wiener** Zeit — Zwilling von
17
+ * `KasseneckReceipt.readableTime`. Der Server liefert seine Zeitstempel
18
+ * uneinheitlich; gedeutet wird deshalb ueber `parseServerTimeStamp` und nie
19
+ * ueber `new Date(text)` (siehe ../vienna-time.ts).
20
+ *
21
+ * Ist der Zeitstempel unlesbar, wirft die Deutung ein gewoehnliches `Error`.
22
+ * Das ist ein Antwortproblem und gehoert in die Fehler-Union dieses Pakets —
23
+ * sonst faellt ein Aufrufer, der nach den Waechtern verzweigt, aus allen
24
+ * heraus (dieselbe Umwandlung wie in client/receipts.ts bei
25
+ * getFirstReceiptDate).
26
+ */
27
+ function belegZeit(timeStamp) {
28
+ let t;
29
+ try {
30
+ t = toViennaWallClock(parseServerTimeStamp(timeStamp));
31
+ }
32
+ catch {
33
+ // Der Zeitstempel selbst wandert NICHT in die Meldung: er kommt aus einer
34
+ // fremden Antwort, und was dort steht, ist nicht unsere Zusage.
35
+ throw new KasseneckValidationError('buildReceiptLayout', 'Beleg enthaelt keinen lesbaren Zeitstempel', 'response');
36
+ }
37
+ const zwei = (n) => String(n).padStart(2, '0');
38
+ return `${zwei(t.day)}.${zwei(t.month)}.${t.year} ${zwei(t.hour)}:${zwei(t.minute)}:${zwei(t.second)}`;
39
+ }
40
+ /**
41
+ * Zahlungsart als Anzeigetext. Drei Faelle, und alle drei kommen vor:
42
+ * bekannter Eintrag (Beschriftung), unbekannter Schluessel (roh — besser als
43
+ * ein Beleg, der sich nicht anzeigen laesst), und **gar keiner**: Start- und
44
+ * Nullbelege tragen keine Zahlungsart, das Backend sendet dann `null`. Da
45
+ * `typeof null === 'object'` ist, ergab das beim Lesen von `.label` einen
46
+ * nackten TypeError.
47
+ */
48
+ function zahlungsartText(wert) {
49
+ if (wert != null && typeof wert === 'object') {
50
+ return wert.label;
51
+ }
52
+ return typeof wert === 'string' ? wert : '';
53
+ }
54
+ // -------------------------------------------------------- Kleinunternehmer
55
+ /**
56
+ * Hinweis auf die Steuerbefreiung, den ein Kleinunternehmerbeleg tragen muss —
57
+ * ohne ihn stuende in der USt-Tabelle „D 0 %" ohne jede Begruendung, und ab
58
+ * 400 € greift § 11 UStG voll.
59
+ *
60
+ * Wortlaut **woertlich** aus dem Backend (`INVOICE_TAX_NOTE.smallBusiness` in
61
+ * functions/index.js), damit auf Beleg und Rechnung derselbe Satz steht. Er
62
+ * traegt einen Gedankenstrich (U+2013), kein Bindestrich; der Bondrucker
63
+ * bekommt ihn ueber die Ersetzung der ESC/POS-Bruecke.
64
+ *
65
+ * Weder `print_paper.dart` noch das Beleg-PDF des Backends drucken den Hinweis
66
+ * heute — hier liegt das Kennzeichen vor, also steht er hier.
67
+ *
68
+ * Der **Name** ist englisch wie die uebrige Oberflaeche (`DEFAULT_BASE_URL`,
69
+ * `formatCents`, `buildReceiptLayout`); der **Wortlaut** ist selbstverstaendlich
70
+ * deutsch — er steht so auf oesterreichischen Belegen.
71
+ */
72
+ export const SMALL_BUSINESS_NOTICE = 'Umsatzsteuerbefreit – Kleinunternehmer gemäß § 6 Abs. 1 Z 27 UStG.';
73
+ // ------------------------------------------------------------------ Signatur
74
+ /** Text, den das Backend statt einer Signatur ablegt, wenn die Karte ausfaellt. */
75
+ const SIGNATUR_AUSGEFALLEN = 'Sicherheitseinrichtung ausgefallen';
76
+ /**
77
+ * Derselbe Text base64url-kodiert — so steht er als dritter JWS-Teil im Feld
78
+ * `sig` (Zwilling von `RKSVService.isSigSuccess`). Fest hinterlegt statt zur
79
+ * Laufzeit kodiert: `btoa`/`Buffer` sind in Browser und Node verschieden zu
80
+ * haben, und der Wert ist unveraenderlich.
81
+ */
82
+ const SIGNATUR_AUSGEFALLEN_BASE64URL = 'U2ljaGVyaGVpdHNlaW5yaWNodHVuZyBhdXNnZWZhbGxlbg';
83
+ /** Wurde der Beleg ohne funktionierende Signatureinheit ausgestellt? */
84
+ export function receiptSignatureFailed(receipt) {
85
+ return receipt.sig.split('.')[2] === SIGNATUR_AUSGEFALLEN_BASE64URL;
86
+ }
87
+ /**
88
+ * Traegt der Beleg eine **Test**-Signatur? Erkannt am ZDA-Kennzeichen `AT100`
89
+ * im maschinenlesbaren Code (`_R1-AT100_...`) — der Testpfad der Testumgebungen.
90
+ * Ob das ein Problem ist, weiss nur der Aufrufer (Testkonto: normal;
91
+ * Produktionskonto: „TESTSIGNATUR“-Warnung).
92
+ */
93
+ export function receiptSignatureIsTest(receipt) {
94
+ return /^_R1-AT100_/.test(receipt.qr ?? '');
95
+ }
96
+ /**
97
+ * Steuersatz einer Position. Ein roher Zahlenwert bedeutet: die Nutzlast
98
+ * fuehrt einen Satz, den dieses Paket noch nicht kennt (siehe
99
+ * models/receipt-item.ts). Der Beleg ist dann bereits ausgestellt und
100
+ * signiert — er muss sich anzeigen lassen, also erhaelt der Satz seine Zahl
101
+ * und einen Platzhalter als Kategorie.
102
+ */
103
+ function steuersatz(vat) {
104
+ if (typeof vat === 'number') {
105
+ return { rate: vat, category: '?' };
106
+ }
107
+ return { rate: vat.rate, category: vat.category };
108
+ }
109
+ /** `20` -> `"20"`, `4.9` -> `"4,9"` (wie im Vorbild). */
110
+ function satzText(rate) {
111
+ return String(rate).replace('.', ',');
112
+ }
113
+ /**
114
+ * Netto in ganzen Cent aus einem Brutto-Betrag.
115
+ *
116
+ * Gerundet wird **von der Null weg** — nicht mit `Math.round`, das `.5` immer
117
+ * Richtung +unendlich schiebt. Sonst spiegelt ein Stornobeleg seinen
118
+ * Originalbeleg nicht: 99 Cent zu 20 % liegen mit 82,5 Cent Netto genau auf der
119
+ * Rundungsgrenze, der Beleg zeigte „0,83 / 0,16", der Storno aber
120
+ * „-0,82 / -0,17" — Beleg und Storno heben sich in der Netto- und
121
+ * MwSt-Spalte nicht auf. Bei 20 % trifft das jeden sechsten Cent-Betrag.
122
+ * (Dieselbe Regel gilt in printing/escpos.ts und in Darts `toStringAsFixed`.)
123
+ */
124
+ function nettoAusBrutto(bruttoCents, rate) {
125
+ const betrag = Math.round((Math.abs(bruttoCents) * 100) / (100 + rate));
126
+ return bruttoCents < 0 ? -betrag : betrag;
127
+ }
128
+ // -------------------------------------------------------------- Gutscheine
129
+ /**
130
+ * Belegtext eines Gutscheins — Zwilling von `KeckVoucher.receiptText`:
131
+ * Gutscheinart, Wert und Name, wobei ein Name, der die Gutscheinart schon
132
+ * nennt, allein stehen bleibt.
133
+ */
134
+ function gutscheinText(voucher) {
135
+ let text = '';
136
+ if (voucher.type === VoucherType.value) {
137
+ text += 'Wertgutschein';
138
+ }
139
+ else if (voucher.type === VoucherType.promo) {
140
+ text += 'Promotionsgutschein';
141
+ }
142
+ const name = voucher.name;
143
+ if (name != null && name.toLowerCase().includes(text.toLowerCase())) {
144
+ return name;
145
+ }
146
+ if (voucher.valueCents != null) {
147
+ text += ` ${gutscheinBetrag(voucher.valueCents)} €`;
148
+ }
149
+ if (name != null && name.length > 0) {
150
+ text += ` - ${name}`;
151
+ }
152
+ return text;
153
+ }
154
+ /**
155
+ * Ganze Betraege kurz (`"10"`), krumme mit zwei Nachkommastellen (`"1,50"`) —
156
+ * Zwilling von `KeckVoucher.formatVoucherAmount`.
157
+ */
158
+ function gutscheinBetrag(cents) {
159
+ return cents % 100 === 0 ? String(cents / 100) : formatCents(cents);
160
+ }
161
+ // ----------------------------------------------------------------- Bauteile
162
+ function textZeile(text, align = 'left', bold = false) {
163
+ return { kind: 'text', text, align, bold };
164
+ }
165
+ /** Zwei Spalten: links beschriftet, rechts der Wert (Vorbild: `addDoubleText`). */
166
+ function paarZeile(links, rechts, linksBreite = 6, rechtsBreite = 6) {
167
+ return {
168
+ kind: 'columns',
169
+ columns: [
170
+ { text: links, width: linksBreite, align: 'left' },
171
+ { text: rechts, width: rechtsBreite, align: 'right' },
172
+ ],
173
+ };
174
+ }
175
+ /** Vier Spalten der USt-Tabelle (Vorbild: `_addTable`). */
176
+ function tabellenZeile(werte, paperSize) {
177
+ const breit = paperSize === 'mm80';
178
+ const breiten = [3, breit ? 4 : 3, 3, breit ? 2 : 3];
179
+ return {
180
+ kind: 'columns',
181
+ columns: werte.map((text, i) => ({
182
+ text,
183
+ width: breiten[i],
184
+ align: i === 3 ? 'right' : 'left',
185
+ })),
186
+ };
187
+ }
188
+ /** Positionszeile: Menge, Bezeichnung, ab Menge 2 der Einzelpreis. */
189
+ function positionsZeile(item, satz) {
190
+ const menge = String(item.quantity);
191
+ // Vorbild: Menge auf zwei Zeichen auffuellen, danach ' x ' — bei drei- und
192
+ // mehrstelligen Mengen faellt das Leerzeichen dahinter weg.
193
+ const kopf = menge.padEnd(2) + (menge.length > 2 ? ' x' : ' x ');
194
+ // "je" und Preis mit geschuetztem Leerzeichen: bricht die Zeile, bleibt "je 0,79" zusammen.
195
+ const einzelpreis = item.quantity > 1 ? ` je\u00a0${formatCents(item.priceCents)}` : '';
196
+ return {
197
+ kind: 'columns',
198
+ columns: [
199
+ { text: `${kopf}${item.name}${einzelpreis}`, width: 7, align: 'left' },
200
+ { text: `${formatCents(receiptItemTotalCents(item))} ${satz.category}`, width: 5, align: 'right' },
201
+ ],
202
+ };
203
+ }
204
+ // ---------------------------------------------------------------- Belegart
205
+ const TESTKASSE_TEXT = 'TESTKASSE — kein gültiger Beleg';
206
+ const TESTSIGNATUR_TEXT = 'TESTSIGNATUR — kein gültiger Beleg';
207
+ /** Erklaerung am Trainingsbeleg (Wortlaut mit dem Betreiber abgestimmt). */
208
+ const TRAINING_ERKLAERUNG = [
209
+ 'Trainingsbuchung — kein Kauf, keine Zahlung, keine steuerliche Buchung.',
210
+ 'Sollten Sie diesen Beleg als Kunde erhalten haben, sagen Sie bitte dem Betrieb Bescheid.',
211
+ ];
212
+ function bannerZeile(text, ton = 'belegart') {
213
+ return { kind: 'banner', text, ton };
214
+ }
215
+ /** Belegtyp als Zeichenkette (Enum-Objekt oder roher Serverwert). */
216
+ function belegtyp(receipt) {
217
+ return typeof receipt.receiptType === 'object' ? receipt.receiptType.value : String(receipt.receiptType ?? '');
218
+ }
219
+ /** Ist das ein Nullbeleg (Start-, Monats-, Jahres-, Schluss-, Pruefbeleg)? */
220
+ export function receiptIsZero(receipt) {
221
+ const t = belegtyp(receipt);
222
+ return t === 'zero' || t === 'start';
223
+ }
224
+ /**
225
+ * Sind alle Betraege des Belegs wirklich 0? (Keine Positionen mit Wert, keine
226
+ * Gutscheine.) Ein Nullbeleg, der das nicht erfuellt, ist ein Datenfehler --
227
+ * Regelwerk 2 setzt ihn dann als normalen Beleg.
228
+ */
229
+ export function receiptAmountsAreZero(receipt) {
230
+ if (receipt.vouchers.length > 0)
231
+ return false;
232
+ return receiptSumCents(receipt) === 0 && receipt.items.every((it) => it.priceCents === 0 || it.quantity === 0);
233
+ }
234
+ /**
235
+ * Zertifizierungsdienst aus der QR-Kennung `_R1-<ZDA>_` (RKSV-Detailspezifikation:
236
+ * AT0 geschlossenes System, AT1 A-Trust, AT2 GlobalTrust, AT3 PrimeSign,
237
+ * AT100 Testsignatur). Unbekannte Kennung: nur die Kennung.
238
+ */
239
+ export function receiptZdaText(qr) {
240
+ const m = /^_R1-(AT\d+)_/.exec(qr ?? '');
241
+ if (!m)
242
+ return null;
243
+ const kennung = m[1];
244
+ const namen = { AT0: 'geschlossenes System', AT1: 'A-Trust', AT2: 'GlobalTrust', AT3: 'PrimeSign', AT100: 'Testsignatur' };
245
+ const name = namen[kennung];
246
+ return name ? `${name} (${kennung})` : kennung;
247
+ }
248
+ /** „YYYY-MM-DD“ oder ISO -> „TT.MM.JJJJ“ (Wiener Kalendertag); Unlesbares -> null. */
249
+ function pruefDatum(wert) {
250
+ if (!wert)
251
+ return null;
252
+ const nurTag = /^(\d{4})-(\d{2})-(\d{2})$/.exec(wert);
253
+ if (nurTag)
254
+ return `${nurTag[3]}.${nurTag[2]}.${nurTag[1]}`;
255
+ try {
256
+ const t = toViennaWallClock(parseServerTimeStamp(wert));
257
+ const zwei = (n) => String(n).padStart(2, '0');
258
+ return `${zwei(t.day)}.${zwei(t.month)}.${t.year}`;
259
+ }
260
+ catch {
261
+ return null;
262
+ }
263
+ }
264
+ /** Block „Prüfangaben“ des Nullbelegs (Regelwerk 2). Nichts Geheimes: alles steht auch im QR bzw. bei FinanzOnline. */
265
+ function pruefangabenBlock(receipt, angaben) {
266
+ const out = [];
267
+ out.push({ kind: 'rule', char: '-' });
268
+ out.push(textZeile('Prüfangaben', 'center', true));
269
+ out.push(paarZeile('Barumsatz:', '0,00 €'));
270
+ out.push(paarZeile('Signatur:', receiptSignatureFailed(receipt) ? 'ausgefallen' : 'signiert'));
271
+ // Die Zertifikat-Seriennummer ist hexadezimal (RKSV-Detailspezifikation) --
272
+ // mit 0x gekennzeichnet, damit niemand sie fuer eine Dezimalzahl haelt.
273
+ if (receipt.certificateSerialNumber)
274
+ out.push(paarZeile('Signaturkarte:', `0x${receipt.certificateSerialNumber}`));
275
+ const zda = receiptZdaText(receipt.qr);
276
+ if (zda)
277
+ out.push(paarZeile('Zertifizierungsdienst:', zda, 7, 5));
278
+ const karte = pruefDatum(angaben?.karteRegistriertAm);
279
+ if (karte)
280
+ out.push(paarZeile('Karte registriert:', karte));
281
+ const kasse = pruefDatum(angaben?.kasseRegistriertAm);
282
+ if (kasse)
283
+ out.push(paarZeile('Kasse registriert:', kasse));
284
+ out.push({ kind: 'rule', char: '-' });
285
+ return out;
286
+ }
287
+ /** Monat/Jahr des Belegs in Wiener Zeit -- fuer „Nullbeleg 08/2026“. */
288
+ function belegMonatJahr(timeStamp) {
289
+ try {
290
+ const t = toViennaWallClock(parseServerTimeStamp(timeStamp));
291
+ return { monat: String(t.month).padStart(2, '0'), jahr: String(t.year) };
292
+ }
293
+ catch {
294
+ return { monat: '??', jahr: '????' };
295
+ }
296
+ }
297
+ /**
298
+ * Grund eines Stornos lesbar -- aus dem EINEN Katalog des Modells
299
+ * (`CANCELLATION_REASONS`, derselbe, den die Kasse anbietet und das Backend
300
+ * annimmt). Unbekannter Code (Fremdclient): sichtbar, aber ohne zu werfen.
301
+ */
302
+ function stornoGrundText(code) {
303
+ if (!code)
304
+ return null;
305
+ return isCancellationReason(code) ? CANCELLATION_REASONS[code] : code;
306
+ }
307
+ /**
308
+ * Kleinunternehmer (§ 6 Abs. 1 Z 27 UStG, unechte Befreiung): auf dem Beleg
309
+ * darf keine USt stehen -- jede Position 0 %. Sagt, ob der Beleg dazu passt;
310
+ * das Backend weist Widersprueche beim Ausstellen ab, hier ist es die Probe.
311
+ */
312
+ export function receiptIsSmallBusinessConsistent(receipt) {
313
+ return receipt.items.every((it) => Number(steuersatz(it.vat).rate) === 0);
314
+ }
315
+ /**
316
+ * Belegart-Block unter dem Betriebskopf: Titel als Banner, Untertitel als
317
+ * zentrierte Textzeilen. Verkaufsbelege bekommen keinen Block.
318
+ */
319
+ function belegartBlock(receipt) {
320
+ const t = belegtyp(receipt);
321
+ const aus = [];
322
+ if (t === 'cancellation') {
323
+ aus.push(bannerZeile('STORNOBELEG'));
324
+ aus.push(textZeile(receipt.cancellationOf ? `Stornobuchung zu Beleg ${receipt.cancellationOf.receiptId}` : 'Stornobuchung', 'center'));
325
+ const grund = stornoGrundText(typeof receipt.cancellationReason === 'string' ? receipt.cancellationReason : undefined);
326
+ if (grund)
327
+ aus.push(textZeile(`Grund: ${grund}`, 'center'));
328
+ }
329
+ else if (t === 'training') {
330
+ aus.push(bannerZeile('TRAININGSBELEG'));
331
+ for (const z of TRAINING_ERKLAERUNG)
332
+ aus.push(textZeile(z, 'center'));
333
+ }
334
+ else if (t === 'start') {
335
+ aus.push(bannerZeile('STARTBELEG'));
336
+ aus.push(textZeile('Nullbeleg zur Inbetriebnahme', 'center'));
337
+ }
338
+ else if (t === 'zero') {
339
+ const { monat, jahr } = belegMonatJahr(receipt.timeStamp);
340
+ switch (receipt.zeroKind) {
341
+ case 'monthly':
342
+ aus.push(bannerZeile('MONATSBELEG'), textZeile(`Nullbeleg ${monat}/${jahr}`, 'center'));
343
+ break;
344
+ case 'annual':
345
+ aus.push(bannerZeile('JAHRESBELEG'), textZeile(`Nullbeleg ${jahr} — Prüfung mit BMF-App`, 'center'));
346
+ break;
347
+ case 'annual_replacement':
348
+ // Ersatz-Jahresbeleg wird im Folgejahr erzeugt und gilt fuer das Vorjahr.
349
+ aus.push(bannerZeile('JAHRESBELEG'), textZeile(`Nullbeleg ${String(Number(jahr) - 1)} — Prüfung mit BMF-App (Ersatzbeleg)`, 'center'));
350
+ break;
351
+ case 'final':
352
+ aus.push(bannerZeile('SCHLUSSBELEG'), textZeile('Nullbeleg zur Außerbetriebnahme', 'center'));
353
+ break;
354
+ case 'outage_end':
355
+ aus.push(bannerZeile('NULLBELEG'), textZeile('Prüfbeleg nach Signaturausfall', 'center'));
356
+ break;
357
+ default:
358
+ aus.push(bannerZeile('NULLBELEG'), textZeile('Prüfbeleg', 'center'));
359
+ }
360
+ }
361
+ return aus;
362
+ }
363
+ // ------------------------------------------------------------------- Aufbau
364
+ /**
365
+ * Baut das Layout eines Belegs. Reihenfolge und Spaltenbreiten folgen
366
+ * `print_paper.dart` (`setKeckReceipt`).
367
+ */
368
+ export function buildReceiptLayout(receipt, company, options = {}) {
369
+ const paperSize = options.paperSize ?? 'mm58';
370
+ const regelwerk = options.regelwerk ?? AKTUELLES_REGELWERK;
371
+ if (regelwerk !== 1 && regelwerk !== 2) {
372
+ throw new KasseneckValidationError('buildReceiptLayout', `Unbekanntes Layout-Regelwerk ${String(regelwerk)} — bitte Paket aktualisieren`, 'request');
373
+ }
374
+ const lines = [];
375
+ // Regelwerk 2: ein „Nullbeleg“ mit echten Betraegen wird nicht reduziert --
376
+ // die Zahlen stehen drauf, statt hinter einer Null zu verschwinden.
377
+ const nullbeleg = receiptIsZero(receipt) && (regelwerk === 1 || receiptAmountsAreZero(receipt));
378
+ const warnungen = [];
379
+ if (options.testKasse)
380
+ warnungen.push(bannerZeile(TESTKASSE_TEXT, 'warnung'));
381
+ if (options.testSignatur)
382
+ warnungen.push(bannerZeile(TESTSIGNATUR_TEXT, 'warnung'));
383
+ // --- Warnrahmen oben (Testkasse/Testsignatur): ueber dem Kopf, damit ihn niemand uebersieht.
384
+ lines.push(...warnungen);
385
+ // --- Belegkopf: Firma, Anschrift, Steuerangabe, Telefon
386
+ lines.push(textZeile(company.companyName, 'center', true));
387
+ lines.push(textZeile(company.street, 'center'));
388
+ lines.push(textZeile(`${company.zip} ${company.city}`, 'center'));
389
+ lines.push(textZeile(receiptCompanyTaxInfo(company), 'center'));
390
+ lines.push(textZeile(company.phone, 'center'));
391
+ // --- Belegart (Storno, Training, Nullbeleg-Arten) direkt unter dem Kopf
392
+ const belegart = belegartBlock(receipt);
393
+ if (belegart.length > 0) {
394
+ lines.push({ kind: 'space', lines: 1 });
395
+ lines.push(...belegart);
396
+ }
397
+ // --- Nullbeleg: reduziert (§ 132a Abs. 3 BAO / RKSV § 11 Abs. 1 -- Pflichtangaben
398
+ // ja, Positionen/MwSt-Tabelle/Zahlungsart/Fusszeilen nein).
399
+ if (nullbeleg) {
400
+ lines.push({ kind: 'space', lines: 1 });
401
+ lines.push(paarZeile('Datum:', belegZeit(receipt.timeStamp), 4, 8));
402
+ lines.push(paarZeile('Kassen-ID:', receipt.cashregisterId));
403
+ lines.push(paarZeile('Beleg-ID:', receipt.receiptId));
404
+ lines.push({ kind: 'space', lines: 1 });
405
+ if (regelwerk === 1) {
406
+ lines.push(textZeile('Betrag: 0,00 €', 'center', true));
407
+ }
408
+ else {
409
+ lines.push(...pruefangabenBlock(receipt, options.pruefangaben ?? null));
410
+ }
411
+ lines.push({ kind: 'space', lines: 1 });
412
+ if (receiptSignatureFailed(receipt)) {
413
+ lines.push(textZeile(SIGNATUR_AUSGEFALLEN, 'center'));
414
+ lines.push({ kind: 'space', lines: 1 });
415
+ }
416
+ lines.push({ kind: 'qr', data: receipt.qr });
417
+ lines.push({ kind: 'space', lines: 1 });
418
+ lines.push(...warnungen);
419
+ return { lines, paperSize, regelwerk };
420
+ }
421
+ // --- Kundendaten
422
+ if (receipt.customerDetails.length > 0) {
423
+ lines.push({ kind: 'space', lines: 1 });
424
+ receipt.customerDetails.forEach((zeile, i) => {
425
+ lines.push(i === 0 ? paarZeile('Kunde:', zeile, 5, 7) : paarZeile('', zeile, 1, 11));
426
+ });
427
+ }
428
+ // --- Belegkennung
429
+ lines.push({ kind: 'space', lines: 1 });
430
+ lines.push(paarZeile('Datum:', belegZeit(receipt.timeStamp), 4, 8));
431
+ lines.push(paarZeile('Kassen-ID:', receipt.cashregisterId));
432
+ lines.push(paarZeile('Beleg-ID:', receipt.receiptId));
433
+ lines.push({ kind: 'space', lines: 1 });
434
+ // --- Positionen; dabei die Steuergruppen aufsammeln (Reihenfolge des
435
+ // ersten Auftretens, wie im Vorbild).
436
+ const gruppen = new Map();
437
+ const gruppeVon = (satz) => {
438
+ const vorhanden = gruppen.get(satz.rate);
439
+ if (vorhanden !== undefined) {
440
+ return vorhanden;
441
+ }
442
+ const neu = { satz, bruttoCents: 0 };
443
+ gruppen.set(satz.rate, neu);
444
+ return neu;
445
+ };
446
+ for (const item of receipt.items) {
447
+ const satz = steuersatz(item.vat);
448
+ gruppeVon(satz).bruttoCents += receiptItemTotalCents(item);
449
+ lines.push(positionsZeile(item, satz));
450
+ }
451
+ // --- Gutscheine: verkaufte Wertgutscheine sind Umsatz zu 0 %, eingeloeste
452
+ // Promotionsgutscheine mindern die Steuergruppen anteilig.
453
+ let promoCents = 0;
454
+ for (const voucher of receipt.vouchers) {
455
+ const wert = voucher.valueCents ?? 0;
456
+ if (voucherIsValid(voucher) && voucher.action === VoucherAction.sell) {
457
+ gruppeVon(steuersatz(VatRate.vat0)).bruttoCents += wert;
458
+ }
459
+ if (voucher.action === VoucherAction.sell && voucher.type === VoucherType.value) {
460
+ lines.push({
461
+ kind: 'columns',
462
+ columns: [
463
+ { text: `1 x ${gutscheinText(voucher)}`, width: 7, align: 'left' },
464
+ { text: `${formatCents(wert)} ${VatRate.vat0.category}`, width: 5, align: 'right' },
465
+ ],
466
+ });
467
+ }
468
+ if (voucher.action === VoucherAction.redeem && voucher.type === VoucherType.promo) {
469
+ promoCents += wert;
470
+ lines.push({
471
+ kind: 'columns',
472
+ columns: [
473
+ { text: gutscheinText(voucher), width: 7, align: 'left' },
474
+ { text: `-${formatCents(wert)} €`, width: 5, align: 'right' },
475
+ ],
476
+ });
477
+ }
478
+ }
479
+ verteilePromoGutschein(gruppen, promoCents);
480
+ // --- USt-Aufteilung
481
+ lines.push({ kind: 'space', lines: 1 });
482
+ lines.push(tabellenZeile(['MwSt%', 'MwSt', 'Netto', 'Brutto'], paperSize));
483
+ for (const gruppe of gruppen.values()) {
484
+ // Netto aus dem Brutto in ganzen Cent, MwSt als Rest: so ergeben Netto und
485
+ // MwSt zusammen immer wieder exakt den Brutto-Betrag der Zeile. (Das
486
+ // Vorbild rundet beide Werte einzeln aus einer Gleitkommazahl und kann
487
+ // dabei um einen Cent auseinanderlaufen.)
488
+ const bruttoCents = gruppe.bruttoCents;
489
+ const nettoCents = nettoAusBrutto(bruttoCents, gruppe.satz.rate);
490
+ lines.push(tabellenZeile([
491
+ `${gruppe.satz.category} ${satzText(gruppe.satz.rate)}%`,
492
+ formatCents(bruttoCents - nettoCents),
493
+ formatCents(nettoCents),
494
+ formatCents(bruttoCents),
495
+ ], paperSize));
496
+ }
497
+ lines.push({ kind: 'rule', char: '-' });
498
+ // --- Summen
499
+ const summeCents = receiptSumCents(receipt);
500
+ const zwischensummeCents = receiptSubSumCents(receipt);
501
+ if (summeCents !== zwischensummeCents) {
502
+ lines.push(paarZeile('Zwischensumme', `${formatCents(zwischensummeCents)} €`));
503
+ for (const voucher of receipt.vouchers) {
504
+ if (voucher.action === VoucherAction.redeem && voucher.type === VoucherType.value) {
505
+ lines.push(paarZeile(gutscheinText(voucher), `-${formatCents(voucher.valueCents ?? 0)} €`));
506
+ }
507
+ }
508
+ lines.push({ kind: 'rule', char: '-' });
509
+ }
510
+ lines.push(paarZeile('Gesamt:', `${formatCents(summeCents)} €`));
511
+ lines.push(paarZeile('Zahlungsart:', zahlungsartText(receipt.paymentMethod)));
512
+ lines.push({ kind: 'space', lines: 1 });
513
+ // --- Rechtshinweise und Ausfallhinweis
514
+ const hinweise = company.isSmallBusiness
515
+ ? [SMALL_BUSINESS_NOTICE, ...receipt.legalMessage]
516
+ : receipt.legalMessage;
517
+ if (hinweise.length > 0) {
518
+ for (const zeile of hinweise) {
519
+ lines.push(textZeile(zeile, 'center'));
520
+ }
521
+ lines.push({ kind: 'space', lines: 1 });
522
+ }
523
+ if (receiptSignatureFailed(receipt)) {
524
+ lines.push(textZeile(SIGNATUR_AUSGEFALLEN, 'center'));
525
+ lines.push({ kind: 'space', lines: 1 });
526
+ }
527
+ // --- RKSV-QR-Code
528
+ lines.push({ kind: 'qr', data: receipt.qr });
529
+ lines.push({ kind: 'space', lines: 1 });
530
+ // --- Dankestext und Fusszeilen
531
+ if (company.thanksMessage.length > 0) {
532
+ lines.push({ kind: 'space', lines: 1 });
533
+ for (const zeile of company.thanksMessage) {
534
+ lines.push(textZeile(zeile, 'center'));
535
+ }
536
+ }
537
+ for (const fuss of [company.footer1, company.footer2, company.footer3, company.footer4]) {
538
+ // Leere Fusszeilen werden ausgelassen: sie truegen nichts und erzeugten
539
+ // auf dem Bildschirm eine sichtbare Luecke.
540
+ if (fuss != null && fuss.length > 0) {
541
+ lines.push(textZeile(fuss, 'center'));
542
+ }
543
+ }
544
+ // --- Warnrahmen unten (Testkasse/Testsignatur)
545
+ if (warnungen.length > 0) {
546
+ lines.push({ kind: 'space', lines: 1 });
547
+ lines.push(...warnungen);
548
+ }
549
+ return { lines, paperSize, regelwerk };
550
+ }
551
+ /**
552
+ * Verteilt einen eingeloesten Promotionsgutschein anteilig auf die
553
+ * Steuergruppen — Zwilling von `print_paper.dart` (Zeilen 296-325).
554
+ *
555
+ * Der Gutschein mindert keinen einzelnen Artikel, sondern den Umsatz: jede
556
+ * Gruppe gibt ihren Anteil am Brutto ab. Der durch die ganzzahlige Teilung
557
+ * uebrig bleibende Cent geht an die groesste Gruppe, damit die Tabelle in
558
+ * Summe wieder die Zwischensumme ergibt.
559
+ */
560
+ function verteilePromoGutschein(gruppen, promoCents) {
561
+ const gesamtCents = [...gruppen.values()].reduce((summe, g) => summe + g.bruttoCents, 0);
562
+ const nutzbarCents = promoCents > gesamtCents ? gesamtCents : promoCents;
563
+ if (nutzbarCents <= 0 || gesamtCents <= 0) {
564
+ return;
565
+ }
566
+ const anteile = new Map();
567
+ let verteiltCents = 0;
568
+ for (const [satz, gruppe] of gruppen) {
569
+ const anteil = Math.trunc((nutzbarCents * gruppe.bruttoCents) / gesamtCents);
570
+ anteile.set(satz, anteil);
571
+ verteiltCents += anteil;
572
+ }
573
+ const restCents = nutzbarCents - verteiltCents;
574
+ if (restCents > 0) {
575
+ const groesste = [...gruppen.entries()].sort((a, b) => b[1].bruttoCents - a[1].bruttoCents)[0];
576
+ if (groesste !== undefined && groesste[1].bruttoCents > 0) {
577
+ anteile.set(groesste[0], (anteile.get(groesste[0]) ?? 0) + restCents);
578
+ }
579
+ }
580
+ for (const [satz, gruppe] of gruppen) {
581
+ gruppe.bruttoCents -= anteile.get(satz) ?? 0;
582
+ }
583
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Unterpfad `@kreiseck/kasseneck-api/register` — die **Anmeldung der
3
+ * Browser-Kasse**: ein Geraet koppeln, seine Kassen-Benutzer auflisten, einen
4
+ * davon per PIN anmelden (mit oder ohne Benutzerauswahl), und die entstandene
5
+ * Sitzung erneuern bzw. beenden.
6
+ *
7
+ * Diese Aufrufe stehen vor allen anderen: ohne sie gibt es keine
8
+ * Identitaet, mit der ein Beleg entstehen koennte.
9
+ *
10
+ * **Sie zerfallen in zwei Haelften, und der Unterschied ist wesentlich:**
11
+ *
12
+ * | Aufruf | Anmeldung |
13
+ * |---|---|
14
+ * | [pairRegisterDevice] | **keine** — der Kopplungs-Code ist der Nachweis |
15
+ * | [listRegisterUsersForDevice] | **keine** — der Ausweis des Geraets ist der Nachweis |
16
+ * | [unpairRegisterDevice] | **keine** — der Ausweis des Geraets; sperrt es im Backend (Entkoppeln) |
17
+ * | [registerUserLogin] | **keine** — dieser Aufruf erzeugt sie |
18
+ * | [registerPinLogin] | **keine** — wie registerUserLogin, nur ohne Benutzerauswahl (Geraete-Modus `pin`) |
19
+ * | [renewRegisterSession] | Kassen-Benutzer (`registerUserAuth`) |
20
+ * | [endRegisterSession] | Kassen-Benutzer (`registerUserAuth`) |
21
+ *
22
+ * Die ersten drei nehmen deshalb **keinen Transport** entgegen, sondern nur
23
+ * die Verbindungsangaben, und bauen ihn selbst — die anmeldungsfreie Anmeldung
24
+ * dahinter wird bewusst nicht exportiert, sonst liesse sich damit jeder Aufruf
25
+ * des Pakets ohne Anmeldung bauen (ausfuehrlich in pairing.ts). Die letzten
26
+ * beiden nehmen den Transport wie jeder andere Endpunkt-Aufruf und stehen
27
+ * zusaetzlich in `createKasseneckApi`.
28
+ *
29
+ * Der uebliche Ablauf einer Browser-Kasse:
30
+ *
31
+ * ```ts
32
+ * import {
33
+ * pairRegisterDevice, listRegisterUsersForDevice, registerUserLogin,
34
+ * } from '@kreiseck/kasseneck-api/register';
35
+ *
36
+ * // Einmalig: Code aus dem Panel gegen den Geraeteausweis tauschen.
37
+ * const geraet = await pairRegisterDevice({ code: 'K7NPQR34', label: 'Schank' });
38
+ *
39
+ * // Bei jedem Schichtwechsel: auswaehlen und per PIN anmelden.
40
+ * const { users } = await listRegisterUsersForDevice(geraet);
41
+ * const sitzung = await registerUserLogin({
42
+ * ...geraet, userId: users[0]!.id, pin: '1234', cashregisterId: geraet.cashregisterId,
43
+ * });
44
+ * ```
45
+ *
46
+ * Mit `sitzung.customToken` meldet sich der Verbraucher bei Firebase an; das
47
+ * daraus entstehende ID-Token und `sitzung.sessionId` ergeben zusammen
48
+ * `registerUserAuth` — ab da laeuft alles Weitere ueber den ueblichen Weg.
49
+ */
50
+ export { type RegisterDeviceConnection, type RegisterDeviceCredentials, type PairRegisterDeviceOptions, type PairedRegisterDevice, type RegisterUserKind, type RegisterUserSummary, type RegisterUserPerms, type RegisterScope, type RegisterUser, type RegisterUserSession, type ListRegisterUsersForDeviceOptions, type RegisterUserLoginOptions, type RegisterPinLoginOptions, type RegisterPinPolicy, type RegisterLoginMode, type RegisterDeviceUsers, type RegisterClientInfo, type RegisterGeo, type RegisterGeraeteAngaben, pairRegisterDevice, listRegisterUsersForDevice, unpairRegisterDevice, registerUserLogin, registerPinLogin, } from './pairing.js';
51
+ export { renewRegisterSession, endRegisterSession } from './session.js';