@kreiseck/kasseneck-api 0.7.2 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/dist/cjs/kasse/index.d.ts +1 -0
  3. package/dist/cjs/kasse/index.js +7 -1
  4. package/dist/cjs/kasse/texte.d.ts +336 -0
  5. package/dist/cjs/kasse/texte.js +150 -0
  6. package/dist/cjs/receipt/layout.d.ts +11 -5
  7. package/dist/cjs/receipt/layout.js +248 -26
  8. package/dist/esm/kasse/index.d.ts +1 -0
  9. package/dist/esm/kasse/index.js +2 -0
  10. package/dist/esm/kasse/texte.d.ts +336 -0
  11. package/dist/esm/kasse/texte.js +145 -0
  12. package/dist/esm/receipt/layout.d.ts +11 -5
  13. package/dist/esm/receipt/layout.js +247 -26
  14. package/fixtures/belege/karte-eigener.json +44 -0
  15. package/fixtures/belege/karte-gptom-ios.json +59 -0
  16. package/fixtures/belege/karte-gptom.json +59 -0
  17. package/fixtures/belege/karte-hobex-cloud.json +54 -0
  18. package/fixtures/belege/karte-hobex-hps.json +54 -0
  19. package/fixtures/belege/karte-mypos.json +55 -0
  20. package/fixtures/belege/karte-stripe-eps.json +52 -0
  21. package/fixtures/belege/karte-stripe.json +57 -0
  22. package/fixtures/belege/karte-sumup.json +53 -0
  23. package/fixtures/erwartet/karte-eigener.grid32.txt +22 -0
  24. package/fixtures/erwartet/karte-eigener.grid48.txt +22 -0
  25. package/fixtures/erwartet/karte-eigener.lines.json +216 -0
  26. package/fixtures/erwartet/karte-gptom-ios.grid32.txt +34 -0
  27. package/fixtures/erwartet/karte-gptom-ios.grid48.txt +34 -0
  28. package/fixtures/erwartet/karte-gptom-ios.lines.json +286 -0
  29. package/fixtures/erwartet/karte-gptom.grid32.txt +34 -0
  30. package/fixtures/erwartet/karte-gptom.grid48.txt +34 -0
  31. package/fixtures/erwartet/karte-gptom.lines.json +286 -0
  32. package/fixtures/erwartet/karte-hobex-cloud.grid32.txt +29 -0
  33. package/fixtures/erwartet/karte-hobex-cloud.grid48.txt +29 -0
  34. package/fixtures/erwartet/karte-hobex-cloud.lines.json +256 -0
  35. package/fixtures/erwartet/karte-hobex-hps.grid32.txt +33 -0
  36. package/fixtures/erwartet/karte-hobex-hps.grid48.txt +33 -0
  37. package/fixtures/erwartet/karte-hobex-hps.lines.json +272 -0
  38. package/fixtures/erwartet/karte-mypos.grid32.txt +38 -0
  39. package/fixtures/erwartet/karte-mypos.grid48.txt +36 -0
  40. package/fixtures/erwartet/karte-mypos.lines.json +353 -0
  41. package/fixtures/erwartet/karte-stripe-eps.grid32.txt +28 -0
  42. package/fixtures/erwartet/karte-stripe-eps.grid48.txt +28 -0
  43. package/fixtures/erwartet/karte-stripe-eps.lines.json +250 -0
  44. package/fixtures/erwartet/karte-stripe.grid32.txt +31 -0
  45. package/fixtures/erwartet/karte-stripe.grid48.txt +31 -0
  46. package/fixtures/erwartet/karte-stripe.lines.json +268 -0
  47. package/fixtures/erwartet/karte-sumup.grid32.txt +32 -0
  48. package/fixtures/erwartet/karte-sumup.grid48.txt +30 -0
  49. package/fixtures/erwartet/karte-sumup.lines.json +316 -0
  50. package/fixtures/hobex-hps-codes.json +1 -1
  51. package/fixtures/kasse-meldungen-faelle.json +32 -0
  52. package/fixtures/kasse-texte.json +385 -0
  53. package/fixtures/manifest.json +54 -0
  54. package/fixtures/oberflaeche.json +1 -1
  55. package/package.json +3 -2
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Was die Kasse selbst sagt — auf dem Bildschirm, in beiden Huellen.
3
+ *
4
+ * Die Browser-Kasse und die Kassen-App sind eine Kasse. Was der Kassier bei
5
+ * einem Fehler liest, steht hier genau einmal; beide Apps beziehen den Satz
6
+ * von hier, und wer ihn aendert, aendert ihn auf beiden Seiten oder gar nicht.
7
+ *
8
+ * Nicht hier: die fachlichen Saetze des Backends („Kopplungs-Code ist
9
+ * abgelaufen.", „PIN falsch, noch 2 Versuche"). Die gehen unveraendert durch.
10
+ *
11
+ * Ein Satz sagt, was los ist, und was zu tun ist. Nie Innereien.
12
+ *
13
+ * Schluessel: `bereich.name`. Ein Schluessel wird nie umgedeutet — wer den
14
+ * Sinn aendert, legt einen neuen an. `nur` nennt die Seite, wenn ein Satz nur
15
+ * auf einer Plattform vorkommen kann.
16
+ */
17
+ const MELDUNGEN_ROH = {
18
+ // --- Transport -----------------------------------------------------------
19
+ 'netz.keine_verbindung': { text: 'Keine Verbindung zum Server. Bitte die Internetverbindung prüfen und erneut versuchen.' },
20
+ 'netz.zeitablauf': { text: 'Der Server antwortet nicht. Bitte die Internetverbindung prüfen und erneut versuchen.' },
21
+ 'server.unerwartet': { text: 'Der Server hat unerwartet geantwortet (HTTP {status}). Bitte den Support verständigen.', platzhalter: ['status'] },
22
+ // --- Kopplung ------------------------------------------------------------
23
+ 'kopplung.code_fehlt': { text: 'Bitte den Kopplungs-Code eingeben.' },
24
+ 'kopplung.fehlgeschlagen': { text: 'Die Kopplung ist fehlgeschlagen. Bitte erneut versuchen.' },
25
+ 'kopplung.unvollstaendig': { text: 'Die Kopplung ist unvollständig zurückgekommen. Bitte im Panel einen neuen Code erzeugen und noch einmal versuchen.' },
26
+ 'geraet.browser_speicher': { text: 'Dieses Gerät konnte nicht gespeichert werden — der Browser-Speicher steht nicht zur Verfügung. Bitte den privaten Modus verlassen.', nur: ['web'] },
27
+ // --- Anmeldung und Sitzung -----------------------------------------------
28
+ 'anmeldung.pin_fehlt': { text: 'Bitte die PIN eingeben.' },
29
+ 'anmeldung.fehlgeschlagen': { text: 'Die Anmeldung ist fehlgeschlagen. Bitte erneut versuchen.' },
30
+ 'anmeldung.nicht_abgeschlossen': { text: 'Die Anmeldung konnte nicht abgeschlossen werden. Bitte erneut versuchen.' },
31
+ 'anmeldung.wird_abgeschlossen': { text: 'Anmeldung wird gerade abgeschlossen — bitte gleich noch einmal drücken.' },
32
+ 'sitzung.abgelaufen': { text: 'Die Sitzung ist abgelaufen. Bitte erneut anmelden.' },
33
+ 'sitzung.abgelaufen_ohne_netz': { text: 'Keine Verbindung zum Server — die Sitzung ist abgelaufen. Bitte erneut anmelden.' },
34
+ 'sitzung.keine': { text: 'Keine Sitzung.' },
35
+ 'rechte.nicht_aenderbar': { text: 'Das darfst du nicht ändern — das Recht dafür vergibt der Inhaber im Panel.' },
36
+ // --- Verkauf und Positionen ----------------------------------------------
37
+ 'kasse.laden_fehlgeschlagen': { text: 'Die Kasse konnte nicht geladen werden.' },
38
+ 'artikel.laden_fehlgeschlagen': { text: 'Die Artikel konnten nicht geladen werden.' },
39
+ 'artikel.keine_freigegeben': { text: 'Keine Artikel an dieser Kasse. Im Panel unter „Kacheln & Gruppen“ freischalten.' },
40
+ 'artikel.keine_gefunden': { text: 'Keine Artikel gefunden.' },
41
+ 'artikel.steuersatz_unbekannt': { text: '„{name}“ hat einen Steuersatz, den die Kasse nicht kennt.', platzhalter: ['name'] },
42
+ 'position.betrag_fehlt': { text: 'Bitte einen Betrag eingeben.' },
43
+ 'position.bezeichnung_fehlt': { text: 'Bitte eine Bezeichnung eingeben — sie steht am Beleg.' },
44
+ 'rabatt.prozent_ungueltig': { text: 'Bitte einen Prozentwert zwischen 0,1 und 100 eingeben.' },
45
+ // --- Abschluss -----------------------------------------------------------
46
+ 'abschluss.fehlgeschlagen': { text: 'Der Abschluss ist fehlgeschlagen.' },
47
+ 'abschluss.unklar': { text: 'Unklar, ob der Beleg entstanden ist — die Antwort kam nicht an. Bitte nicht noch einmal abschließen: der Beleg kann bereits erstellt und signiert sein. Im Panel unter „Belege“ nachsehen; nur wenn er dort fehlt, den Verkauf erneut abschließen.' },
48
+ 'abschluss.nullbeleg_fehlgeschlagen': { text: 'Der Nullbeleg konnte nicht erstellt werden.' },
49
+ 'abschluss.signatur_ausgefallen': { text: 'Die Signatureinheit hat nicht geantwortet. Der Beleg ist gültig und trägt den Vermerk „Sicherheitseinrichtung ausgefallen“.' },
50
+ // --- Kartenzahlung -------------------------------------------------------
51
+ 'kartenzahlung.nicht_moeglich': { text: 'Kartenzahlung nicht möglich.' },
52
+ 'kartenzahlung.nicht_zustande': { text: 'Die Kartenzahlung ist nicht zustande gekommen.' },
53
+ 'kartenzahlung.nicht_gestartet': { text: 'Kartenzahlung nicht gestartet: {grund}', platzhalter: ['grund'] },
54
+ 'kartenzahlung.unklar': { text: 'Unklar, ob die Kartenzahlung durchgegangen ist — die Verbindung zum Terminal riss ab. Bitte am Terminal-Beleg nachsehen, BEVOR neu kassiert wird: die Karte kann belastet sein.' },
55
+ // Die Kennung ist der einzige Anker, um die Zahlung am Terminal-Beleg wiederzufinden — liegt sie vor, gilt dieser Satz statt kartenzahlung.unklar.
56
+ 'kartenzahlung.unklar_mit_kennung': { text: 'Unklar, ob die Kartenzahlung durchgegangen ist — die Verbindung zum Terminal riss ab. Bitte am Terminal-Beleg nachsehen, BEVOR neu kassiert wird: die Karte kann belastet sein. Kennung der Zahlung: {kennung}.', platzhalter: ['kennung'] },
57
+ 'kartenzahlung.connect_nicht_verbunden': { text: 'Kartenzahlung nicht möglich: Kasseneck Connect ist nicht verbunden — Einstellungen → Kasseneck Connect.', nur: ['web'] },
58
+ 'terminal.keines_gefunden': { text: 'Kein Hobex-Terminal gefunden — ist es eingeschaltet und im selben Netz wie dieser Rechner?', nur: ['web'] },
59
+ 'terminal.nicht_bereit': { text: 'Terminal antwortet, ist aber nicht betriebsbereit: {antwort} — TID prüfen.', platzhalter: ['antwort'], nur: ['web'] },
60
+ 'gptom.app_fehlt': { text: 'Die GP-Tom-App ist auf diesem Gerät nicht da.', nur: ['app'] },
61
+ 'gptom.suche_fehlgeschlagen': { text: 'GP Tom: Suche nach der App fehlgeschlagen: {grund}', platzhalter: ['grund'], nur: ['app'] },
62
+ 'gptom.zahlung_fehlgeschlagen': { text: 'GP Tom: Zahlung fehlgeschlagen: {grund}', platzhalter: ['grund'], nur: ['app'] },
63
+ 'gptom.terminal_antwortet_nicht': { text: 'Das Terminal hat nicht geantwortet: {grund}', platzhalter: ['grund'], nur: ['app'] },
64
+ 'gptom.zahlung_nicht_abgeschlossen': { text: 'Die Zahlung wurde nicht abgeschlossen ({code}).', platzhalter: ['code'], nur: ['app'] },
65
+ // --- Belege und Storno ---------------------------------------------------
66
+ 'belege.laden_fehlgeschlagen': { text: 'Die Belege konnten nicht geladen werden.' },
67
+ 'belege.keine_im_zeitraum': { text: 'Keine Belege in diesem Zeitraum.' },
68
+ 'beleg.laden_fehlgeschlagen': { text: 'Der Beleg konnte nicht geladen werden.' },
69
+ 'beleg.nicht_gefunden': { text: 'Der Beleg wurde nicht gefunden.' },
70
+ 'storno.fehlgeschlagen': { text: 'Das Storno ist fehlgeschlagen.' },
71
+ 'storno.grund_fehlt': { text: 'Bitte einen Grund wählen.' },
72
+ 'storno.position_fehlt': { text: 'Bitte mindestens eine Position wählen.' },
73
+ // --- Druck ---------------------------------------------------------------
74
+ 'druck.fehlgeschlagen': { text: 'Der Ausdruck ist fehlgeschlagen.' },
75
+ 'druck.nicht_moeglich': { text: 'Druck nicht möglich: {grund}', platzhalter: ['grund'] },
76
+ 'druck.testdruck_fehlgeschlagen': { text: 'Der Testdruck ist fehlgeschlagen.' },
77
+ 'druck.testdruck_nicht_moeglich': { text: 'Der Testdruck ist fehlgeschlagen: {grund}', platzhalter: ['grund'] },
78
+ 'druck.kein_drucker': { text: 'Kein Bondrucker eingerichtet — in den Einstellungen unter Drucker & Lade.' },
79
+ 'druck.drucker_nicht_erreichbar': { text: 'Drucker nicht erreichbar.' },
80
+ 'druck.job_abgelaufen': { text: 'Drucker hat den Beleg nicht abgeholt (abgelaufen).', nur: ['web'] },
81
+ 'druck.kein_drucker_gefunden': { text: 'Kein Drucker gefunden — ist er eingeschaltet und im selben Netz wie dieser Rechner?', nur: ['web'] },
82
+ 'druck.nur_chrome': { text: '{weg}-Druck geht nur in Chrome oder Edge (Windows, Mac, Android) — nicht in Safari und nicht am iPad.', platzhalter: ['weg'], nur: ['web'] },
83
+ 'druck.kein_weg_drucker': { text: 'Kein {weg}-Drucker verbunden — „{weg}-Drucker verbinden“ und den Drucker im Dialog wählen.', platzhalter: ['weg'], nur: ['web'] },
84
+ 'bluetooth.aus': { text: 'Bluetooth ist ausgeschaltet. Bitte einschalten und erneut suchen.', nur: ['app'] },
85
+ 'bluetooth.freigabe_fehlt': { text: 'Bitte die Freigabe in den Geräte-Einstellungen erteilen.', nur: ['app'] },
86
+ 'bluetooth.suche_fehlgeschlagen': { text: 'Die Suche ist fehlgeschlagen: {grund}', platzhalter: ['grund'], nur: ['app'] },
87
+ // --- Kasseneck Connect (nur Browser) -------------------------------------
88
+ 'connect.antwortet_nicht': { text: 'Kasseneck Connect antwortet nicht — läuft das Programm auf diesem Rechner?', nur: ['web'] },
89
+ 'connect.code_abgelaufen': { text: 'Der Kopplungs-Code ist abgelaufen — im Agent einen neuen erzeugen („kasseneck-connect pair“).', nur: ['web'] },
90
+ 'connect.zu_viele_versuche': { text: 'Zu viele Fehlversuche — eine Minute warten und noch einmal versuchen.', nur: ['web'] },
91
+ 'connect.drucker_antwortet_nicht': { text: 'Der Drucker antwortet nicht — Strom, Netzwerk und IP prüfen.', nur: ['web'] },
92
+ 'connect.nicht_gekoppelt': { text: 'Diese Kasse ist mit Kasseneck Connect nicht gekoppelt — in den Einstellungen „Koppeln“ drücken.', nur: ['web'] },
93
+ 'connect.drucker_unbekannt': { text: 'Diesen Drucker kennt Kasseneck Connect nicht (mehr) — bitte neu suchen.', nur: ['web'] },
94
+ 'connect.ursprung_nicht_freigegeben': { text: 'Kasseneck Connect nimmt von dieser Adresse nichts an (Ursprung nicht freigegeben).', nur: ['web'] },
95
+ 'connect.nicht_installiert': { text: 'Kasseneck Connect nicht gefunden — bitte installieren.', nur: ['web'] },
96
+ // --- Einstellungen -------------------------------------------------------
97
+ 'einstellungen.laden_fehlgeschlagen': { text: 'Die Einstellungen konnten nicht geladen werden.' },
98
+ 'einstellungen.speichern_fehlgeschlagen': { text: 'Die Einstellung konnte nicht gespeichert werden.' },
99
+ 'einstellungen.noch_nicht_geladen': { text: 'Noch nicht geladen — bitte kurz warten oder neu anmelden.' },
100
+ 'einstellungen.ip_ungueltig': { text: 'Keine gültige IP-Adresse (z. B. 192.168.1.50).' },
101
+ 'entkopplung.fehlgeschlagen': { text: 'Entkoppeln fehlgeschlagen.' },
102
+ 'logo.hochladen_fehlgeschlagen': { text: 'Hochladen fehlgeschlagen.' },
103
+ 'logo.entfernen_fehlgeschlagen': { text: 'Entfernen fehlgeschlagen.' },
104
+ // --- Nur App -------------------------------------------------------------
105
+ 'app.nicht_im_browser': { text: 'Die Kassen-App läuft nicht im Browser — dafür gibt es kasse.kasseneck.at.', nur: ['app'] },
106
+ 'app.im_browser_oeffnen': { text: 'Bitte im Browser öffnen: {ziel}', platzhalter: ['ziel'], nur: ['app'] },
107
+ };
108
+ // Auf den gemeinsamen Typ gebracht: `Object.entries(MELDUNGEN)` liefert sonst
109
+ // pro Schluessel den engsten Literaltyp, und `platzhalter`/`nur` waeren nur
110
+ // auf manchen Zweigen der Vereinigung vorhanden.
111
+ export const MELDUNGEN = MELDUNGEN_ROH;
112
+ /**
113
+ * In welcher Reihenfolge ein Fehler eingeordnet wird — auf beiden Seiten
114
+ * dieselbe. Die Arten:
115
+ * api — HTTP 200, `status:'error'`: der Satz des Backends, woertlich
116
+ * klartext — schon fuer den Bildschirm geschrieben: sein eigener Text
117
+ * zeitablauf — die Frist lief ab, die Anfrage war draussen
118
+ * netz — keine Verbindung zustande gekommen oder abgerissen
119
+ * unerwartet — der Server hat geantwortet, aber nicht wie zugesagt
120
+ * (HTML statt JSON, 500, fehlende Huelle); `status` = HTTP-Code
121
+ * sonst — alles Uebrige ist technisch: der Ersatzsatz des Vorgangs
122
+ */
123
+ export const FEHLERREGELN = [
124
+ { art: 'api', verhalten: 'server_text' },
125
+ { art: 'klartext', verhalten: 'eigener_text' },
126
+ { art: 'zeitablauf', schluessel: 'netz.zeitablauf' },
127
+ { art: 'netz', schluessel: 'netz.keine_verbindung' },
128
+ { art: 'unerwartet', schluessel: 'server.unerwartet' },
129
+ { art: 'sonst', verhalten: 'ersatz' },
130
+ ];
131
+ /** Der Satz zum Schluessel, Platzhalter ersetzt. Fehlt ein Wert, wirft es — ein `{status}` am Tresen waere schlimmer. */
132
+ export function meldung(schluessel, werte = {}) {
133
+ const eintrag = MELDUNGEN[schluessel];
134
+ return eintrag.text.replace(/\{([a-z]+)\}/g, (_, name) => {
135
+ const wert = werte[name];
136
+ if (wert === undefined)
137
+ throw new Error(`meldung(${schluessel}): Platzhalter {${name}} ohne Wert`);
138
+ return String(wert);
139
+ });
140
+ }
141
+ /** Gilt der Satz auf dieser Seite? */
142
+ export function meldungGiltFuer(schluessel, seite) {
143
+ const eintrag = MELDUNGEN[schluessel];
144
+ return eintrag.nur === undefined || eintrag.nur.includes(seite);
145
+ }
@@ -22,11 +22,15 @@ import type { PosPaperSize } from '../printing/escpos.js';
22
22
  *
23
23
  * Bewusst **nicht** enthalten: Firmenlogo und Rasterbilder (brauchen
24
24
  * Bildverarbeitung, die im Browser anders aussieht als in Node), PDF-Erzeugung
25
- * und Druckeransteuerung. Von den anbieterspezifischen Kartenzahlungsbloecken
26
- * des Vorbilds (GP Tom, SumUp, myPOS, Stripe) ist nur der **Hobex-Block**
27
- * enthalten: die Browser-Kasse bedient das HPS-Terminal selbst (via Kasseneck
28
- * Connect) und druckt den Bon aus genau diesem Modell die uebrigen Anbieter
29
- * drucken in ihren eigenen Apps.
25
+ * und Druckeransteuerung.
26
+ *
27
+ * Die anbieterspezifischen Kartenzahlungsbloecke sind dagegen **alle**
28
+ * enthalten (Hobex, GP Tom, SumUp, myPOS, Stripe). Bis 0.8.0 trug das Modell
29
+ * nur den Hobex-Block, weil die Browser-Kasse nur das HPS-Terminal selbst
30
+ * bedient und die uebrigen Anbieter „in ihren eigenen Apps drucken". Das
31
+ * Argument gilt fuer die Frage, wer das TERMINAL bedient — nicht dafuer, was
32
+ * auf dem BELEGDOKUMENT steht. Aus der einen Regel wurde die andere, und
33
+ * derselbe Beleg trug im PDF weniger als auf dem Bon.
30
34
  */
31
35
  /** Ausrichtung einer Zeile bzw. einer Spalte. */
32
36
  export type LayoutAlign = 'left' | 'center' | 'right';
@@ -129,6 +133,8 @@ export interface BuildReceiptLayoutOptions {
129
133
  * diesem Paket ausnahmslos ganzzahlig gerechnet).
130
134
  */
131
135
  export declare function formatCents(cents: number): string;
136
+ /** Zwilling von `stripeReceiptLines` (print_paper.dart). */
137
+ export declare function stripeReceiptLines(daten: Record<string, unknown>, cardPaymentId: string | null): string[];
132
138
  /**
133
139
  * Hinweis auf die Steuerbefreiung, den ein Kleinunternehmerbeleg tragen muss —
134
140
  * ohne ihn stuende in der USt-Tabelle „D 0 %" ohne jede Begruendung, und ab
@@ -60,6 +60,36 @@ function originalDatum(timeStamp) {
60
60
  * `typeof null === 'object'` ist, ergab das beim Lesen von `.label` einen
61
61
  * nackten TypeError.
62
62
  */
63
+ /**
64
+ * Die anbieterspezifischen Kartenzahlungsbloecke.
65
+ *
66
+ * WARUM DIESE BLOECKE HIER STEHEN
67
+ *
68
+ * Bis 0.8.0 trug dieses Modell nur den Hobex-Block, mit der Begruendung, die
69
+ * uebrigen Anbieter druckten "in ihren eigenen Apps". Das Argument gilt fuer
70
+ * die Frage, wer das TERMINAL bedient -- nicht dafuer, was auf dem
71
+ * BELEGDOKUMENT steht. Aus der einen Regel wurde die andere, und damit
72
+ * verschwanden vier Bloecke: Wer denselben Beleg als PDF oeffnete, sah "nur
73
+ * Kartenzahlung", waehrend der Bon daneben Marke, letzte Ziffern, Betrag und
74
+ * Referenz auswies. Vorher (backend `generateReceiptPdf`) hatte das PDF alle
75
+ * sieben Anbieter.
76
+ *
77
+ * Gebaut werden die Zeilen deshalb hier, an genau EINER Stelle. Wer sie
78
+ * anderswo noch einmal baut, oeffnet die Drift wieder.
79
+ *
80
+ * Vorbild ist `print_paper.dart` im Dart-Paket (`_gpTom`, `_hobexHps`,
81
+ * `_sumup`, `_mypos`, `_stripe`); die Fixtures unter `fixtures/belege/karte-*`
82
+ * halten je Anbieter eine Zeile-fuer-Zeile-Probe fest.
83
+ */
84
+ /** Ein Feld aus `cardPaymentData` als Text; fehlend/null wird leer. */
85
+ function kartenFeld(daten, schluessel) {
86
+ const v = daten[schluessel];
87
+ return v == null ? '' : String(v);
88
+ }
89
+ /** Nicht-leere Teile mit Mittelpunkt verbinden. */
90
+ function verbunden(teile) {
91
+ return teile.filter(Boolean).join(' · ');
92
+ }
63
93
  /**
64
94
  * Kartenzahlungsblock fuer Hobex (HPS am Geraet wie Cloud-API): die Felder
65
95
  * kommen aus `cardPaymentData` in der Form von [hobexReceiptToCardPaymentData]
@@ -67,43 +97,234 @@ function originalDatum(timeStamp) {
67
97
  * Antwortcode. Bei CVM "1" verlangt die Karte eine Unterschrift; dafuer
68
98
  * bekommt der Bon eine Linie. Der PAN haengt beim HPS das Ablaufdatum mit
69
99
  * Unterstrich an ("…4720_2810") — gedruckt wird nur die maskierte Nummer.
70
- *
71
- * Andere Anbieter (GP Tom, SumUp, myPOS, Stripe) drucken in ihren eigenen
72
- * Apps; ihre Bloecke bleiben bewusst draussen (siehe Kopfkommentar).
73
100
  */
74
- function hobexKartenblock(receipt) {
75
- const provider = receipt.creditCardProvider;
76
- const daten = receipt.cardPaymentData;
77
- if ((provider !== 'hobexHps' && provider !== 'hobexCloudApi') || daten == null) {
78
- return [];
79
- }
80
- const feld = (k) => {
81
- const v = daten[k];
82
- return v == null ? '' : String(v);
83
- };
84
- const paar = (teile) => teile.filter(Boolean).join(' · ');
85
- const pan = feld('cardNumber').split('_')[0] ?? '';
86
- const rc = feld('responseCode');
101
+ function hobexBlock(daten) {
102
+ const feld = (k) => kartenFeld(daten, k);
87
103
  const zeilen = [textZeile('Hobex Beleg', 'center', true)];
88
104
  const inhalt = [
89
105
  feld('date'),
90
- paar([feld('tid') && `TID ${feld('tid')}`, feld('no') && `Nr. ${feld('no')}`]),
91
- paar([feld('type'), feld('cardBrand')]),
92
- pan,
93
- rc && `RC ${rc}`,
106
+ verbunden([feld('tid') && `TID ${feld('tid')}`, feld('no') && `Nr. ${feld('no')}`]),
107
+ verbunden([feld('type'), feld('cardBrand')]),
108
+ feld('cardNumber').split('_')[0] ?? '',
109
+ feld('responseCode') && `RC ${feld('responseCode')}`,
110
+ ];
111
+ for (const zeile of inhalt) {
112
+ if (zeile)
113
+ zeilen.push(textZeile(zeile, 'center'));
114
+ }
115
+ if (feld('cvm') === '1')
116
+ zeilen.push(...unterschriftsfeld());
117
+ return zeilen;
118
+ }
119
+ /** Zwei Leerzeilen, Linie, Wort -- das Unterschriftsfeld auf dem Bon. */
120
+ function unterschriftsfeld() {
121
+ return [
122
+ { kind: 'space', lines: 2 },
123
+ textZeile('------------------', 'center'),
124
+ textZeile('Unterschrift', 'center'),
125
+ ];
126
+ }
127
+ /**
128
+ * GP-Tom-`transactionType`: das Plugin-`toMap` schreibt den Schluessel mit
129
+ * Tippfehler (`transacitonType`), aeltere Daten tragen ihn gar nicht. Beide
130
+ * Schreibweisen werden gelesen -- ein Beleg von damals soll nicht anders
131
+ * aussehen als einer von heute.
132
+ */
133
+ function gpTomVorgangsart(daten) {
134
+ switch (daten.transactionType ?? daten.transacitonType) {
135
+ case 1: return 'Sale';
136
+ case 2: return 'Void';
137
+ case 3: return 'Refund';
138
+ case 4: return 'Close Batch';
139
+ default: return '';
140
+ }
141
+ }
142
+ /** GP-Tom-Betrag: ganze Zahl = Cent, sonst schon ein Eurobetrag. */
143
+ function gpTomBetrag(wert) {
144
+ if (typeof wert !== 'number')
145
+ return '-';
146
+ return Number.isInteger(wert) ? formatCents(wert) : wert.toFixed(2).replace('.', ',');
147
+ }
148
+ function gpTomBlock(daten) {
149
+ const feld = (k) => kartenFeld(daten, k);
150
+ const art = gpTomVorgangsart(daten);
151
+ const zeilen = [textZeile('GP Tom Beleg', 'center', true)];
152
+ const inhalt = [
153
+ `Batch: ${feld('batchNumber')}`,
154
+ `Receipt: ${feld('externalTransactionID')}`,
155
+ `TID: ${feld('terminalID')}`,
156
+ feld('emvAid'),
157
+ (daten.emvAppLable != null || daten.cardDataEntry != null)
158
+ ? `${feld('emvAppLable')} ${feld('cardDataEntry')}`.trim() : '',
159
+ feld('cardNumber'),
160
+ `${art ? `${art} ` : ''}Amount ${feld('currencyCode')} ${gpTomBetrag(daten.amount)}`,
161
+ daten.pinOk === true ? 'PIN OK' : 'PIN NOT OK',
162
+ `Authorization Code ${feld('approvedCode')}`,
163
+ `Sequence Number: ${feld('sequenceNumber')}`,
94
164
  ];
95
165
  for (const zeile of inhalt) {
96
166
  if (zeile)
97
167
  zeilen.push(textZeile(zeile, 'center'));
98
168
  }
99
- if (feld('cvm') === '1') {
100
- zeilen.push({ kind: 'space', lines: 2 });
101
- zeilen.push(textZeile('------------------', 'center'));
102
- zeilen.push(textZeile('Unterschrift', 'center'));
169
+ return zeilen;
170
+ }
171
+ function sumupBlock(daten) {
172
+ const feld = (k) => kartenFeld(daten, k);
173
+ const betrag = typeof daten.amount === 'number'
174
+ ? (Number.isInteger(daten.amount) ? formatCents(daten.amount) : daten.amount.toFixed(2).replace('.', ','))
175
+ : '-';
176
+ return [
177
+ textZeile('Sumup Beleg', 'center', true),
178
+ paarZeile('Kartentyp:', feld('cardType') || 'n/a'),
179
+ paarZeile('Kartennummer:', `**** **** **** ${feld('cardLastDigits')}`),
180
+ paarZeile('Zahlungstyp:', feld('paymentType') || 'n/a'),
181
+ paarZeile('Gesamtbetrag:', `${betrag} ${feld('currency')}`.trim()),
182
+ paarZeile('Transaktionscode:', feld('transactionCode') || '-'),
183
+ paarZeile('Modus:', feld('entryMode').toUpperCase()),
184
+ ];
185
+ }
186
+ /**
187
+ * myPOS liefert den Zeitpunkt als `YYMMDDhhmmss` ohne Trenner. Ein Wert, der
188
+ * nicht so aussieht, wird UNVERAENDERT durchgereicht statt zerschnitten: eine
189
+ * halb geratene Uhrzeit auf einem Beleg ist schlimmer als ein roher Wert.
190
+ */
191
+ function myposZeitpunkt(wert) {
192
+ const t = /^(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/.exec(wert);
193
+ if (!t)
194
+ return wert;
195
+ return `${t[3]}.${t[2]}.20${t[1]} ${t[4]}:${t[5]}:${t[6]}`;
196
+ }
197
+ function myposBlock(daten) {
198
+ const feld = (k) => kartenFeld(daten, k);
199
+ const zeilen = [
200
+ textZeile('MyPos Beleg', 'center', true),
201
+ paarZeile('TERMINAL ID:', feld('TID') || '-'),
202
+ paarZeile('DATUM:', myposZeitpunkt(feld('date_time'))),
203
+ ];
204
+ if (feld('application_name'))
205
+ zeilen.push(textZeile(feld('application_name'), 'center', true));
206
+ zeilen.push(paarZeile('KARTE:', feld('pan') || '-'));
207
+ if (daten.signature_required === true)
208
+ zeilen.push(...unterschriftsfeld());
209
+ zeilen.push(paarZeile('STAN:', feld('STAN') ? feld('STAN').padStart(6, '0') : '-'));
210
+ zeilen.push(paarZeile('AUTH. CODE:', feld('authorization_code') || '-'));
211
+ zeilen.push(paarZeile('RRN:', feld('reference_number') || '-'));
212
+ zeilen.push(paarZeile('AID:', feld('AID') || '-'));
213
+ return zeilen;
214
+ }
215
+ const STRIPE_KARTENART = {
216
+ debit: 'Debitkarte',
217
+ credit: 'Kreditkarte',
218
+ prepaid: 'Prepaid-Karte',
219
+ };
220
+ const STRIPE_WALLET = {
221
+ apple_pay: 'Apple Pay',
222
+ google_pay: 'Google Pay',
223
+ };
224
+ /** EPS-Bank-Slug (`bank_austria`) -> Anzeigename (`Bank Austria`). */
225
+ function stripeEpsBank(slug) {
226
+ return slug.split('_').filter(Boolean)
227
+ .map((w) => w[0].toUpperCase() + w.slice(1)).join(' ');
228
+ }
229
+ function stripeMarkenZeile(daten) {
230
+ const teile = [];
231
+ if (daten.cardBrand != null)
232
+ teile.push(String(daten.cardBrand));
233
+ const art = STRIPE_KARTENART[String(daten.cardFunding)];
234
+ if (art)
235
+ teile.push(art);
236
+ let zeile = teile.join(' ');
237
+ if (daten.wallet != null) {
238
+ const wallet = STRIPE_WALLET[String(daten.wallet)] ?? String(daten.wallet);
239
+ zeile = zeile ? `${zeile} (${wallet})` : `(${wallet})`;
240
+ }
241
+ return zeile;
242
+ }
243
+ /**
244
+ * Stripe-Zahlzeitpunkt: Sekunden seit Epoche, gedruckt in **Wiener** Zeit.
245
+ * Ueber `toViennaWallClock`, nicht ueber die lokale Zeitzone -- ein Beleg
246
+ * traegt Ortszeit des Betriebs, nicht die des Geraets, das ihn oeffnet.
247
+ */
248
+ function stripeZeitpunkt(sekunden) {
249
+ const t = toViennaWallClock(new Date(sekunden * 1000));
250
+ const zwei = (n) => String(n).padStart(2, '0');
251
+ return `${zwei(t.day)}.${zwei(t.month)}.${t.year} ${zwei(t.hour)}:${zwei(t.minute)}`;
252
+ }
253
+ /** Zwilling von `stripeReceiptLines` (print_paper.dart). */
254
+ export function stripeReceiptLines(daten, cardPaymentId) {
255
+ const zeilen = [];
256
+ const art = daten.paymentMethodType == null ? null : String(daten.paymentMethodType);
257
+ if (art === 'card') {
258
+ const marke = stripeMarkenZeile(daten);
259
+ if (marke)
260
+ zeilen.push(marke);
261
+ if (daten.cardLastDigits != null)
262
+ zeilen.push(`**** **** **** ${daten.cardLastDigits}`);
263
+ if (daten.threeDSecure === 'authenticated')
264
+ zeilen.push('3-D Secure: ja');
265
+ }
266
+ else if (art === 'eps') {
267
+ if (daten.epsBank != null)
268
+ zeilen.push(`EPS - ${stripeEpsBank(String(daten.epsBank))}`);
269
+ }
270
+ else if (art != null) {
271
+ zeilen.push(art.toUpperCase());
272
+ }
273
+ if (typeof daten.amount === 'number' && Number.isInteger(daten.amount)) {
274
+ const waehrung = daten.currency == null ? 'EUR' : String(daten.currency).toUpperCase();
275
+ zeilen.push(`Gesamtbetrag ${formatCents(daten.amount)} ${waehrung}`);
276
+ }
277
+ if (typeof daten.paidAt === 'number' && Number.isInteger(daten.paidAt)) {
278
+ zeilen.push(`Bezahlt: ${stripeZeitpunkt(daten.paidAt)}`);
279
+ }
280
+ if (daten.statementDescriptor != null)
281
+ zeilen.push(`Abrechnung: ${daten.statementDescriptor}`);
282
+ if (cardPaymentId != null)
283
+ zeilen.push(`Referenz: ${cardPaymentId}`);
284
+ return zeilen;
285
+ }
286
+ function stripeBlock(daten, cardPaymentId) {
287
+ const zeilen = [textZeile('Online-Zahlung (Stripe)', 'center', true)];
288
+ for (const zeile of stripeReceiptLines(daten, cardPaymentId)) {
289
+ zeilen.push(textZeile(zeile, 'center'));
103
290
  }
104
- zeilen.push({ kind: 'space', lines: 1 });
105
291
  return zeilen;
106
292
  }
293
+ /**
294
+ * Der Kartenzahlungsblock des Belegs -- fuer JEDEN Anbieter, den das Backend
295
+ * kennt. Ohne `cardPaymentData` bleibt der Block weg; ein unbekannter Anbieter
296
+ * ebenfalls, still und ohne Fehler: ein Beleg muss sich immer zeigen lassen.
297
+ */
298
+ function kartenblock(receipt) {
299
+ const daten = receipt.cardPaymentData;
300
+ if (daten == null)
301
+ return [];
302
+ const zeilen = (() => {
303
+ switch (receipt.creditCardProvider) {
304
+ case 'hobexHps':
305
+ case 'hobexCloudApi':
306
+ return hobexBlock(daten);
307
+ case 'gpTomAndroid':
308
+ case 'gpTomIos':
309
+ return gpTomBlock(daten);
310
+ case 'sumup':
311
+ return sumupBlock(daten);
312
+ case 'myposPro':
313
+ return myposBlock(daten);
314
+ case 'stripe':
315
+ return stripeBlock(daten, receipt.cardPaymentId ?? null);
316
+ default:
317
+ return [];
318
+ }
319
+ })();
320
+ if (zeilen.length === 0)
321
+ return [];
322
+ // Eine Leerzeile hinter JEDEM Block, nicht nur hinter manchen. Das Vorbild
323
+ // ist hier uneinheitlich (nur `_sumup` setzt am Ende ein `addFeed()`), was
324
+ // den Abstand zur Fusszeile vom Anbieter abhaengig machte. Die Leerzeile
325
+ // gehoert aber zum Blatt, nicht zum Terminal.
326
+ return [...zeilen, { kind: 'space', lines: 1 }];
327
+ }
107
328
  function zahlungsartText(wert) {
108
329
  if (wert != null && typeof wert === 'object') {
109
330
  return wert.label;
@@ -697,7 +918,7 @@ export function buildReceiptLayout(receipt, company, options = {}) {
697
918
  lines.push({ kind: 'qr', data: receipt.qr });
698
919
  lines.push({ kind: 'space', lines: 1 });
699
920
  // --- Kartenzahlungsblock (Hobex) — wie im Vorbild UNTER dem QR-Code.
700
- lines.push(...hobexKartenblock(receipt));
921
+ lines.push(...kartenblock(receipt));
701
922
  // --- Dankestext und Fusszeilen
702
923
  if (company.thanksMessage.length > 0) {
703
924
  lines.push({ kind: 'space', lines: 1 });
@@ -0,0 +1,44 @@
1
+ {
2
+ "company": {
3
+ "companyName": "Bäckerei Muster",
4
+ "street": "Hauptplatz 1",
5
+ "zip": "4830",
6
+ "city": "Hallstatt",
7
+ "phone": "+43 6134 12345",
8
+ "uid": "ATU12345678",
9
+ "taxnr": "",
10
+ "isSmallBusiness": false,
11
+ "footer1": "Vielen Dank für Ihren Einkauf",
12
+ "footer2": "www.baeckerei-muster.at",
13
+ "thanksMessage": [],
14
+ "showKreiseckLogo": false
15
+ },
16
+ "receipt": {
17
+ "receiptId": "AT0-KASSE1-66",
18
+ "cashregisterId": "KASSE1",
19
+ "timeStamp": "2026-08-13T10:15:30",
20
+ "items": [
21
+ {
22
+ "name": "Behandlung",
23
+ "quantity": 1,
24
+ "unitPriceCents": 12000,
25
+ "vatRate": 20
26
+ }
27
+ ],
28
+ "vouchers": [],
29
+ "paymentMethod": "creditCard",
30
+ "turnoverCounterAES256ICM": "UMSATZ",
31
+ "signaturePreviousReceipt": "VORGAENGER",
32
+ "certificateSerialNumber": "6F0404F0",
33
+ "receiptType": "standard",
34
+ "sig": "eyJhbGciOiJFUzI1NiJ9.QVQx.SIGNATURWERT",
35
+ "qr": "_R1-AT1_KASSE1_AT0-KASSE1-66_2026-08-13T10:15:30_120,00_0,00_0,00_0,00_0,00_UMSATZ_VORGAENGER_6F0404F0_SIGNATUR",
36
+ "fullReceiptId": "VOLL",
37
+ "customerDetails": [],
38
+ "legalMessage": [],
39
+ "creditCardProvider": "custom"
40
+ },
41
+ "options": {
42
+ "paperSize": "mm80"
43
+ }
44
+ }
@@ -0,0 +1,59 @@
1
+ {
2
+ "company": {
3
+ "companyName": "Bäckerei Muster",
4
+ "street": "Hauptplatz 1",
5
+ "zip": "4830",
6
+ "city": "Hallstatt",
7
+ "phone": "+43 6134 12345",
8
+ "uid": "ATU12345678",
9
+ "taxnr": "",
10
+ "isSmallBusiness": false,
11
+ "footer1": "Vielen Dank für Ihren Einkauf",
12
+ "footer2": "www.baeckerei-muster.at",
13
+ "thanksMessage": [],
14
+ "showKreiseckLogo": false
15
+ },
16
+ "receipt": {
17
+ "receiptId": "AT0-KASSE1-67",
18
+ "cashregisterId": "KASSE1",
19
+ "timeStamp": "2026-08-13T10:15:30",
20
+ "items": [
21
+ {
22
+ "name": "Behandlung",
23
+ "quantity": 1,
24
+ "unitPriceCents": 12000,
25
+ "vatRate": 20
26
+ }
27
+ ],
28
+ "vouchers": [],
29
+ "paymentMethod": "creditCard",
30
+ "turnoverCounterAES256ICM": "UMSATZ",
31
+ "signaturePreviousReceipt": "VORGAENGER",
32
+ "certificateSerialNumber": "6F0404F0",
33
+ "receiptType": "standard",
34
+ "sig": "eyJhbGciOiJFUzI1NiJ9.QVQx.SIGNATURWERT",
35
+ "qr": "_R1-AT1_KASSE1_AT0-KASSE1-67_2026-08-13T10:15:30_120,00_0,00_0,00_0,00_0,00_UMSATZ_VORGAENGER_6F0404F0_SIGNATUR",
36
+ "fullReceiptId": "VOLL",
37
+ "customerDetails": [],
38
+ "legalMessage": [],
39
+ "creditCardProvider": "gpTomIos",
40
+ "cardPaymentData": {
41
+ "batchNumber": 12,
42
+ "externalTransactionID": "TX-9911",
43
+ "terminalID": "T-55",
44
+ "emvAid": "A0000000031010",
45
+ "emvAppLable": "VISA CREDIT",
46
+ "cardDataEntry": "CONTACTLESS",
47
+ "cardNumber": "**** **** **** 4720",
48
+ "currencyCode": "EUR",
49
+ "amount": 12000,
50
+ "pinOk": false,
51
+ "approvedCode": "123456",
52
+ "sequenceNumber": 42,
53
+ "transacitonType": 3
54
+ }
55
+ },
56
+ "options": {
57
+ "paperSize": "mm80"
58
+ }
59
+ }
@@ -0,0 +1,59 @@
1
+ {
2
+ "company": {
3
+ "companyName": "Bäckerei Muster",
4
+ "street": "Hauptplatz 1",
5
+ "zip": "4830",
6
+ "city": "Hallstatt",
7
+ "phone": "+43 6134 12345",
8
+ "uid": "ATU12345678",
9
+ "taxnr": "",
10
+ "isSmallBusiness": false,
11
+ "footer1": "Vielen Dank für Ihren Einkauf",
12
+ "footer2": "www.baeckerei-muster.at",
13
+ "thanksMessage": [],
14
+ "showKreiseckLogo": false
15
+ },
16
+ "receipt": {
17
+ "receiptId": "AT0-KASSE1-61",
18
+ "cashregisterId": "KASSE1",
19
+ "timeStamp": "2026-08-13T10:15:30",
20
+ "items": [
21
+ {
22
+ "name": "Behandlung",
23
+ "quantity": 1,
24
+ "unitPriceCents": 12000,
25
+ "vatRate": 20
26
+ }
27
+ ],
28
+ "vouchers": [],
29
+ "paymentMethod": "creditCard",
30
+ "turnoverCounterAES256ICM": "UMSATZ",
31
+ "signaturePreviousReceipt": "VORGAENGER",
32
+ "certificateSerialNumber": "6F0404F0",
33
+ "receiptType": "standard",
34
+ "sig": "eyJhbGciOiJFUzI1NiJ9.QVQx.SIGNATURWERT",
35
+ "qr": "_R1-AT1_KASSE1_AT0-KASSE1-61_2026-08-13T10:15:30_120,00_0,00_0,00_0,00_0,00_UMSATZ_VORGAENGER_6F0404F0_SIGNATUR",
36
+ "fullReceiptId": "VOLL",
37
+ "customerDetails": [],
38
+ "legalMessage": [],
39
+ "creditCardProvider": "gpTomAndroid",
40
+ "cardPaymentData": {
41
+ "batchNumber": 12,
42
+ "externalTransactionID": "TX-9911",
43
+ "terminalID": "T-55",
44
+ "emvAid": "A0000000031010",
45
+ "emvAppLable": "VISA CREDIT",
46
+ "cardDataEntry": "CONTACTLESS",
47
+ "cardNumber": "**** **** **** 4720",
48
+ "currencyCode": "EUR",
49
+ "amount": 12000,
50
+ "pinOk": true,
51
+ "approvedCode": "123456",
52
+ "sequenceNumber": 42,
53
+ "transactionType": 1
54
+ }
55
+ },
56
+ "options": {
57
+ "paperSize": "mm80"
58
+ }
59
+ }