kasseneck_api 6.13.0 copy "kasseneck_api: ^6.13.0" to clipboard
kasseneck_api: ^6.13.0 copied to clipboard

PlatformAndroid

RKSV-Registrierkasse für Flutter: signierte Belege, hobex- und Stripe-Kartenzahlung, Bondruck. Austrian cash register by Kreiseck.

6.13.0 #

Anlass: Das Paket hing noch an kreiseck_design 0.1.0, die Browser-Kasse laengst an einer Fassung mit den neuen, neutralen Grautoenen. App und Browser zeigten dieselbe Kasse also in zwei verschiedenen Graustufen -- die Themendatei ist ein Vertrag zwischen beiden, und er war gebrochen. Mit ^0.7.0 decken sie sich wieder.

Was man sieht: 27 Farbwerte des Kassenthemas aendern sich. Die Grautoene verlieren ihren Petrolstich (heller Text #132A2A auf #222626, Nachtgrund #131B1B auf #181A1A), die hellen Signalflaechen werden saettiger. Die Markenfarben bleiben, wie sie waren. Grund ist der Zug am Design-System vom 5. September ("neutral ramp without the petrol cast"); die Zahlenspiele auf die Paragraphennummern sind dabei entfallen.

Ein Test wurde umgebaut, nicht nachgezogen: kasse_thema_test.dart hiess "die Farben sind die Rollen des Design-Systems, keine eigenen Tabellen" und fuehrte genau so eine Tabelle aus vier abgeschriebenen Hex-Werten. Er prueft jetzt gegen kdColor(modus, rolle) und kann nicht mehr veralten.

6.12.2 #

Anlass: Die Paketseite war die erste Seite, die ein Interessent sieht -- und sie sagte nicht, wofuer das Paket da ist. Sie traegt jetzt das Kasseneck-Logo, einen englischen Einzeiler fuer Besucher von aussen, und eine Tabelle, welche Pflichten einer oesterreichischen Registrierkasse wo erledigt werden, mit Verweisen auf die Wissensseiten. Rechtliches steht bewusst nicht dort: die Tabelle beschreibt, was die Software tut, nennt Anmeldung und Aufbewahrung ausdruecklich als Sache des Unternehmers, und der Hinweis darunter sagt, dass sie keine Beratung ersetzt.

Zwei Fehler nebenbei behoben: Die Einbindeanleitung nannte ^5.0.0 -- wer ihr folgte, installierte eine Hauptversion von vor anderthalb Jahren. Und der Abschnitt "Versioning" beschrieb 5.0.0 als "Latest"; er verweist jetzt auf dieses Changelog, statt eine Zahl zu wiederholen, die veraltet.

Die Seite ist nun deutsch wie ihr JavaScript-Zwilling.

Personendaten aus einem Beispielkommentar entfernt. In lib/models/keck_invoice.dart stand seit 2.0.0 (17.03.2026) ein Rechnungsbeispiel mit Namen, Telefonnummer und Anschrift einer echten Person. Es ist durch Musterwerte ersetzt. Der Kommentar wurde mit jedem Paket ausgeliefert; die bereits veroeffentlichten Fassungen lassen sich nicht mehr aendern.

6.12.1 #

Anlass: Am Produktivterminal (TID 3556988, Firmware 2.3.9) antwortet POST /api/transaction/abort/{tid}/{tx} mit HTTP 404, am Testgeraet dagegen normal. Dieser 404 landete im allgemeinen Fehlerzweig und war im Nachweis nicht von einem Leitungsabriss zu unterscheiden -- steps ist aber der Text, der im Belastungsstreit gelesen wird.

  • Nur der Nachweistext. HpsHttpException.isNotFound benennt den 404, und der Abbruch schreibt beide Lesarten hin, ohne sich auf eine festzulegen: das Terminal kennt entweder den Vorgang nicht oder den Endpunkt nicht. Welche zutrifft, ist ungemessen (bei hobex angefragt).
  • Verhalten unveraendert: weiterhin weiterklaeren, nie ein Ausgang.

6.12.0 #

Anlass: das Backend hat seit keck#361 den Endpunkt sendReceiptEmail -- die Kasse schickt einen ausgestellten Beleg als Link auf die oeffentliche Belegseite an eine Adresse, die der Gast am Tresen nennt. In beiden Kassen ist der Weg fertig gebaut, aber hinter einer Schnittstelle, hinter der nichts steht (belegversand.dart, Schalter belegMailVerfuegbar = false): es fehlte der Aufruf im Paket. Kein PDF im Anhang -- die Belegseite setzt dasselbe Zeilenmodell wie Bildschirm und Bondrucker und gibt dort auf Wunsch eines aus.

  • Neu RegisterReceiptClient.belegSenden (Kassen-Anmeldung) und neu KasseneckApi.belegSenden (API-Schluessel-Zugang): fullReceiptId, an und optional sprache. Welche Kasse gemeint ist, entscheidet die Sitzung bzw. der cashregister-token -- es gibt bewusst kein cashregisterId am Aufruf. Zwilling: sendReceiptEmail in @kreiseck/kasseneck-api 0.13.0.
  • Neu Belegmailergebnis mit to (normalisierte Adresse), at (ISO in Wiener Zeit) und via (eigen / plattform / plattform-fallback). Gelesen wird nachsichtig: an dieser Stelle ist die Mail draussen, und ein Wurf ueber einem fehlenden Feld saehe fuer die Kasse aus wie "nicht gesendet" -- der Kassier schickte sie dann ein zweites Mal an den Gast.
  • Neu belegMailFehlercodes und istBelegMailFehlercode: die vier stabilen Codes des Backends (adresse_ungueltig, beleg_nicht_gefunden, zu_oft, versand_fehlgeschlagen). Sie kommen unveraendert als KasseneckApiError.code heraus -- die Kasse entscheidet am Code, nie am deutschen Text.
  • Die Adresse wird hier nicht auf Form geprueft. Es gibt genau eine Adresspruefung, und die steht im Backend (kreiseck_validator); eine zweite, anders strenge wiese Adressen ab, die dort durchgehen -- und mit einem Fehlertyp, an dem die Kasse nicht entscheiden kann. Abgewiesen wird hier nur die leere Angabe.
  • Neue oeffentliche Symbole: RegisterReceiptClient.belegSenden, KasseneckApi.belegSenden, Belegmailergebnis (mit Belegmailergebnis.aus), belegMailFehlercodes, istBelegMailFehlercode, Aufrufe.sendReceiptEmail -- erreichbar aus kasse.dart und kasseneck_api.dart.
  • Die Anheftung npm_version in zwillinge.yaml steht wieder auf dem veroeffentlichten Stand (0.10.0 -> 0.12.0); die Vertragsdateien unter test/fixtures/vertrag/ sind neu gezogen. Inhaltlich aendert sich daran nichts ausser der Versionszeile -- 0.11 und 0.12 haben die gemeinsame Oberflaeche nicht angefasst.

6.11.0 #

Anlass: sastre storniert noch ueber den alten Weg (cancelReceipt -> createReceipt ohne Bezug): keine Restmengen, kein Schutz vor doppeltem Storno, keine Verkettung mit dem Original. Den neuen Endpunkt gab es in diesem Paket nur fuer die Kassen-Anmeldung (RegisterReceiptClient.stornieren), nicht fuer den API-Schluessel-Zugang, ueber den sastre spricht. Der npm-Zwilling hatte ihn dort schon (cancelReceipt im Client).

  • Neu KasseneckApi.stornieren: Storno ueber den Endpunkt cancelReceipt mit Bezug, Grund (stornogruende), optionalem Teilstorno (Stornoposition) und Anmerkung. Liefert Stornoergebnis (Storno-Beleg, Bezug, Restmengen). Fachliche Ablehnungen kommen als KasseneckApiError mit code aus stornoFehlercodes.
  • Kartendaten der Erstattung (kartenanbieter, kartenzahlungId, kartenzahlungsdaten) gehen an den Storno-Beleg -- nur bei Rueckzahlweg Karte, nie die Daten der Originalzahlung. Backend: keck#371, npm 0.12.0.
  • KasseneckApiError, Stornoergebnis, Stornoposition, stornogruende, stornoFehlercodes und istStornoFehlercode sind jetzt auch aus kasseneck_api.dart erreichbar.
  • Die Abkuendigung von cancelReceipt/createCancelReceipt nennt stornieren als Ersatz.

6.10.0 #

Anlass: hobex hat am 11.09.2026 die Antwortcodeliste der HPS-Anwendung geschickt. Drei der Codes kamen seit dem 28.08.2026 im Betrieb vor (100004, 100005, 100015) und waren bis jetzt ungedeutet. Jede Zahlung damit lief in die Klaerung und endete erst ueber die Zwei-9027-Regel.

  • Alle 31 Codes der Liste stehen in HpsCodes.all, zusammen mit den gemessenen: Code, hobex-Titel, Bedeutung, Wirkung (HpsCodeEffect), Grund (HpsCodeReason) und Quelle (gemessen / dokumentiert). TransactionResponse.isConclusive liest seine Positivliste jetzt aus dieser Tabelle.

  • Was vor dem Host scheitert, ist eine Ablehnung. Kartenlesen, EMV-Kernel, Eingaben am Geraet, Geraetezustand, fehlerhafte Anfrage: declined, ohne Abbruch und ohne Statusabfrage. Dazu 100029: das Terminal storniert laut hobex selbst.

  • Neu: "ungewiss" (HpsCodeEffect.hostUncertain, TransactionResponse.isHostUncertain) fuer 100006, 100007, 100023, 100024, 100026, 100027 und 100999. Der Host war beteiligt, das Terminal storniert nicht selbst. Die Zwei-9027-Regel greift hier nicht. Ein 9027 danach heisst nur, dass das Terminal nichts gespeichert hat, nicht, dass nichts belastet ist. Die Klaerung endet nach zwei Abfragen ohne Neues als unresolved; sagt der Status 0, gilt die Genehmigung, jede andere Aussage des Terminals entscheidet nichts mehr. Ein Abbruchversuch entfaellt: der Vorgang ist am Terminal schon beendet. Bei einer Aufhebung entscheidet ein unveraendertes 0 auf die Originalzahlung dann ebenfalls nichts.

  • HpsCode.rejectsRequest / TransactionResponse.isConclusiveAsStatus: zehn Codes weisen die Anfrage selbst ab (9002, 100001, 100008, 100108, 100009, 100010, 100013, 100018, 100022, 100998). Auf eine Zahlung sind sie eine Ablehnung, auf eine STATUSABFRAGE sagen sie nichts ueber den gesuchten Vorgang (gemessen fuer 100108). Die Klaerung liest den Status deshalb ueber isConclusiveAsStatus; wer selbst Statusabfragen auswertet, sollte das auch tun. Ohne diese Unterscheidung haette ein gesperrtes Terminal (100022) eine verlorene Zahlung als "nicht belastet" ausgewiesen.

  • HpsResult.reason traegt fuer jeden Ausgang den Grund, mit einem Satz fuer den Bediener (HpsCodeReason.hint). Er wird dort gesetzt, wo entschieden wurde: ein bestaetigter Abbruch ist aborted, eine ueber die Zwei-9027-Regel geklaerte Zahlung traegt den Grund ihres eigenen Codes, HTTP 409 ist terminalBusy. HpsResult.isHostUncertain sagt einer spaeteren Nachfrage, dass ein 9027 dort nicht "nicht belastet" heisst.

  • 100011 "Not Found" ist keine Aussage, traegt aber nicht die Zwei-9027-Regel: sie ist nur fuer 9027 gemessen. Der Nachweistext dazu enthaelt bewusst nicht "keine Auskunft".

  • Der Nachweis nennt bei einer Ablehnung den hobex-Titel mit (Terminal: abgelehnt (100015 "Card declined")). Wer den Text abgelehnt (<code>) woertlich prueft, muss nachziehen.

  • TransactionResponse.isUnknownCode heisst jetzt: der Code fehlt in der Tabelle. Fuer alle bisher bekannten Codes aendert sich nichts.

  • Vertrag auf npm 0.10.0 nachgezogen. Neu test/hobex_hps_codes_vertrag_test.dart: die Codetabelle und die Saetze je Grund muessen der des npm-Zwillings gleichen, Feld fuer Feld.

6.9.1 #

Anlass: 6.9.0 enthielt versehentlich test/integration/credentials.local.json — eine ungetrackte Datei mit echten Zugangsdaten der Demo-Kasse. Ursache: .pubignore ersetzt .gitignore beim Veroeffentlichen; die Datei ist zwar gitignoriert, stand in .pubignore aber nicht. Frueher fiel das nicht auf, weil aus Worktrees veroeffentlicht wurde, in denen die Datei gar nicht liegt. Aeltere Fassungen sind nicht betroffen (6.8.0, 6.7.0 und 6.0.0 geprueft).

  • .pubignore schliesst jetzt test/integration/credentials.local.json, *.local.json, *.env und .env* aus. Sonst keine Aenderung gegenueber 6.9.0; wer 6.9.0 einbindet, soll auf 6.9.1 wechseln.

6.9.0 #

Anlass: Am echten Beleg fehlte der QR-Code, während die Probe im Drucker-Wizard ihn druckte. Nachgerechnet: der native Weg druckte jedes Modul fest mit sechs Punkten, unabhängig von der Papierbreite. Ein Beleg-QR mit realer RKSV-Nutzlast hat 57 Module, mit Ruhezone also (57 + 8) · 6 = 390 Druckpunkte — ein 58-mm-Drucker hat 384. Die Wizard-Probe ist kürzer (41 Module, 294 Punkte) und passte. Zu breit heißt bei den meisten Geräten nicht „abgeschnitten", sondern gar kein QR — auf einem Pflichtbeleg der schlechteste aller Ausgänge.

  • QrMass rechnet die Modulgröße, statt sie zu raten (neu, aus package:kasseneck_api/printing.dart). Aus Papierbreite, Modulanzahl und einem Deckel ergibt sich die größte Größe, bei der Symbol plus Ruhezone aufs Papier passt: floor(papierbreitePunkte / (module + 8)). Untergrenze 4 Punkte (≈ 0,5 mm bei 203 dpi — darunter tun sich billige Thermodrucker und Handykameras schwer); passt es damit nicht, sind 3 erlaubt, aber gemeldet; passt auch das nicht, sagt das Ergebnis „passt nicht". Die Modulanzahl kommt aus der QR-Rechnung für die konkrete Nutzlast bei Fehlerkorrektur M — nicht aus einer Tabelle: M braucht nie weniger Module als das L, mit dem der native Befehl druckt, die Rechnung ist damit konservativ statt knapp. Neu öffentlich: QrMass, QrGroesse, QrModulGroesse.

  • KeckPaperSize.druckPunkte (neu): 384 bzw. 576 Punkte Kopfbreite. Nicht zu verwechseln mit imageWidth (296/504) — die lässt absichtlich Rand, und wer rechnet, ob ein Symbol passt, braucht die echte Kopfbreite.

  • Ohne Wahl ändert sich nichts. QrModulGroesse.auto deckelt bei 6 — dem Wert, den der native Weg seit jeher gedruckt hat. Auf 80 mm wären gerechnet 8 Punkte möglich; ohne Deckel bekäme dort jedes Bestandsgerät ungefragt einen größeren QR. Zwei Golden-Tests halten den kompletten Bytestrom eines nativ gedruckten Belegs auf 58 und 80 mm gegen den Stand von 6.8.0. Geändert wird nur dort, wo heute gar nichts herauskommt.

  • Wählbar als Deckel: qrGroesse: an setKeckReceipt, setBelegLayout, KeckPrinterService.getPaperFromReceipt/getBytesFromReceipt/ printReceiptBluetooth, KeckPrinter.printReceipt, KasseneckReceipt.getPrintBytes/printReceiptBluetoothauto (Vorgabe), klein (4), mittel (6), gross (8). Ein Deckel hebt nie an, was nicht passt.

  • Der Notausgang: Passt das Symbol nativ auch mit 3 Punkten nicht, wird der QR als Bild gedruckt statt gar nicht. Das neue Feld PrintPaper.qrAusweich sagt, dass ausgewichen wurde und warum; es reicht bis KeckPrintResult.qrAusweich und KeckPrinterService.letzterQrAusweich. Getrennt von qrFehler, weil die Handlung eine andere ist: qrFehler heißt „Beleg ohne QR", qrAusweich heißt „gedruckt, aber der eingestellte Weg taugt für dieses Gerät nicht" — das gehört dem Chef gesagt, nicht dem Kunden. Dasselbe Feld meldet auch den Druck unter der Mindestgröße.

  • QrPrintMode.nativeModel1 (neu): der native Befehl mit ausdrücklicher Wahl von Modell 1 (GS ( k 04 00 31 41 49 00). Manche günstigen Drucker beherrschen nur diesen älteren Symboltyp — belegt ist ein Gerät, das bei Modell 2 unter dem Code eine „0" ausgibt, das Parameterbyte 0x30 des Druckbefehls, das es nicht als Befehl erkennt. QrPrintMode.native schickt weiterhin gar keinen Modellbefehl und bleibt damit byteidentisch. Wer QrPrintMode erschöpfend auswertet, bekommt einen vierten Fall.

  • PrintPaper.addQrCode nimmt size jetzt optional: ohne Angabe wird gerechnet, mit Angabe gilt sie unverändert — der Weg für Aufrufer, die genau wissen, was ihr Gerät kann. QRCode/EscPosGenerator.qrcode kennen dafür den neuen Schalter modell1.

6.8.0 #

Anlass: 6.7.0 gab qrModus die harte Vorgabe raster. Die Browser-Kasse druckt den Signatur-QR aber seit jeher ueber den nativen ESC/POS-Befehl; jedes Geraet, das nie durch den Drucker-Wizard laeuft, haette damit ploetzlich ein Rasterbild gedruckt — ueber BLE mehrere Sekunden je Bon, und niemand haette etwas umgestellt.

  • KasseQrModus.auto ist die neue Vorgabe und heisst unbestimmt: an diesem Geraet hat noch niemand am Papier entschieden. Jede Kasse bleibt dann bei ihrer bisherigen Praxis; nur ein ausdruecklich gesetzter Wert aendert etwas.

  • druckmodus ist druckmodusOder(QrPrintMode vorgabe) gewichen. Die Vorgabe ist Pflicht und hat keinen Standardwert: was auto bedeutet, weiss nur der Aufrufer (App: Rasterbild, Browser-Kasse: nativer Befehl). Ein Standardwert an dieser Stelle haette eine der beiden Kassen still umgestellt.

  • Wer KasseQrModus erschoepfend auswertet, bekommt einen dritten Fall; der Typ ist eine Version alt und hat ausser der Kassen-App keine Verbraucher.

  • Vertrag auf npm 0.9.4 nachgezogen.

6.7.0 #

Anlass: Der Drucker-Wizard der Kassen-App und der Browser-Kasse laesst zwei Probedrucke machen — den Signatur-QR einmal als Rasterbild (GS v 0), einmal ueber den nativen ESC/POS-Befehl (GS ( k) — und fragt, welcher lesbar herauskam. Diese Antwort hatte bisher nirgends Platz: beide Kassen druckten fest im Rastermodus. An Geraeten, die GS v 0 nicht koennen, kam damit ein Bon ohne lesbaren QR heraus, und das ist nach § 132a BAO keine Belegerteilung — der Ausfall faellt am Tresen niemandem auf.

  • KasseQrModus (raster | escpos) und KasseSettingsGeraet.qrModus, Vorgabe raster. Am Geraet und nicht am Betrieb: welchen Befehl ein Thermodrucker versteht, entscheidet das Modell an dieser einen Kasse. Bestehende Geraete drucken unveraendert weiter.

  • KasseQrModusDruck.druckmodus uebersetzt die Einstellung in den QrPrintMode, den KeckPrinterService.getPaperFromReceipt nimmt; beide sind jetzt aus package:kasseneck_api/printing.dart benennbar. Ohne diesen Weg liesse sich der gewaehlte Modus zwar speichern, aber nie drucken.

  • Vertrag auf npm 0.9.3 nachgezogen (zwillinge.yaml), Kopie unter test/fixtures/vertrag/ neu gezogen.

6.6.1 #

Anlass: Der GP-Tom-Kartenblock brach auf dem Thermobon mitten ab — PIN, Autorisierungscode und Sequenznummer fehlten, der Rest des Belegs stimmte.

  • pinOk wird jetzt mit == true gelesen (PrintPaper._gpTom). Vorher stand dort eine Ternary auf dem Rohwert. InquireResult.pinOk ist bool? und fehlt bei jeder kontaktlosen Zahlung ohne PIN — dem Normalfall unter 50 €. Der Zugriff warf, und der catch (_) um den Kartenblock verschluckte es: gedruckt wurde bis zur Betragszeile, die drei Zeilen danach fielen weg. Das Beleg-Widget entschied schon immer so; die beiden Pfade sind wieder deckungsgleich.

  • formatGpTomAmount antwortet auf jede Nicht-Zahl mit '-' statt zu werfen. Derselbe Riss eine Zeile höher: ein Betrag als Text (fremde Antwort, Umweg über JSON) beendete den Block schon nach der Kartennummer. Das Backend-PDF antwortet auf denselben Wert seit jeher mit '-' (functions/helper.js) — die zugesagte Parität galt für diesen Fall nicht. null, NaN und Infinity ebenso.

Nicht angefasst, aber gefunden (dieselbe Ursache, andere Anbieter): PrintPaper._mypos liest data['date_time'] ungeschützt in ein String und schneidet daraus mit substring; _hobexHps/_hobexApi reichen rohe Werte an addDoubleText(String, String); _sumup castet data['amount'] as num. Alle enden im selben catch (_) und damit in einem abgeschnittenen Block. Heute speisen die Konverter dieses Pakets diese Blöcke mit lauter Texten — die GP-Tom-Daten dagegen kommen roh aus dem Plugin, deshalb riss es dort zuerst.

6.6.0 #

Anlass: Eine GP-Tom-Zahlung aus der Kassen-App hinterließ auf dem Beleg keinen einzigen Kartenblock. RegisterReceiptClient.verkaufen kannte den Kartenanbieter gar nicht und schickte nur Kennung und Terminaldaten; im Beleg stand creditCardProvider: null, und weil Backend und Bon-Bauer daran schalten, traf kein Zweig — der Gast bekam keinen Kartenbeleg, obwohl alle Daten da waren.

  • Neu: verkaufen(kartenanbieter: …) (CreditCardProvider, optional) — geht als creditCardProvider unter dem Enum-Namen hinaus, genau wie im JS-Zwilling. Ohne Angabe steht das Feld nicht im Rumpf; das Backend entscheidet dann wie bisher.

  • Neuer Export in kasse.dart: CreditCardProvider samt kartenblockUeberschrift. Wer mit Karte kassiert, benennt den Anbieter am Verkauf und brauchte dafür bisher einen zweiten Import.

  • Ein Anbieter ohne zahlungsart: creditCard wirft einen KasseneckValidationError (kind: 'request'), bevor etwas hinausgeht: entweder ist die Zahlungsart falsch oder der Anbieter, und beides gehört an den Tresen zurück statt in die Signaturkette.

  • Bewusst nicht übernommen: die Regel des alten Wegs (KasseneckApi._createReceipt), nach der eine cardPaymentId bei jedem Anbieter außer custom Pflicht ist. Ein eigenes Terminal meldet keine Transaktionskennung; ein Verkauf, der daran scheitert, lässt das Geld geflossen und den Beleg aus. Der Anbieter geht mit, was immer sonst fehlt.

  • stornieren bekommt bewusst kein solches Argument: den Storno-Beleg baut der Server, und cancelReceipt nimmt nur items, note und paymentMethod entgegen — ein mitgegebener Anbieter fiele stumm weg. Steht jetzt am Aufruf.

6.5.1 #

  • Vertrag auf npm 0.9.0 gezogen. Damit liegen die neun Golden-Belege für Kartenzahlungen auch hier — je einer pro Anbieter (Hobex HPS und Cloud, GP Tom Android und iOS, SumUp, myPOS, Stripe mit Karte und mit EPS, dazu ein eigener Anbieter ohne Terminaldaten). beleg_raster_test.dart rendert sie durch das Dart-Raster und vergleicht zeichengenau gegen grid32.txt und grid48.txt des Pakets: der Renderer hier ist damit an den Bauer dort gebunden, Zeichen für Zeichen.

    Das ist der Teil, der Drift künftig unmöglich macht — nicht die Blöcke selbst. Wer im Paket einen Anbieter aufnimmt, bekommt dort einen roten Test, bis ein Golden dazuliegt; und dieser Golden landet beim nächsten Ziehen hier und muss zeichengenau stimmen.

  • Der Testname nannte „22 Belege" und war nach dem ersten neuen Beleg falsch. Geprüft wird die Deckung mit dem Manifest, nicht eine Anzahl.

6.5.0 #

Anlass: Bei einer Stripe-Zahlung standen auf dem Bon aus der App Marke, letzte vier Ziffern, 3-D Secure, Betrag, Zahlzeitpunkt und Referenz. Dasselbe Blatt als PDF geöffnet sagte nur „Zahlungsart: Kartenzahlung".

  • Der Bon kommt jetzt aus dem gelieferten Zeilenmodell. Der Beleg wird an EINER Stelle gebaut — im Backend, über @kreiseck/kasseneck-api; alles hier rendert nur noch. Es gab zwei Bauer: das Zeilenmodell und setKeckReceipt. Wo sie sich unterschieden, zeigte derselbe Beleg je nach Oberfläche etwas anderes. KeckPrinterService.getPaperFromReceipt nimmt setBelegLayout, sobald ein Layout vorliegt.

  • setBelegLayout folgt jetzt dem QR-Modus. Es rief fest den nativen Befehl GS ( k. Damit hätten Drucker ohne diesen Befehl gar keinen QR gedruckt, sobald der Bon aus dem Zeilenmodell kommt — und auf einer österreichischen Kassa ist der QR die maschinenlesbare Signatur. Beide Wege gehen jetzt durch dieselbe Stelle (_qrNachModus), damit ein neuer Modus nicht an einem von beiden vorbeigeht. setBelegLayout ist dadurch async.

  • Neu: KasseneckReceipt.layoutIstVollstaendig. Zeigt das gelieferte Zeilenmodell alles, was der Beleg hergibt? Trägt der Beleg Kartendaten, das Layout aber keinen Block dazu, dann stammt es von einem Backend vor Paket 0.9.0 — dann druckt der alte Bauer weiter. Bewusst kein Versionsvergleich: eine Zahl im Layout wäre eine zweite Zusage, die selbst wieder driften kann. Gefragt wird die Sache selbst.

    Damit ist die Ausrollreihenfolge entschärft: ein Gerät, das gegen ein älteres oder zurückgerolltes Backend spricht, verliert die Kartenzahlungsblöcke nicht.

  • Die Überschriften der Kartenblöcke stehen jetzt einmal, bei CreditCardProvider (kartenblockUeberschrift). Bauer und Prüfung lesen dieselbe Tabelle — sonst griffe die Prüfung ins Leere, sobald jemand eine Überschrift ändert.

Wegwerfen, sobald kein Backend unter Paket 0.9.0 mehr im Feld ist: layoutIstVollstaendig, PrintPaper.setKeckReceipt und KeckReceiptWidget. Dann gibt es wirklich nur noch einen Bauer — und print_widget_consistency_test.dart, der heute die beiden Dart-Bauer gegeneinander hält, wird überflüssig. Das ist der Maßstab dafür, dass wir fertig sind.

6.4.0 #

Anlass: Vorfall vom 02.09.2026 am Produktivterminal 3556988 (HPS 1.11.4, Firmware 2.3.9) — die erste echte Host-Ablehnung, die je beobachtet wurde.

  • 55 („PIN falsch") ist jetzt eine gemessene Ablehnung → declined. Die Zahlung antwortete direkt mit 55, die Statusabfrage danach elfmal in Folge ebenfalls. Der Code war eine Wissensluecke: 90 s Klaerung ins Budget, unresolved, stehender Merker, Rueckfrage an den Bediener — fuer eine falsch getippte PIN. Neu: TransactionResponse.wrongPinCode, in der Positivliste.

    Zwei Befunde, die ueber den einen Code hinausgehen (doc/kartenzahlung.md): eine vom Host abgelehnte Zahlung wird am Terminal aufbewahrt und ist mit ihrem Code abrufbar — anders als ein abgebrochener Vorgang, der danach 9027 antwortet. Und Host-Codes sind zweistellig (ISO 8583); die Zwei-9027-Regel aus 5.2.0 greift bei ihnen deshalb nicht, sie enden bis zur Messung weiterhin bei unresolved. Bewusst keine Familienregel fuer zweistellige Codes: dort stehen auch Genehmigungen (08, 10, 11, 85).

  • Neu: HpsResult.lastResponse — was das Terminal zuletzt sagte, auch wenn der Ausgang offen bleibt. Am 02.09.2026 sah der Bediener nur „Ausgang unklar", musste raten und buchte die abgelehnte Zahlung als bezahlt; 75 EUR Umsatz waren weg. Der Klartext „PIN falsch" haette die Entscheidung getragen. response bleibt bei unresolved weiterhin null (kein Beleg aus einer Nicht-Aussage); lastResponse ist Material fuer Anzeige und Katalog, nie ein Beleg. Bei schluessigem Ausgang ist es null.

  • Der Nachweis nennt bei einem unbekannten Code den Klartext des Terminals: Terminal nennt einen unbekannten Code (55) "PIN falsch". Nur dort — bei gemessenen Codes ist die Bedeutung benannt.

6.3.0 #

  • Neu: RegisterClient.listRegisterSessionsForDevice — welche Sitzungen hält diese Kasse gerade (Lizenzplätze, Sitzungen mit Gerät, Zeiten, selbst, Benutzername im Auswahlmodus). Zwilling von npm 0.6.48; Ausweis über das Gerätegeheimnis wie bei listRegisterUsersForDevice.
  • Neu: registerUserLogin(takeoverSessionId: …) — welche Sitzung bei einer Übernahme weichen soll. Ohne Angabe verdrängt das Backend wie bisher die älteste; eine nicht mehr laufende Kennung wird abgewiesen, nie still ersetzt.
  • Vertrag: der Aufruf ist damit kein offener Posten mehr (Issue #44).

6.2.0 #

  • Veraltet: KasseneckApi.cancelReceipt und KasseneckApi.createCancelReceipt (@Deprecated). Das ist der alte Storno-Weg über createReceipt ohne Bezug zum Original — keine Restmengen, kein Schutz vor doppeltem Storno, Gutscheine werden nicht zurückgenommen. Das Backend nimmt ihn weiter an und legt deprecation in die Antwort. Stattdessen RegisterReceiptClient.stornieren (kasse.dart): Bezug, Grund, Teilstorno, stabile Fehlercodes. Nichts entfällt, nur der Analyzer weist hin.

6.1.0 #

Anlass: Das Backend legt bei jedem fachlichen Storno-Fehler einen stabilen code neben die Meldung und nimmt einen Rabattgutschein jetzt bei jedem Storno anteilig der Menge zurück (keck#314). Das Paket reicht beides durch — nichts entfällt.

  • KasseneckApiError.code (neu, optional): der code der Antworthülle, wenn der Endpunkt einen legt — heute cancelReceipt. Nur ein nicht leerer Text zählt; sonst null. Entscheide am Code, nie am Text.
  • stornoFehlercodes und istStornoFehlercode in kasse.dart: der Katalog der vierzehn Codes, dieselbe Liste wie functions/storno-core.js und @kreiseck/kasseneck-api.
  • Storno-Einträge (KasseneckReceipt.cancellations) können promoAdjustmentCents tragen (Cent je Steuertopf, was dieser Storno an Rabatt zurückgenommen hat); die Lesung war schon tolerant, das README beschreibt es jetzt.
  • README: neuer Abschnitt „Cancellations (Storno)" mit Aufruf, Fehlercodes und Gutschein-Regeln.

6.0.0 #

Anlass: Das Design-System kreiseck_design liefert jetzt Farbe, Form und Modi für alle vier Kassenstile; Kassenthema hielt sie bisher als einzige Quelle in vier handgetippten Tabellen. Dieses Release löst die Tabellen auf — Kassenthema wird eine dünne Sicht auf die Rollen des Design-Systems, das Kassen-Fach (Kachelhöhe, Spalten, Kachelstil, Emoji, Kategoriefarben, Schriftfaktor) bleibt unverändert bestehen.

  • Breaking: Stilfarben, stilfarben und markenfarbe entfallen. Die vier Farbtabellen der Stile (klar/warm/nacht/kontrast) und die feste Markenkonstante waren handgetippte Zwillinge dessen, was kreiseck_design jetzt als Rollen liefert.
  • Breaking: Kassenthema.farben entfällt, ebenso die fünf Konstruktor-Parameter farben, marke, markeTief, markeHell, aufMarke — an ihrer Stelle steht der neue Parameter modus (KdMode). Wer Kassenthema bisher selbst konstruiert hat statt über Kassenthema.aus(...), muss umstellen.
  • Alle Farb-Getter bleiben (grund, flaeche, flaecheHoch, text, leise, rand, strich, gut, gutHell, warnung, warnungHell, fehler, fehlerHell, marke, markeTief, markeHell, aufMarke) — ihre Werte kommen jetzt aus den Rollen von kreiseck_design für den Modus des jeweiligen Stils (klarlight, warmwarm, nachtdark, kontrastcontrast), nicht mehr aus eigenen Tabellen. Die Betriebsfarbe färbt die Kasse weiterhin nicht — unverändert seit 5.x, jetzt nur anders begründet.
  • Radien und Linie kommen jetzt aus dem Design-System: radius 14, radiusKachel/ radiusKlein 10, linie 1,5 (Kontraststil 2) — in jedem Modus gleich. Der Kontrast-Stil verkleinert die Radien nicht mehr (bisher 6/6/4) — er bleibt an schärferen Linien und fehlenden Schatten erkennbar, nicht mehr an kleineren Radien.
  • Golden-Datei (test/fixtures/kasse/kasse-thema.json) neu geschrieben: in allen vier Stilen ändern sich sämtliche Farbwerte (neue Rampen), dazu radiusKachel/radiusKlein von 12/8 auf 10 und linie von 1 auf 1,5 in jedem Stil, im Kontraststil zusätzlich radius von 6 auf 14. Unverändert: schriftfaktoren, kachelhoehen, kachelhoeheRegel, schattenTiefe — reine Kassen-Fachwerte, die das Design-System nicht berührt.
  • Neue Abhängigkeit: kreiseck_design (vorerst als Pfadabhängigkeit, bis zur Veröffentlichung auf pub.flutter-io.cn).
  • Breaking: Farbe.ausHex verlangt jetzt ersatz. Der bisherige Vorgabewert war das Panel-Blau (#1B46F5) — ein stilles Detail dieses Pakets, das an jeder Aufrufstelle unbemerkt mitlief. Wer bisher ohne ersatz aufgerufen hat, muss ihn jetzt benennen.
  • SDK-Untergrenzen angehoben: sdk: ^3.12.1, flutter: ">=3.44.0"Color.toARGB32() gibt es erst ab Flutter 3.27, und kreiseck_design selbst verlangt 3.44.

5.2.1 #

  • Behoben: eine zu lange Spalte verlor auf dem Bon still ihren Rest. EscPosGenerator.row splittet zu langen Spalteninhalt korrekt in eine Fortsetzungszeile ab, rief sie dann aber nur auf und verwarf das Ergebnis (row(nextRow); statt bytes += row(nextRow);). Alles, was ueber die Spaltenbreite hinausging, fiel damit ersatzlos weg.

    Herkunft: der Erzeuger wurde mit 4.0.0 aus esc_pos_utils 1.1.0 uebernommen — dort steht der Fehler. esc_pos_utils_plus hatte ihn behoben, und genau diese Fassung war bis 3.3.0 im Einsatz. Beim Loesen von esc_pos_utils_plus ging die Korrektur mit verloren; seit 4.0.0 ist sie in jeder veroeffentlichten Fassung fort.

    Wirkung: ein Artikelname, der laenger ist als seine Spalte, wurde auf dem gedruckten Beleg abgeschnitten. Gemessen am 28.08.2026 im Vergleich der Bon-Bytes von 3.3.0 gegen 5.2.0, 80 mm: aus 3 x Marmelade Himbeere je 4,90 wurde 3 x Marmelade Himbeere je , der Einzelpreis fehlte; aus einem langen Artikelnamen fiel die zweite Haelfte weg. Der Beleg zeigte damit etwas anderes als verkauft wurde — und zwar auf Papier, das der Kunde mitnimmt.

    Zwei Tests halten es fest: dass jedes Zeichen einer ueberlangen Spalte in den Bytes ankommt, und dass eine passende Zeile einzeilig bleibt, waehrend eine zu lange eine Fortsetzung bekommt. Beide fallen ohne die Korrektur.

    Der zweite zaehlt dafuer ZEILENVORSCHUEBE, nicht Bytes. Die naheliegende Pruefung "die umbrochene Zeile ist laenger" faellt auf den Fehler nicht herein -- sie ist auch ohne die Korrektur wahr (gemessen 35 gegen 24 Bytes), weil die abgeschnittene Spalte auf ihre volle Breite aufgefuellt wird. Sie waere ein Test gewesen, der nie rot wird.

5.2.0 #

Anlass: Zwei Luecken, beide am 28.08.2026 am Terminal 3600335 gemessen.

  • Abgelehnte Alltagsfaelle enden nicht mehr bei „Ausgang unklar". declined entstand nur aus den Codes der Positivliste. Jeder andere galt als Wissensluecke und wurde unresolved — mit Warnung, stehendem Merker und ohne einfaches Nochmal. Das trifft ausgerechnet den haeufigsten Fall im Alltag: „keine Deckung" oder „Karte abgelaufen", deren Codes nie gemessen wurden.

    Neu in der Positivliste: 9003 („Invalid Amount"), 100019 („Amount is not in a valid range") und 100108 („Invalid TID"). Alle drei weist das Terminal ab, bevor es eine Karte verlangt — 9003 gemessen nach 15,7 s bei 99 999,99 EUR, ohne jede Kartenaufforderung. Positive Aussagen, keine Luecken.

    Dazu eine allgemeine Regel fuer alles, was unbenannt bleibt: hat das Terminal die erzeugende Anfrage mit einem Ergebniscode beantwortet, ist der Vorgang dort beendet. Erst dadurch bekommt 9027 beim Pollen einen Aussagewert. Gemessen antwortet eine genehmigte Zahlung mit 0 (Beleg 408811, dreimal wiederholt), eine abgelehnte mit 9027 (bei 100003 zweimal, bei 9003 einmal). Zwei 9027 in Folge ergeben deshalb declined.

    Drei Absicherungen, damit daraus keine neue Doppelbelastung wird: ohne vorangegangene Antwort mit Code greift die Regel nicht — der Vorfall vom 24.08.2026 (Antwort verloren, Zahlung laeuft weiter) bleibt unveraendert unresolved; erst die zweite Abfrage entscheidet, weil die erste in das Fenster faellt, in dem der Datensatz am Terminal noch nicht stehen muss; und „zweimal" heisst hintereinander. Alle drei sind mit einer Mutationsprobe abgesichert.

    Was weiterhin fehlt: die echten Codes fuer „keine Deckung" und „Karte abgelaufen". Sie liessen sich nicht messen — das Testgeraet wird per Apple Pay bedient, eine ablehnende Karte stand nicht zur Verfuegung. Die Regel oben faengt sie ab, benennen kann sie sie nicht.

  • Neu: discoverHpsTerminals findet die Adresse des Terminals im Netz. Ein Aufruf statt einer Adresse aus der Einrichtung: TCP-Scan der lokalen /24-Netze auf Port 8080, danach eine Nachfrage, die aus einem offenen Port ein hobex-Terminal macht. stopAtFirst: true bricht beim ersten Treffer ab — im echten Netz gemessen 8,4 s gegenueber 18,7 s ueber alle Netze.

    Die Nachfrage prueft zwei Merkmale, und das ist erzwungen: die naheliegende Pruefung GET /api/terminals antwortet auf der gemessenen Firmware mit 404 "Endpoint not implemented", ebenso /api/status, /api/version und /api/terminals/{tid}/status. Eine Suche, die nur darauf baut, findet ein voll funktionsfaehiges Terminal nicht. Zweites Merkmal ist GET /api/terminals/0/diagnosis — dasselbe, das kasseneck-connect benutzt.

    Fuer iOS ab 14 noetig: NSLocalNetworkUsageDescription in der Info.plist. Fehlt der Eintrag, laufen die Verbindungen ins Leere und die Suche meldet schlicht „nichts gefunden" — ohne Fehler, der darauf hinweist.

    Die TID liefert die Suche nur mit, wenn die Firmware GET /api/terminals kennt. Auf der gemessenen tut sie das nicht; die TID muss dort bei der Einrichtung eingetragen werden.

5.1.1 #

  • HobexCloudPayments ist abgekuendigt (@Deprecated). Der Cloud-Weg wird nicht mehr weiterentwickelt; neue Einbindungen nutzen HpsPayments mit einem lokalen hobex-HPS-Terminal. Nichts wird entfernt: Bestandsbelege tragen CreditCardProvider.hobexCloudApi, und sie muessen les- und druckbar bleiben — abgekuendigt ist der aktive Zahlweg, nicht die Darstellung.

    Warum abkuendigen statt korrigieren: Der HPS-Weg hat mit 5.1.0 eine Positivliste bekommen — nur Codes, deren Bedeutung gemessen ist, gelten als Aussage. Der Cloud-Weg ordnet weiterhin jeden Code ungleich '0' als Ablehnung ein. Das ist dort begruendet und nicht blind uebernommen: die Cloud signalisiert "kenne ich nicht" ueber null aus hobexGetStatus, nicht ueber einen Ergebniscode. Die Begruendung ruht aber auf einer ungepruefen Annahme — dass die Cloud nie einen Code liefert, der eine Wissensluecke bedeutet. Genau diese Sorte Annahme ist beim HPS-Weg an einer Messung gescheitert.

    Sie hier nachzuziehen waere Arbeit an einem Weg, der nicht mehr genutzt wird, und ohne die Messwerte, die den HPS-Weg gut gemacht haben. Ehrlicher ist die Abkuendigung: wer den Cloud-Weg noch einbindet, sieht jetzt, dass er nicht gepflegt wird.

5.1.0 #

Anlass: Am 27.08.2026 zeigte eine Messung am selben hobex-HPS (TID 3600335, HPS 1.10.0, Firmware 7.3.6), dass die mit 5.0.0 aufgestellte eiserne Regel — declined entsteht ausschließlich aus einer positiven Aussage — an einer Stelle nicht durchgehalten wurde. TransactionResponse.isConclusive galt bis dahin für jeden Code außer null und 9027: eine Positivliste, die sich als Negativliste ausgab und unterstellte, alle nicht-aussagekräftigen Codes zu kennen. Ein neu aufgetretener, bis dahin unbenannter Code (9900, "Technical Error Database") war darüber schlüssig — der Klärweg hätte daraus declined gemacht, für einen Vorgang, unter dem tatsächlich Geld geflossen sein kann. Derselbe Fehler wie am 24.08.2026, nur an anderer Stelle entstanden.

  • Ursache von 9900, gemessen: eine nicht rein numerische Transaktionskennung. Die HPS-Schnittstelle verlangt laut hobex-Dokumentation schon immer eine numerische Kennung — das Paket hat bis hierher nur die Länge geprüft und Buchstaben klaglos durchgelassen. Am Terminal gemessen: eine echte Kartenzahlung mit einer nicht rein numerischen Kennung (z. B. A1787860907) wird anstandslos angenommen — Karte gelesen, Kryptogramm vorhanden, Geld kann geflossen sein —, doch jede spätere Statusabfrage auf GENAU diese Kennung antwortet dauerhaft mit 9900, unabhängig davon, was am Terminal tatsächlich geschah; eine rein numerische, nie gesehene Kennung bekommt dagegen die erwartbare Antwort (9027). Erst wird das Geld bewegt, dann der Nachweis vernichtet. Das Risiko dafür ist in der Praxis gering, nicht abstrakt: jeder Erzeuger im Ökosystem liefert bereits rein numerische Kennungen (HpsClient.newTransactionId(), der JS-Zwilling); der Fall greift nur, wenn ein Aufrufer eine eigene Kennung übergibt, was CreditCardProvider.custom ausdrücklich erlaubt.
  • Behoben: HpsClient setzt den dokumentierten Vertrag jetzt durch. transactionId und originalTransactionId (bei refund, bis hierher komplett ungeprüft) werden vor jedem Netzweg auf reine Ziffern geprüft, an derselben Stelle und mit derselben Härte wie die bestehende Längenprüfung — ein Verstoß wirft ArgumentError, bevor irgendetwas hinausgeht. Breaking: ein Aufrufer, der bisher eine nicht rein numerische Kennung übergeben hat (nur über CreditCardProvider.custom erreichbar), bekommt jetzt einen Wurf statt eines stillschweigend angenommenen, aber am Terminal unauffindbaren Vorgangs.
  • Der eigentliche Fehler liegt tiefer als 9900 und ist jetzt behoben: TransactionResponse.isConclusive ist eine echte Positivliste. Schlüssig ist nur noch ein Code, dessen Bedeutung gemessen und benannt ist (0, 9002, 9011, 100002, 100003, 100010). Jeder andere Code — ob er wie ein Fehlercode aussieht, ob er neu ist oder schlicht nie gemessen wurde — ist eine Wissenslücke und führt zu unresolved, nie zu declined. Breaking (stille Bedeutungsänderung ohne Namensänderung): isConclusive und isNoStatement bleiben als Symbole bestehen, aber isConclusive liefert für einen Code, der zuvor als schlüssig galt (jeder Code ungleich null/9027, etwa ein erfundener oder unbekannter Ablehnungscode), jetzt false. Wer sich auf das alte Verhalten verlassen hat, bekommt für einen nie gemessenen Code unresolved statt declined — die sichere statt die geratene Richtung, aber ein anderes Ergebnis als vorher.
  • Ein zweiter, unabhängig gemessener Code bestätigt die Umkehrung: 9002 "Invalid Transaction". Dieselbe Lage wie bei 9900, aber mit einer REIN NUMERISCHEN, unbekannten Original-Kennung: eine Gutschrift darauf antwortet nach 1,2 Sekunden mit 9002 — kein Kartenfluss, keine Auszahlung, die Karte wurde nie angefordert. Anders als 9027 und 9900 ist das eine positive Aussage: das Terminal hat den Vorgang selbst als ungültig verworfen, bevor irgendetwas in Bewegung kam. Neu benannt (TransactionResponse.invalidTransactionCode, isInvalid) und Teil der Positivliste — führt zu declined. Die Kontrollprobe zu 9900 zugleich: dieselbe Anfrage lieferte mit einer nicht rein numerischen Kennung 9900, mit einer numerischen, aber unbekannten Kennung 9002 — zwei verschiedene Codes für zwei verschiedene Gründe.
  • HTTP 409 "Terminal is busy" bekommt eine eigene, benannte Behandlung — außerhalb der Code-Zuordnung. Am 27.08.2026 gemessen: läuft bereits ein Vorgang und wird ein zweiter gestartet, kommt 409 nach 87 Millisekunden; der abgewiesene Vorgang hinterlässt KEINE Spur (Statusabfrage darauf: 9027, zweimal geprüft). Damit ist die in 5.0.0 offen gelassene Entwurfsfrage entschieden: HpsPayments liest ein 409 auf die ERZEUGENDE Anfrage (Zahlung, Gutschrift, der direkte Aufhebungs-Request von cancel) jetzt als declined, ohne Abbruchversuch oder Polling. 409 ist ein HTTP-Status, kein responseCode — er entsteht, bevor überhaupt ein Antwortrumpf gelesen wird, und ist deshalb bewusst NICHT Teil von TransactionResponse.isConclusive, sondern eine eigene Prüfung (HpsHttpException.terminalBusyStatusCode, .isTerminalBusy, ausgewertet in HpsPayments._fromTerminalBusy). Die positive Aussage gilt AUSSCHLIESSLICH für die erzeugende Anfrage: ein 409 auf einen HpsClient.abort-Versuch oder auf eine Statusabfrage während der Klärung bleibt unverändert ein gewöhnlicher Transportfehler — er sagt dort nur, dass diese eine Anfrage nicht durchkam, nichts über den Vorgang, den sie klären sollte (eigens durch Tests abgesichert, die genau diese Vermischung verhindern).
  • Neue öffentliche Symbole auf TransactionResponse: abortedCode (100002, "Aborted" — bisher nur in Doku/Tabellen erwähnt, jetzt benannt), cardNotPresentCode (100003, "Card not present" — ebenso), invalidTransactionCode (9002, "Invalid Transaction"), isInvalid (Getter), technicalErrorCode (9900, "Technical Error Database"), isTechnicalError (Getter), isUnknownCode (Getter — true für einen vorhandenen Code, der weder schlüssig noch eine der beiden benannten Wissenslücken isNoStatement/isTechnicalError ist).
  • Neue öffentliche Symbole auf HpsHttpException: terminalBusyStatusCode (409), isTerminalBusy (Getter).
  • Der Nachweistext (steps) unterscheidet jetzt drei Nicht-Ergebnisse statt zwei, plus den eigenen 409-Fall. Er wird im Belastungsstreit gelesen: "Terminal kennt den Vorgang nicht" (9027, isNoStatement), "Terminal meldet einen technischen Fehler" (9900, isTechnicalError), "Terminal nennt einen unbekannten Code" (isUnknownCode) und "Terminal beschäftigt (HTTP 409)" sind vier verschiedene Aussagen, keine davon behauptet eine Ursache, die nicht feststeht (AGENTS.md, Regel 7). Betroffen: pay, refund und der direkte Antwortweg von cancel (HpsPayments).
  • doc/kartenzahlung.md um die Befunde zu 9900, 9002 und 409 ergänzt, samt der Selbst-nachmessen-Tabelle (rein numerisch vs. mit Buchstaben) und einem zusammenfassenden Überblick über den gesamten gemessenen Stand.

Der JS-Zwilling ist nicht betroffen: zwillinge.yaml/test/zwillinge_test.dart führen ausschließlich Kasseneck-Backend-Einstellungen; der lokale HPS-Zahlweg und seine Ergebniscodes sind kein Teil dieses Vertrags.

5.0.0 #

Anlass: Am 24.08.2026 wurde ein Kunde am Kartenterminal zweimal mit 25 € belastet. Die App meldete beide Male „Kartenzahlung fehlgeschlagen", bei hobex waren beide Vorgänge genehmigt. Ursache: HpsClient.payment() erzeugte die Transaktionskennung intern und gab sie erst mit der Antwort heraus — blieb die Antwort aus (Verbindungsabbruch, Frist), war die Kennung nie bekannt, und damit waren Statusabfrage und Storno strukturell unerreichbar. Jede Wiederholung wurde dadurch zwangsläufig ein zweiter, eigenständiger Vorgang. Dieses Release macht die Kennung zum ersten Schritt eines Zahlvorgangs statt zu dessen Nebenprodukt und ersetzt geratene Fehlschläge durch geklärte Ausgänge.

  • Zahlung, Rückerstattung und Aufhebung mit geklärtem Ausgang statt geratenem Fehlschlag. Neu: HpsPayments (pay/refund/cancel, HPS-Terminal) und HobexCloudPayments (pay, Hobex-Cloud) legen die Transaktionskennung fest, bevor der erste Request hinausgeht, und liefern ein dreiwertiges CardPaymentOutcome statt eines Booleans: approved (Geld ist geflossen), declined (definitiv kein Geld geflossen — Wiederholung gefahrlos) oder unresolved (Ausgang unbekannt — keine automatische Wiederholung). Bleibt die erste Antwort aus, wird geklärt statt aufgegeben: auf dem HPS-Weg zuerst mit einem einmaligen Abbruchversuch, danach — und auf dem Cloud-Weg von Anfang an — mit einer Abfrageschleife mit Backoff (1 s, 2 s, 4 s, 8 s, gedeckelt bei 10 s, Gesamtbudget 90 s, einstellbar über den Konstruktor). Zum Ablauf im Einzelnen siehe die beiden folgenden Punkte. Das Ergebnis (HpsResult/HobexCloudResult) trägt in jedem Fall die Transaktionskennung und einen Klärverlauf (steps) für Anzeige und Protokoll — auch bei unresolved.
  • Der Bug vom 24.08. ist damit geschlossen: eine ausbleibende Antwort führt nicht mehr zu einer neuen, kennungslosen Wiederholung, sondern zu einer Klärung über dieselbe Kennung.
  • Der Klärweg beruht auf Messungen am Terminal, nicht auf Annahmen. Am 26.08.2026 an einem hobex-HPS gemessen (TID 3600335, HPS 1.10.0, Firmware 7.3.6, Host tecstest.hobex.at): die Statusabfrage antwortet mit 9027 „Original Tx not found" gleichermaßen auf eine nie gesehene Kennung, auf einen gerade laufenden Kartenfluss, auf „Karte nicht aufgelegt" und auf einen abgebrochenen Vorgang; nur eine genehmigte Zahlung antwortet mit 0, und dieser Wert bleibt erhalten. 9027 ist damit kein Ergebnis, sondern eine Wissenslücke — es ist als benannter Begriff im Modell geführt (TransactionResponse.noStatementCode, isNoStatement, isConclusive) statt als verstreute Zeichenkettenprüfung. Es über != '0' als Ablehnung zu lesen, hätte für einen laufenden Vorgang „nichts belastet, Wiederholung gefahrlos" gemeldet — genau die Doppelbelastung, gegen die dieses Release gebaut ist.
  • Der Abbruch steht jetzt vorne, nicht hinter der Statusabfrage. Der Abbruch ist der Diskriminator, den die Statusabfrage nicht liefert: gemessen gelingt er (responseCode '0'), solange der Vorgang abbrechbar ist, und scheitert (100010), sobald er abgeschlossen ist — die genehmigte Zahlung bleibt dabei unangetastet. Bleibt die Antwort auf eine Zahlung aus, läuft die Klärung deshalb: abort einmalig versuchen → responseCode == '0' ergibt beweisbar declined (ohne jede Statusabfrage) → jeder andere Code oder ein Transportfehler beim Abbruch führt ins Pollen der Statusabfrage, die nun eine echte Aussage liefert → 9027 beim Pollen ist kein Ergebnis, sondern ein Grund weiterzumachen → Budget erschöpft ergibt unresolved. Die vorherige Reihenfolge (erst pollen, abbrechen nur wenn der Status „läuft noch" meldet) war wirkungslos: diesen Zustand meldet die Statusabfrage nie. Damit wird unresolved zum seltenen Randfall statt zum Regelfall. Bewusst in Kauf genommen: gelingt der Abbruch, während der Kunde die Karte auflegt, reißt er dessen Zahlung ab — geldseitig die sichere Richtung, und er wird ohnehin nur ausgelöst, wenn die Zahlung schon ohne Antwort dasteht. Der Abbruchversuch selbst ist auf ein Sechstel des Klärbudgets gedeckelt (15 s von 90 s): er startet mit dem vollen Budget im Rücken und hätte hängend den gesamten Klärweg aufgefressen, ohne dass je eine Statusabfrage stattgefunden hätte. refund folgt demselben Weg, cancel nicht: bei refund ist die übergebene Kennung die des neuen Vorgangs, und der Abbruch-Endpunkt kennt keinen Transaktionstyp — ohne Abbruch hätte die Klärung einer Rückerstattung gar keinen Diskriminator und endete fast immer bei unresolved. Bei cancel ist die Kennung die der längst abgeschlossenen Originalzahlung; ein Abbruch darauf wäre sinnlos (gemessen: 100010).
  • Die eiserne Regel bleibt, mit einer zweiten belegten Quelle: declined entsteht ausschließlich aus einer positiven Aussage — einem echten Ergebniscode des Terminals ungleich '0' und ungleich 9027, oder einem nachweislich gelungenen Abbruch. Niemals aus einem Transportfehler, einem Zeitablauf oder einer Wissenslücke. Die Kennung ist in jedem zurückgegebenen Ergebnis gesetzt.
  • Breaking: HpsClient.abort() liefert jetzt eine TransactionResponse statt String?. Das Terminal meldet ein gescheitertes Abbrechen mit HTTP 200 und 100010 im Rumpf. Die bisherige Fassung las nur json['transactionId'] und warf nur bei Nicht-2xx — ein gescheiterter Abbruch hätte damit ausgesehen wie ein geglückter, und aus einer echten Belastung wäre declined geworden. Aufrufer lesen die Kennung jetzt über response.transactionId und den Ausgang über response.responseCode.
  • Die Storno-Klärung beruht auf einem gemessenen Ergebniscode statt auf einem Feld, das diese Firmware nicht liefert. Bis 26.08. entschied state == 'VOID' — auf HPS 1.10.0 / FW 7.3.6 ist state jedoch in jeder bisher gesehenen Antwort null, bei genehmigten, abgebrochenen, unbekannten und aufgehobenen Vorgängen gleichermaßen. Die Bedingung wurde nie wahr; jede Storno-Klärung lief ins Budget und endete als unresolved, egal was tatsächlich geschah. Gemessen antwortet die Statusabfrage auf die Kennung der Originalzahlung nach einem erfolgreichen Void mit 9011 „Transaction Canceled". Neue Zuordnung im Klärweg: 9011 → die Aufhebung hat gewirkt (approved); '0' → die Originalzahlung steht unverändert, die Aufhebung hat nicht gegriffen (declined — weiterhin belastet, ein erneuter Void ist gefahrlos); 9027 und alles andere → weiter klären, am Ende unresolved. Der direkte Antwortweg von cancel wird unverändert über den responseCode dieser Antwort eingeordnet, die die Aufhebung selbst betrifft — der responseCode der Originalzahlung wird damit nie als Erfolg der Aufhebung gelesen. state == 'VOID' gilt nur noch zusätzlich, niemals als notwendige Bedingung. Zwei Sicherungen dabei: '0' entscheidet erst ab der zweiten beantworteten Statusabfrage — die erste läuft ohne Pause, unmittelbar nachdem der Void-Request abgerissen ist, und trifft damit genau das Fenster, in dem der Void noch unterwegs sein kann. Ein voreiliges „hat nicht gegriffen" ist nicht harmlos: nach dem Tagesabschluss ist die Folgehandlung eine Rückerstattung, und der Kunde bekäme sein Geld zweimal. Reicht das Budget nur für eine Abfrage, endet die Klärung bei unresolved. Und: 9011 auf dem direkten Antwortweg von cancel wird nicht mehr als declined gelesen (das hätte „weiterhin belastet" für einen aufgehobenen Vorgang gemeldet), sondern als nicht schlüssig — was 9011 dort genau heißt, ist ungemessen, also wird der Zustand der Originalzahlung abgefragt statt geraten.
  • Cloud-Weg neu im Paket: KasseneckApi.hobexGetStatus fragt den Stand einer Hobex-Cloud-Transaktion ab und ist die Klärstufe hinter HobexCloudPayments — sie unterscheidet „Dienst hat geantwortet und kennt die Kennung nicht" (null, weiter pollen) von „konnte gar nicht erst fragen" (Transportfehler, wirft). Scheitern drei Statusabfragen in Folge am Transport, endet die Klärung vorzeitig mit unresolved statt den Mitarbeiter das volle Budget warten zu lassen. Dazu KasseneckApi.cardTimeout (3 Minuten, für hobexPay/hobexRefund) getrennt von readTimeout (30 s) — die bisher pauschale 30-Sekunden-Frist war für einen Belege-Cache gedacht und meldete am Terminal eine durchgelaufene Zahlung als Fehlschlag. hobexGetStatus selbst bleibt bei readTimeout: eine Abfrage, kein Kartenfluss. Sie liest die Antwort über dieselbe geprüfte Hülle wie jeder andere Aufruf — ihre eigene Prüfung hängte den ganzen Rumpf an die Ausnahmemeldung, und die Ausnahmen der Klärschleife landen im Nachweistext, der im Belastungsstreit gelesen wird; ihr json.decode stand außerdem ungesichert und gab bei HTML im Rumpf eine rohe FormatException.
  • Cloud-Rückerstattung bleibt roh, bewusst. HobexCloudPayments deckt nur pay ab — kein refund. Eine Rückerstattung über die Hobex-Cloud läuft weiterhin unverändert über KasseneckApi.hobexRefund und bleibt damit ungeklärt: kein CardPaymentOutcome, keine Klärschleife. Bewusst nicht nachgebaut, denn ungeprüft ist, worauf hobexGetStatus mit der Kennung einer Rückerstattung überhaupt antwortet — genau diese Frage hat beim HPS-cancel einen Fehler aufgedeckt (die dort übergebene Kennung ist die der Originalzahlung, deren responseCode bleibt für immer '0' und wäre fälschlich als Erfolg der Aufhebung gelesen worden). Blind nachzubauen hätte denselben Fehler riskiert, diesmal ohne die Prüfung, die ihn dort gefangen hat. Betroffen von der Fristanhebung auf cardTimeout (siehe oben) ist hobexRefund trotzdem: der Aufrufer wartet jetzt bis zu 3 Minuten statt bisher 30 Sekunden auf dieselbe Nicht-Aussage.
  • Beobachter für den Zahlweg: ein injizierbarer HpsObserver-Rückruf meldet Requests, Fehlschläge und den Stand der Klärung. Ohne Protokoll gab es zum Vorfall vom 24.08. keine Daten außer den hobex-Belegen; das soll sich beim nächsten Vorfall nicht wiederholen. HpsPayments nimmt ihn wie bisher entgegen, jetzt zusätzlich auch HobexCloudPayments. HpsEvent, HpsEventKind und HpsObserver werden dafür zusätzlich aus kasseneck_api.dart exportiert (bisher nur aus hobex_hps.dart).
  • Breaking: HpsClient.cancel() verlangt jetzt amount. Das Terminal weist einen Void ohne Betrag mit 400 Missing amount ab.
  • Der dreiwertige Ausgang heißt CardPaymentOutcome und liegt in lib/src/payments/card_payment_outcome.dart statt im HPS-Baum, weil er jetzt auch vom Cloud-Weg geteilt wird. Die Werte approved/declined/unresolved sind unverändert; er ist der Typ von HpsResult.outcome und wird aus hobex_hps.dart exportiert.
  • Breaking (stille Verhaltensänderung): HpsClient.close() ist jetzt dauerhaft ein No-op. Seit der Verbindungs-Robustheit baut jeder Request seine eigene Verbindung auf und schließt sie selbst wieder — es gibt keine wiederverwendete Verbindung mehr, die close() freigeben müsste. Aufrufer, die sich auf eine tatsächliche Freigabe verlassen, bekommen keine.
  • Breaking: eine zu lange Transaktionskennung wirft jetzt einen ArgumentError, bevor irgendein Request hinausgeht — vorher lief das unbehandelt in ein 400 vom Terminal. Geprüft wird nur die Länge (max. 18 Stellen), nicht auf reinen Ziffernvorrat.
  • Erzeugte Transaktionskennung beweisbar kollisionsfrei statt zufallsbasiert: HpsClient.newTransactionId() bildet die Kennung jetzt über Zeitstempel plus einen Zähler je Millisekunde (Snowflake-Verfahren) statt über Zeitstempel plus Zufallsziffern. Die Kollisionsfreiheit gilt innerhalb eines Prozesses — sie ist keine geräteübergreifende Garantie. Zwei getrennte Prozesse (etwa zwei App-Instanzen), die in derselben Millisekunde je eine Kennung für dasselbe Terminal bilden, können weiterhin dieselbe Kennung erzeugen, weil sie voneinander nichts wissen.
  • tid wird im Client normalisiert (führende Null abgestreift) statt in jeder aufrufenden App einzeln.
  • Beispiel und Bibliotheks-Doku zeigen den sicheren Weg. example/example.dart demonstrierte den Kartenverkauf bis zuletzt über HpsClient.payment() ohne transactionId und mit if (!res.isApproved) return; — Zeile für Zeile der Vorfallsmechanismus vom 24.08. Jetzt über HpsPayments: Kennung vor dem Aufruf, dreiwertiger Ausgang, und unresolved ausdrücklich anders behandelt als declined. Dasselbe überholte Snippet stand im Bibliotheks-Doc von hobex_hps.dart, und der Klassenkopf von TransactionResponse lehrte noch responseCode != "0" als Ablehnungstest — genau die widerlegte Lesart. Beide auf den Messstand gezogen. Kein öffentliches Symbol geändert, aber es ist die Fläche, die Aufrufer und pub.flutter-io.cn zuerst sehen.
  • Frist über den ganzen Abruf statt nur bis zum Antwortkopf: ein Terminal, das Kopf und 200 schickt und den Rumpf offen lässt, hielt die App bisher unbegrenzt fest.

Der Belegweg — dieselbe Denkweise, dieselben Korrekturen:

  • Ein Logo-Server kann den Verkauf nicht mehr anhalten. LogoService.loadLogo rief httpClient.get(...) ohne jede Frist auf eine Adresse, die der Server in der Belegantwort mitgibt — und zwar in KasseneckReceipt.init(), also hinter dem bereits signierten Beleg. Ein Host, der die Verbindung annimmt und nie antwortet (hängender Proxy, Captive Portal, überlasteter CDN-Knoten), ließ sellReceipt/cancelReceipt/zeroReceipt nie zurückkehren und nie werfen: der Beleg stand in der Signaturkette, der Bildschirm zeigte nichts, und wer die App neu startete und erneut kassierte, erzeugte einen zweiten Umsatz. Dasselbe blockierte getReceipts für einen ganzen Zeitraum. Der Abruf hat jetzt eine harte Frist (LogoService.frist, Vorgabe LogoService.standardFrist = 3 s) über Kopf und Rumpf; ein Fehlschlag heißt schlicht „kein Logo". Drei Sekunden sind der Kompromiss zwischen einer langsamen Mobilverbindung und der Spanne, um die ein toter Host jeden Verkauf verzögern darf. Der Abruf bleibt bewusst im Verkaufsweg abgewartet: er ist damit gedeckelt, und der Bon direkt nach dem Verkauf trägt sein Logo verlässlich statt je nach Zeitpunkt.
  • Gleichzeitige Abrufe derselben Logo-Adresse teilen sich einen Request. Die Cache-Prüfung lag vor dem await; getReceipts feuerte deshalb bei N Belegen mit derselben Adresse N gleichzeitige Requests.
  • Neue öffentliche Symbole: LogoService.frist, LogoService.standardFrist.
  • Breaking (stille Verhaltensänderung): sellReceipt, cancelReceipt, createCancelReceipt und zeroReceipt warten jetzt bis zu 90 statt 30 Sekunden. Neu: KasseneckApi.signatureTimeout (Vorgabe 90 s), getrennt von readTimeout. Dieselbe Diagnose wie beim Kartenweg, nur auf dem Belegweg übersehen: ein Zeitablauf beendet nur das Warten der Kasse, nicht die Arbeit des Servers — der Beleg kann längst signiert und in der Kette sein, während der Aufrufer eine TimeoutException bekommt und sie als Fehlschlag liest. Der Kassen-Weg gibt dem Abschluss aus genau diesem Grund seit jeher 90 Sekunden (RegisterReceiptClient.abschlussFrist); die pauschalen 30 Sekunden auf dem api_key-Weg waren ein Rest. Eine eigene, benannte Frist statt eines höheren readTimeout, damit eine Belegliste nicht mitwächst: die drei Fristen (readTimeout 30 s, signatureTimeout 90 s, cardTimeout 3 min) benennen jeweils, was wartet.
  • financeWebService beachtet readTimeout. getCashboxStatus und getSignatureStatus liefen über eine fest verdrahtete halbe Minute; wer readTimeout im Konstruktor setzte, erreichte sie damit nicht.
  • Breaking: ein unlesbares Antwortfeld verliert den signierten Beleg nicht mehr. KasseneckReceipt.create/.fromJson wiesen ungeprüfte dynamic-Werte an nicht nullbare Felder zu (qr, sig, receiptId, timeStamp, taxnr, footer1/2, is_small_business …). Ein einziges fehlendes Feld erzeugte einen rohen TypeError nach der Signatur — für den Aufrufer nicht von „Verkauf fehlgeschlagen" zu unterscheiden, und mit der verworfenen Antwort ging die receiptId verloren: der Beleg stand in der Kette, die Kasse wusste nichts von ihm, der Kunde bekam keinen (RKSV-Belegerteilungspflicht), und der naheliegende zweite Versuch war ein zweiter Umsatz. Neu: KasseneckReceiptFormatError mit field, receiptId und causeType, geworfen von beiden Belegwegen. Die Kennung wird zuerst gelesen und in jedem Fehler mitgegeben, sooft sie in der Antwort stand — sie ist der Faden, an dem getReceipt(receiptId) bzw. RegisterReceiptClient.holen(receiptId) den Beleg nachholt. Dieselbe Regel wie am Kartenterminal: die Kennung steht vor allem anderen fest. causeType trägt nur den Typ der zugrunde liegenden Ausnahme, nie ihre Meldung — eine FormatException führt ihre Eingabe im Text, und Antwortinhalte gehören nicht ins Protokoll.
  • Die Grenze verläuft zwischen Signatur und Zierde. Signatur- und Identitätsfelder (receiptId, cashregisterId, timeStamp, qr, sig, certificateSerialNumber, signaturePreviousReceipt, turnoverCounterAES256ICM) werden nicht ersetzt: ein Ersatzwert wäre keine Toleranz, sondern eine Behauptung über RKSV-Daten. Kopf- und Fußzeilen (Firma, Anschrift, Steuerangabe, Fußzeilen, is_small_business, logo_url) dürfen dagegen fehlen — null und '' drucken gleich, und einen signierten Beleg wegen einer fehlenden Fußzeile zu verwerfen ist die teurere Verwechslung. Wer sich bisher darauf verlassen hat, dass ein fehlendes Kopfdatum wirft, bekommt jetzt ein leeres Feld.
  • Eine fehlende Firma ist trotzdem ein Mangel — nur kein Wurf. § 132a Abs. 3 Z 1 BAO verlangt die eindeutige Bezeichnung des leistenden Unternehmers; ein fehlendes company ergab bis hierher stumm einen Pflichtbeleg mit leerer erster Zeile. Zu werfen kostet den bereits signierten Beleg, der in der Kette steht — deshalb neu KasseneckReceipt.fehlendePflichtangaben (Feldnamen, nie Werte) und pflichtangabenVollstaendig: der Aufrufer erkennt den unvollständigen Beleg, ohne ihn zu verlieren. Abgeleitet statt gespeichert, damit die Aussage auf jedem Bauweg gilt (create, fromJson, fromMetadata, Konstruktor, aus Isar zurückgelesen) und nicht veraltet, wenn der Kopf nachgetragen wird. Nur die Bezeichnung des Unternehmers steht darin: taxnr, Anschrift und Fußzeilen sind auf einem Beleg keine Pflichtangaben (erst auf einer Rechnung nach § 11 UStG) und bleiben Zierde.
  • Die zehn impliziten Map<String, dynamic>-Zuweisungen auf dem api_key-Weg sind weg (createReceipt, listMyTipRecipients, getReceipt, getReportV2, getCashboxStatus, getSignatureStatus, createPaymentLinkStripe, stripeCaptureIntent, hobexPay, hobexRefund, dazu getFirstReceiptDate). Sie liefen bei einem 200 mit Array, Skalar oder null in einen rohen TypeError, bei einem 200 mit HTML im Rumpf (Captive Portal, CDN-Fehlerseite) in eine rohe FormatException — bei createReceipt mitten im Verkauf. Beides wirft jetzt KasseneckHttpError — denselben Typ mit denselben reason-Werten ('not-json', 'missing-status', 'data-not-object'), die der Kassen-Weg für dieselben drei Lagen setzt, und wie dort ohne jeden Rumpfinhalt. Ein blankes Exception hätte hier nicht gereicht: nach der Signatur ist „die Antwort ist kaputt, der Beleg existiert" etwas anderes als „der Verkauf ist fehlgeschlagen", und unterscheiden kann das nur, wer den Fall gezielt fangen darf.
  • getReceipts unterscheidet „im Zeitraum nichts verkauft" von „Antwort kaputt". data.metadata und data.receipts wurden hart gecastet — ein NoSuchMethodError bzw. TypeError als Hülle um einen sorgfältig abgesicherten Per-Beleg-try/catch herum. Jetzt KasseneckValidationError(kind: 'response'), wortgleich mit derselben Prüfung in belege.dart auf dem Kassen-Weg; ebenso die fehlende Empfängerliste in listTipRecipients, die bis hierher ebenfalls nur ein blankes Exception war.
  • Die Zählzeile im Protokoll von getReceipts lief vor der Prüfung. Ein debugPrint las data['receipts'] mit einem String-Index, bevor data überhaupt als Objekt geprüft war: bei data: [...] oder data: "text" griff der Index in eine Liste bzw. einen Text, bei receipts: {…} scheiterte der Cast — jedes Mal genau der rohe TypeError, den die beiden Zeilen darunter verhindern sollten. Gezählt wird jetzt erst nach den Prüfungen.
  • Auch der Storno-Weg verliert den signierten Beleg nicht mehr. RegisterReceiptClient.stornieren prüft data.cancellationOf und data.remaining vor dem Einlesen des Belegs und warf dabei KasseneckValidationError ohne jeden Bezug — der gesetzlich vorgeschriebene Storno-Beleg war zu diesem Zeitpunkt bereits ausgestellt, signiert und in der Kette, der Aufrufer bekam ihn nicht und hatte nichts zum Nachholen. Ein zweiter Storno wäre eine zweite Rücknahme gewesen. KasseneckValidationError trägt jetzt ein optionales receiptId, und die drei Prüfungen dieses Abschnitts geben es mit.
  • Neue öffentliche Symbole: KasseneckReceipt.fehlendePflichtangaben, KasseneckReceipt.pflichtangabenVollstaendig, KasseneckReceiptFormatError (aus kasseneck_api.dart und kasse.dart), dazu KasseneckHttpError und KasseneckValidationError aus kasseneck_api.dart — wer die Antwortfehler des api_key-Wegs fangen soll, muss ihre Typen auch benennen können, ohne register.dart zu importieren. KasseneckValidationError.receiptId. KasseneckReceipt.fromMetadata nimmt den Belegteil jetzt als Object? entgegen statt als Map<String, dynamic> und prüft ihn selbst.
  • Zeitablauf und Verbindungsfehler sind auf dem Kassen-Weg nicht mehr derselbe Fehler. RegisterTransport.rufen und RegisterClient._rufen fingen mit on Object alles ab und meldeten KasseneckHttpError(name, 0, 'network') — der Ursprungsfehler wurde restlos verworfen. Über createReceipt sind das zwei verschiedene Lagen: eine abgelaufene Frist heißt „die Anfrage war draußen, der Ausgang ist unbekannt" (der Beleg kann längst signiert sein), ein Verbindungsfehler ist etwas anderes. Beides in denselben Ausgang zu werfen nahm dem Aufrufer die einzige Handhabe, die er hat — dieselbe Vermischung von Nichtwissen und Aussage wie am 24.08. am Terminal. Neu: reason ist KasseneckHttpError.zeitablauf ('timeout') oder KasseneckHttpError.netz ('network'), und causeType trägt den Typ der Ursache (TimeoutException, SocketException, ClientException) fürs Protokoll — nie ihre Meldung, die kann Werte tragen. Bewusst kein dreiwertiges Ergebnis auf dem Belegweg: package:http trennt „Verbindung kam nie zustande" nicht von „Verbindung riss, nachdem die Anfrage draußen war"; ein declined aus einem Transportfehler wäre genau die Behauptung, die Regel 2 verbietet. Das Paket erhält deshalb die Unterscheidung und überlässt den Schluss dem Aufrufer, statt ihn zu raten. Das steht so am Typ dokumentiert.
  • Ein Programmierfehler wird nicht mehr als Netzstörung gemeldet. Der jsonEncode des Rumpfes stand im selben try wie der Request; ein nicht serialisierbarer Parameter kam als network heraus — also als Fehler, nach dem ein Beleg entstanden sein könnte, obwohl nichts hinausgegangen war.
  • Ein kaputtes data geht nicht mehr als „nichts eingestellt" durch. RegisterTransport.rufen gab bei status: success und einem data, das kein Objekt ist (Array, Zahl, Text), stillschweigend {} zurück; KasseSettings.aus({}) machte daraus den vollen Standardsatz. Eine defekte Antwort erreichte den Bildschirm damit als Vorgabefarbe, Standard-Beleglayout und Standard-Belegausgabe. Schlimmer beim Schreiben: betriebSpeichern/geraetSpeichern meldeten „gespeichert" und gaben die Standardwerte als angeblich neuen Stand zurück — der Bediener sieht seine eben gespeicherte Einstellung nicht und speichert erneut, mit einer Änderung, die er gegen den Standard statt gegen den echten Stand bildet. Jetzt: fehlendes data bleibt ein leeres Objekt (nicht jeder Aufruf hat eine Nutzlast), ein data, das da ist und kein Objekt ist, wirft KasseneckHttpError(reason: 'data-not-object'), und ein Schreibaufruf ohne data.betrieb/data.geraet wirft KasseneckValidationError(kind: 'response'). Dieselbe Grenze, die belege.dart und kasseneck_api.dart bei den Listen längst ziehen: leer ist etwas anderes als kaputt.
  • Neue öffentliche Symbole: KasseneckHttpError.causeType, KasseneckHttpError.zeitablauf, KasseneckHttpError.netz.
  • Der gedruckte USt-Ausweis stimmt jetzt mit dem Bildschirm überein — und geht auf. Für dieselbe Zahl gab es drei Rechenwege: das Beleg-Widget zerlegte ganzzahlig über nettoCentsAusBrutto (richtig), PrintPaper.setKeckReceipt rechnete Netto und MwSt getrennt als double und rundete beide einzeln, und ustCents am Kassieren-Schirm rundete nach brutto * satz / (100 + satz). Auf dem Papier hieß das: 0,39 € zu 20 % druckten „MwSt 0,07 / Netto 0,33" — zusammen 0,40 € und damit mehr, als der Gast gezahlt hat. Über 1…10000 Cent zu 20 % nachgerechnet sind das 682 falsch gedruckte Zeilen, 583 davon gehen nicht auf; bei 19/13/10/4,9 % fallen die Regeln nie auseinander, weshalb der Fehler so lange durchging. Alle drei Wege rechnen jetzt über lib/src/vat_math.dart: einmal runden, die MwSt ist die Differenz.
  • Breaking: ustCents entfällt, ustSumme hat einen zweiten Parameter. An seine Stelle tritt ustCentsAusBrutto(bruttoCents, rate) — dieselbe Signatur, aber die Regel des Belegs (0,99 € zu 20 % sind 0,16 € MwSt, nicht 0,17; über 1…10000 Cent 1667 abweichende Beträge, alle bei 20 % und jedem Brutto ≡ 3 mod 6). ustSumme(warenkorb, {rabattCents}) gruppiert außerdem je Steuersatz statt je Position und zählt den Rabatt mit: dreimal 0,33 € zu 20 % ergaben je nach Tippweise 0,18 € oder 0,17 € statt der 0,16 €, die auf dem Beleg stehen, und ein Korb über 12,00 € mit 2,00 € Rabatt wies 2,00 € MwSt aus statt 1,67 €.
  • Ein kaputtes Logo kostet nicht mehr den ganzen Beleg. PrintPaper.setKeckReceipt rief decodePng(receipt.logo!) ungesichert auf, und zwar als erste Handlung — LogoService prüft beim Abruf nur den HTTP-Status, nicht ob die Bytes ein Bild sind (der eigene Test legt [1,2,3] als „Logo" im Cache ab). Eine Fehlerseite vom CDN, ein abgebrochener Download oder ein falscher Content-Type warfen damit ungefangen bis in die aufrufende App und nahmen den kompletten Bon mit, samt des gesetzlich vorgeschriebenen QR-Codes, der erst weit danach entsteht. Der Kassier stand vor einem Kunden und bekam kein Papier. Ein Logo ist Zierde: schlägt es fehl, druckt der Beleg jetzt ohne — wie das Kreiseck-Branding es schon immer tat.
  • Neue öffentliche Symbole (package:kasseneck_api/kasse.dart): nettoCentsAusBrutto, ustCentsAusBrutto, ustSummePositionen. Die richtige Regel lag bisher in lib/src/ und war von außen gar nicht erreichbar — wer die enthaltene MwSt anzeigen wollte, hatte nur die abweichende zur Wahl.
  • Der Beleg-QR bricht den Druck nicht mehr ab — und verschwindet nicht mehr lautlos. Der native ESC/POS-Weg (QrPrintMode.native, vom MyPos-Terminaldruck fest erzwungen) schob die Nutzlast ungeprüft durch latin1.encode, das bei jedem Zeichen über 0xFF wirft; BelegQr.data ist von der _printable()-Filterung als einziges Feld ausgenommen. Die Nutzlast geht jetzt als UTF-8 hinaus — für reines ASCII byteweise dasselbe wie bisher, und derselbe Code, den der Bildmodus-Weg über das qr-Paket ohnehin schon erzeugte. Bewusst nicht entschärft: ein durch ? ersetztes Zeichen ergäbe einen sauber lesbaren QR, der nicht mehr zum signierten Beleg passt — falsche Daten sind schlimmer als keine.
  • Behoben: das Längenfeld des nativen QR-Befehls lief ab 253 Byte Nutzlast über. pH stand fest auf 0x00 und pL wurde modulo 256 abgeschnitten; bei 300 Byte Nutzlast las der Drucker 44 Byte Daten. Die Länge geht jetzt als pL/pH hinaus; oberhalb dessen, was das Feld fasst (QRCode.maxNutzlast), wirft der Generator, statt dem Drucker etwas Protokollwidriges zu schicken.
  • Ein QR, der nicht entstehen kann, wird sichtbar — statt den Druck abzubrechen (nativer Weg) oder ihn stillschweigend wegzulassen (Bildmodus: if (kDebugMode) print(...), im Release also nichts). Abwägung: ein Abbruch hält die Kasse an, während ein Kunde davor steht, und macht den Beleg nicht rechtmäßiger — der ist an dieser Stelle längst signiert und im DEP. Ein stiller Ausfall wiederum liefert einen Pflichtbeleg, der vollständig aussieht und keiner ist, und hinterher weiß niemand davon. Der Bon läuft deshalb durch, trägt aber den Aufdruck „!! QR-CODE FEHLT !!" und darunter die Belegdaten in Klarschrift. Eine leere Nutzlast (das Backend kann data: "" liefern) gilt dabei als Ausfall statt als QR ohne Inhalt.
  • Der Ausfall erreicht den Aufrufer auf jedem Druckweg. PrintPaper.qrFehler allein genügte nicht: getBytesFromReceipt und getMyPosPaperFromReceipt bauen das PrintPaper intern und geben nur bytes bzw. myPosPaper heraus — das Objekt mit dem Ausfall wurde verworfen, und damit war er für printReceiptMypos(), printReceiptWifi(), printReceiptBluetooth() und KasseneckReceipt.getPrintBytes() unerreichbar. Neu: KeckPrinterService.letzterQrFehler (nur lesbar) trägt den Ausfall des zuletzt gesetzten Belegs und wird bei jedem Bau über getBytesFromReceipt/getMyPosPaperFromReceipt gesetzt, auch auf null — er bleibt also nie von einem früheren Beleg stehen. getPaperFromReceipt schreibt ihn ausdrücklich nicht, und damit auch KeckPrinter.printReceipt nicht: sonst überschriebe ein Druck auf Drucker A genau das Signal, das ein gleichzeitig laufender printReceiptMypos auf Terminal B gerade auslesen will. Ein Feld statt erweiterter Rückgabetypen, weil keiner der drei Rückgabewerte (Bytes, das fremde MyPosPaper, die fremde PrintResponse) den Ausfall tragen kann, ohne dass jede App ihre Druckstellen umschreibt — und weil dieser Dienst ohnehin durchgehend statisch ist (Papiergröße, Drucker-IP, Bluetooth-Gerät stehen global; zwei Belege gleichzeitig druckt er nicht). Wer ohne globalen Zustand auskommen will, nimmt das neue KeckPrinterService.getPaperFromReceipt (liefert das fertige PrintPaper) oder den transportbasierten KeckPrinter, dessen KeckPrintResult den Ausfall selbst mitführt.
  • Neue öffentliche Symbole: PrintPaper.qrFehler, QRCode.maxNutzlast, KeckPrinterService.letzterQrFehler (Getter, kein Setter), KeckPrinterService.getPaperFromReceipt, KeckPrintResult.qrFehler (additiv, beide Konstruktoren nehmen es optional benannt entgegen), PrintPaper aus printing.dart.
  • Behoben: der Kachel-Artikel las seinen Preis abgeschnitten statt gerundet. KasseArtikel.aus nahm (json['unitPriceCents'] as num).toInt(), während alle vier Schwesterstellen (KasseneckItem, KeckVoucher, KeckInvoiceItem, belege.dart) runden. Rechnet das Backend den Preis in JavaScript aus Euro hoch — der Normalfall —, ist 19.99 * 100 dort exakt 1998.9999999999998: der Artikel verkaufte sich dauerhaft einen Cent zu billig, und zwar auf einen signierten Beleg. Ebenso 8,70 → 8,69, 1,15 → 1,14, 0,29 → 0,28.
  • Behoben: der MyPos-Textfallback der MwSt-Tabelle schnitt große Beträge ab. Die Spaltenbreite stand fest auf 32 ~/ 4 — unabhängig von der Papierbreite, auf 80 mm also weiterhin in der 58-mm-Aufteilung —, und jede Spalte lief durch substring(0, len): ab 100.000,00 € fiel die letzte Stelle des Betrags weg („100000,00" wurde zu „100000,0"). Die Breite kommt jetzt aus paperSize.defaultCharCount, und aufgefüllt wird nur, nie gekürzt. Eine zu breite Zeile ist unschön; ein gekürzter Betrag ist falsch.
  • Breaking: betragAusText weist Beträge über hoechstbetragCent (100.000,00 €) jetzt mit null ab. Die Konstante wurde bis hierher an keiner Stelle gelesen — der Kommentar beschrieb einen Schutz, den es nicht gab: „1250" für gemeinte 12,50 € ergab ungebremst 1.250,00 € auf einem unveränderlichen Beleg. Im selben Zug liefert die Funktion bei sehr vielen Ziffern null, statt entgegen ihrer Zusage eine FormatException aus int.parse zu werfen (ab 19 Stellen, erreichbar über eine hängende Taste oder eine eingefügte Zeichenkette).

Am Terminal gemessen und offen Gebliebenes:

  • Die sechs Annahmen über das Terminal sind am 26.08.2026 abgearbeitet (TID 3600335, HPS 1.10.0, Firmware 7.3.6). Vier davon sind gemessen, zwei bleiben offen:
    • Wiederholung unter derselben Kennungwiderlegt. Für eine bereits genehmigte Kennung startet das Terminal einen zweiten Kartenfluss; es entdoppelt nicht. Eine Wiederholung unter derselben Kennung schützt vor nichts. Eine bereits genehmigte Kennung bleibt in der Statusabfrage aber genehmigt, auch nachdem unter ihr ein zweiter Versuch abgebrochen wurde.
    • Kennt das Terminal eine Kennung nach einem Abbruch noch / was antwortet transactionStatus() auf eine unbekannte Kennunggemessen: in beiden Fällen 9027, ebenso bei laufendem Kartenfluss und bei abgebrochenem Vorgang. Nur eine genehmigte Zahlung antwortet mit 0. Siehe den Klärweg oben.
    • Scheitert abort() zuverlässig, sobald die Karte aufliegtpräzisiert: die Grenze ist nicht der Kartenkontakt, sondern der Abschluss. abort gelingt (0), solange der Vorgang läuft — auch während der Kunde die Karte auflegt —, und scheitert (100010), sobald er genehmigt ist; die genehmigte Zahlung bleibt unangetastet. Genau darauf beruht der neue Klärweg.
    • Was ein leerer 200-Rumpf bedeutet und ob eine nicht rein numerische Kennung angenommen wirdweiterhin offen. Beides ließ sich ohne künstlich herbeigeführte Fehlerlagen nicht auslösen. Die im Code hinterlegte konservative Behandlung (leerer Code wie fehlender Code; geprüft wird nur die Länge, nicht der Ziffernvorrat) bleibt.
  • Nachgemessen am 26.08.2026: abort wirkt auf einer laufenden Rückerstattung genauso. Der Abbruch antwortet auch dort mit responseCode "0", und die Gutschrift endet daraufhin mit 100002 „Aborted". Damit ist der Klärweg für refund gemessen und nicht nur aus dem Aufbau geschlossen — der Abbruch-Endpunkt kennt keinen Transaktionstyp, und die übergebene Kennung ist die des neuen Vorgangs.
  • Zwei Nebenbefunde derselben Messung: die Diagnose führt ein Feld transactionId, das auch während eines laufenden Kartenflusses null bleibt und als Unterscheidungshilfe damit unbrauchbar ist; und /api/terminals antwortet auf dieser Firmware mit 404 Endpoint not implemented — die in 4.8.0 ergänzte terminals()-Methode läuft dort ins Leere.
  • Die Storno-Messung hat den Pflichtparameter aus 4.7.0 bestätigt: ein Void ohne amount wird mit 400 Missing amount abgewiesen. Die Aufhebung selbst bekommt eine eigene neue transactionId; die an cancel() übergebene Kennung bleibt die der Originalzahlung.
  • Weiterhin unbestätigt: die Erreichbarkeit von hobexGetStatus unter api.kasseneck.at/v1 — die Methode ist bisher nur gegen Mocks geprüft, nicht gegen das Backend. Der Cloud-Weg ist von den HPS-Messungen nicht berührt: er kennt weder abort() noch 9027 und bleibt unverändert.

4.24.0 #

  • Trinkgeld-Empfänger abrufen (listMyTipRecipients): tipEmpfaenger() am Kassen-Client und derselbe Aufruf auf dem api_key-Weg liefern die Personen, denen sich Trinkgeld zuweisen lässt — je Person registerUserId, name und owner — dieselbe Gestalt wie der Empfänger am Beleg-Item.
  • Es ist dieselbe Menge, die der Verkauf akzeptiert: Wer in der Liste steht, wird von createReceipt nicht zurückgewiesen. Entschieden wird das im Backend an einer Stelle, die Liste und Annahme gemeinsam benutzen.
  • KeckTipPerson.mit(cents:) macht aus einer Person einen KeckTipRecipient. Wer die Liste benutzt, kann keine Kennung erwischen, die der Server ablehnt.
  • owner entscheidet die Bezeichnung am Beleg: „Trinkgeld" (Inhaber — Entgelt des Betriebs) gegen „Trinkgeld Personal" (durchlaufender Posten). Deshalb reist das Flag mit — als nicht nullbares bool, denn ein fehlendes Flag gäbe es bei dieser Frage nicht.
  • Ist ein Kassen-Benutzer ohne das Recht tipAssign angemeldet, steht nur er selbst in der Liste.
  • Vertrag auf @kreiseck/kasseneck-api 0.6.46 angehoben.

4.23.0 #

  • Die Hobex-Transaktionskennung entsteht jetzt nach demselben Verfahren wie im JS-Zwilling (@kreiseck/kasseneck-api, newHobexTransactionId in src/payments/hobex.ts). Bisher rechneten beide Pakete unterschiedlich: hier Geraetezeit plus Mikrosekunden-Rest und eine Zufallsziffer, dort Wiener Wanduhrzeit plus vier Zufallsziffern. Ein Verfahren statt zweier.
  • Wiener Zeit statt Geraetezeit (ViennaTime.toWallClock): zwei Kassen desselben Betriebs in verschiedenen Zeitzonen trugen sonst Kennungen, die sich um Stunden unterscheiden, und der Tageswechsel in der Kennung fand nicht zum Geschaeftstag statt.
  • Vier Zufallsziffern statt Mikrosekunden-Rest und einer Zufallsziffer. Der Zwilling laeuft in JavaScript und hat keine Mikrosekunden-Uhr; vier Zufallsziffern unterscheiden zwei Zahlungen derselben Millisekunde besser als eine.
  • Die Form der Kennung aendert sich damit: der Zeitanteil steht jetzt in den ersten 15 Stellen (JJMMTThhmmss + Millisekunde), der Zufallsanteil in den letzten vier. Laenge und Zeichenvorrat bleiben unveraendert: 19 Stellen, rein numerisch. Bereits erzeugte Kennungen bleiben gueltig; Hobex prueft nur Laenge und Ziffern.
  • newHobexTransactionId({DateTime? zeitpunkt, double Function()? zufall}): neben dem Zeitpunkt ist jetzt auch die Zufallsquelle einspeisbar — beides dient dem Test. Aufrufe ohne Parameter sind unveraendert.
  • Gemeinsame Golden-Werte in beiden Testsuiten: je ein fester Zeitpunkt in der Winter- und in der Sommerzeit, mit festem Zufallswert, ergibt eine Zeichenkette, die in test/kasseneck_api_client_test.dart und im npm-Paket in test/payments.test.ts wortgleich gepinnt ist. Beide Seiten rechnen die Sommerzeit-Umstellung selbst aus; laufen sie auseinander, faellt ein Test statt eines Terminals.

4.22.0 #

  • Absturz beim Anlegen einer Hobex-Kartentransaktion behoben. KasseneckApi.newHobexTransactionId() baute die Transaktionskennung aus DateTime.now().toString() und schnitt daraus 19 Stellen zu. Dart laesst in dieser Textform den Mikrosekunden-Rest jedoch weg, sobald er 0 ist — dann war der Zwischenstring drei Stellen zu kurz und der Zuschnitt warf einen RangeError. Das traf rund jeden tausendsten Aufruf, und zwar im Zahlungsweg: beim Anlegen der Kartentransaktion, waehrend der Kunde am Terminal steht. Der Fehler steckte seit 1.1.0 in jeder veroeffentlichten Fassung.
  • Die Kennung wird jetzt aus den Bestandteilen des Zeitpunkts mit fester Stellenzahl gebildet (Jahr ohne Jahrhundert, Monat, Tag, Stunde, Minute, Sekunde je 2 Stellen, Millisekunde 3, Mikrosekunden-Rest 3, dazu eine Zufallsziffer 1-9) statt aus einem zurechtgeschnittenen Text. Das Format bleibt unveraendert: 19 Stellen, rein numerisch, aus der Uhrzeit abgeleitet — im bisher funktionierenden Fall kommt Zeichen fuer Zeichen dasselbe heraus. Nur der Absturzfall faellt weg.
  • newHobexTransactionId({DateTime? zeitpunkt}): der neue benannte Parameter dient allein dem Test, damit der Fehlerfall gezielt getroffen werden kann statt zufaellig. Aufrufe ohne Parameter sind unveraendert.
  • Neue Tests fuer genau diese Faelle: Mikrosekunden 0, Millisekunden und Mikrosekunden 0, einstellige Werte in Monat/Tag/Stunde, Jahreswechsel, volle Stellen — sowie ein Vergleich ueber 500 Zeitpunkte, der die Gleichheit mit der bisherigen Bildung festhaelt.

4.21.0 #

  • Zwillingsprüfung: Das Paket prüft sich gegen den Vertrag des JS-Pakets @kreiseck/kasseneck-api (angeheftete Version in zwillinge.yaml). Die Vertragsdateien werden mit tool/zwillinge.sh ziehen geholt und nie von Hand geändert; die CI vergleicht die Kopie byteweise mit dem veröffentlichten Tarball.
  • Vier Prüfungen in flutter test: Standardwerte, Enum-Werte (jeder Wert muss das Einlesen überstehen), Rechte-Schlüssel (kein Schlüssel darf im Auffangbecken landen) und Aufrufnamen. Was fehlt, muss in zwillinge.yaml benannt werden — dauerhaft mit Grund oder als offene Schuld mit Issue-Nummer.
  • Damit sind 33 Lücken benannt, die dieses Paket bisher stillschweigend hatte: 20 Enum-Werte, 5 Aufrufe, 6 Tasten-Aktionen und 2 abweichende Standardwerte. Alle stehen als art: offen mit Issue-Nummer in zwillinge.yaml (#22 bis #26); die CI schreibt bei jedem Lauf, wie viele es noch sind.
  • Die Buchführung ist in beide Richtungen streng: Wer eine Lücke schließt, ohne die Zeile aus zwillinge.yaml zu streichen, wird ebenso rot wie umgekehrt — sonst sänke die Zahl in der CI nie.
  • Vorgabefarbe ist #116B6B (Petrol aus der Markenpalette) — betrifft Betriebe ohne eigene Farbe.
  • belegAusgabe steht standardmäßig auf fragen: die Fertig-Seite bietet QR und Bon an, wie Backend und Browser-Kasse es längst tun.
  • Die Aufrufnamen stehen als Konstanten in Aufrufe an einer Stelle.
  • Zwei Standardwerte weichen weiterhin ab und stehen in zwillinge.yaml unter wert_ausnahmen — unter derselben Strenge wie die Hauptliste: die Tastenbelegung tasten (Issue #25) und terminalPort, Vertrag 8080 gegen 20008 hier (Issue #23).

4.20.0 #

  • Trinkgeld am Beleg (models/keck_tip.dart): sellReceipt(tip: …) reicht Betrag, Zahlart und Empfänger an createReceipt durch; die Positionen baut das Backend (tip-core). Absicht: Ob ein Anteil Entgelt ist (Inhaber, anteilig auf die Steuersätze der Warenpositionen) oder durchlaufender Posten mit 0 % (Mitarbeiter, Erlass 2.4.6/2.4.2.1), entscheidet dort das inhaber-Flag des Kassen-Benutzers — der Aufrufer schickt für beide dasselbe.
  • KeckTip.fuer(registerUserId, cents: …) für den Regelfall, KeckTip.euro(…) mit einmaliger Rundung, KeckTipRecipient für die Aufteilung auf mehrere.
  • Geprüft wird schon im Client, mit dem Wortlaut des Backends: Betrag als ganze Zahl in Cent > 0, Empfängerliste nicht leer, Summe der Anteile gleich dem Betrag, keine Person zweimal. Ein falscher Betrag verursacht damit keinen Netzweg.
  • ReceiptType.allowsTip: nur standard und training. Ein Storno spiegelt die Positionen des Originals — über den Parameter entstünde beim Zurücknehmen neues Trinkgeld. Ein Beleg nur mit Trinkgeld wird abgelehnt (er hängt an einer Leistung).
  • Trinkgeld auslesen: KasseneckReceipt.tipItems/tipCents/tip sowie getrennt staffTipCents (durchlaufender Posten — bei Karte der Betrag, der weitergegeben werden muss) und ownerTipCents (Entgelt, in sumCents enthalten). An der Position: KasseneckItem.isOwnerTip, tipRecipientId, tipRecipientName. Gerechnet wird aus den Positionen, nicht aus dem abgeleiteten tipCents des Belegdokuments — signiert sind die Positionen.
  • Nachgewiesen an den Golden-Belegen des JS-Pakets (rabatt-trinkgeld, rabatt-chef-trinkgeld) und gegen die echte Demo-Kasse: Beleg mit Trinkgeld ausstellen, 0-%-Position und Gesamtbetrag prüfen, stornieren — die Spiegelung nimmt das Trinkgeld vorzeichengetreu zurück.
  • Ohne tip geht kein Feld hinaus; der Aufruf ist unverändert.

4.19.0 #

  • KasseneckItem: neue optionale Felder kind ('tip'/'discount'), recipient, paymentMethod, articleId — Zwilling von ReceiptItem im JS-Paket 0.6.44. Die Kennzeichnungen reisen durch toJson/fromJson und negative (Storno-Spiegelung); Zeilen ohne bleiben schlank.
  • verteileRabatt kennzeichnet seine Zeilen als kind: 'discount' — der Bon fasst sie zu einer Summenzeile mit Zwischensumme zusammen, der Bericht führt sie als „Rabatte“.
  • articleId an Positionen ist die Grundlage der Erlösgruppen-Zuordnung im Monatsbericht.
  • Golden-Belege auf den Stand des JS-Pakets 0.6.44 gehoben: 22 Fälle (neu: rabatt-einfach, rabatt-trinkgeld, rabatt-chef-trinkgeld, storno-rabatt, rabatt-wertgutschein).

4.18.0 #

  • Belegaufrufe der Kasse (package:kasseneck_api/kasse.dart): RegisterReceiptClient mit verkaufen (Normalbeleg samt Belegkopf in einer Antwort), auflisten (Zusammenfassungen mit Summe in ganzen Cent, Bediener, Storno-Stand), holen und stornieren (voll oder in Teilen, mit Grund und Restmengen). Der Verkauf ist der einzige nicht folgenlos wiederholbare Aufruf — es geht genau ein Aufruf hinaus, auch nach einem Netzhaenger.
  • Gemeinsamer Weg der laufenden Sitzung: RegisterTransport (ID-Token als Bearer, Sitzung als Kopfzeile register-session, Kasse als Parameter, eigene Frist je Aufruf). RegisterSessionClient laeuft jetzt darueber, statt die Huelle ein zweites Mal zu fuehren; sein Verhalten aendert sich nicht.
  • Eine leere Storno-Positionsliste ist ein Fehler und kein Vollstorno — sonst wuerde aus einem missglueckten Teilstorno still ein voller.

4.17.0 #

  • Warenkorb und Kassieren (package:kasseneck_api/kasse.dart), Zwilling von warenkorb.ts und kassieren.ts der Browser-Kasse: Warenkorb (erfassen, Menge setzen, Höchstmenge je Beleg, verkaufte Positionen abziehen, Anzeigezeilen je Mengenmodus), betragAusText/alsEuro (jeder Betrag eine ganze Zahl in Cent — gelesen über die Ziffern, nie über Fließkomma), zahlungsarten, rabattCents, zuZahlen, rueckgeld, schnellbetraege, abschlussPruefung, ustCents/ustSumme.
  • verteileRabatt: Rabatt als negative Position je Steuersatz mit Rundung nach größtem Rest — die Summe der Zeilen ist immer genau der Rabatt, keine Zeile größer als der Umsatz ihres Satzes.

4.16.0 #

  • Behoben: die Kopplungs- und Sitzungsaufrufe zeigten auf api.kasseneck.at/v1 — dort antwortet auf sie eine HTML-404, die Kopplung schlug also immer fehl. Sie liegen hinter den Hosting-Umschreibungen der Kasse; die Vorgabe ist jetzt https://kasse.kasseneck.at/api.

4.15.0 #

  • Kassen-Einstellungen (package:kasseneck_api/kasse.dart): KasseSettings mit allen 38 Betriebs- und 20 Gerätefeldern als Zwilling von kasse/settings.ts und kasse-settings-core.js. Standardwerte gegen die Golden-Datei des JS-Pakets geprüft; Gespeichertes wird gemischt (Landkarten je Schlüssel, damit neue Steuersätze beim Altbestand ankommen), Unbekanntes fällt auf den Standard zurück statt zu raten. Dazu kartenAktiv (Karte nur mit eingerichtetem Anbieter) und aktiveSaetze (feste Reihenfolge für den Bildschirm).
  • listRegisterUsersForDevice liefert settings jetzt als KasseSettings statt als Rohdaten.

4.14.0 #

  • RegisterClient.sitzung(...) liefert den RegisterSessionClient mit derselben Adresse, demselben HTTP-Client und demselben Zeitlimit — eine Verbindung statt zweier, und ein für Tests eingesetzter HTTP-Client erwischt beide Wege.

4.13.0 #

  • Laufende Sitzung (register.dart): RegisterSessionClient mit renewRegisterSession (liefert den neuen Ablauf) und endRegisterSession. ID-Token und Sitzung werden bei jedem Aufruf frisch erfragt — Tokens laufen nach einer Stunde ab, die Kassen-Sitzung schon nach 90 Sekunden.

4.12.0 #

  • Kopplung und Anmeldung eines Kassengeräts (package:kasseneck_api/register.dart), Zwilling von register/pairing.ts im JS-Paket: RegisterClient mit pairRegisterDevice (achtstelliger Code aus dem Panel → dauerhafter Geräte-Ausweis), listRegisterUsersForDevice (Benutzer, PIN-Regel, Anmeldemodus, Standortsperre), registerUserLogin / registerPinLogin (Sitzung: Custom Token + sessionId) und unpairRegisterDevice. Diese Aufrufe laufen ohne Anmeldung — der Code bzw. das Gerätegeheimnis ist der Nachweis; sie stehen deshalb neben KasseneckApi und nicht darin.
  • Rechte werden gelesen wie im Backend: Schalter als ja/nein, cancelScope/receiptsScope als Reichweite (none|own|all), Altbestand ohne Reichweite migriert (cancel entscheidet, Belege gelten als „alle"). Ein fehlendes Recht gilt als nicht erteilt.
  • Fehlerarten: KasseneckValidationError (Aufruf oder Antwort unvollständig), KasseneckApiError (fachlicher Fehler des Backends), KasseneckHttpError. Weder PIN noch Gerätegeheimnis stehen je in einer Meldung.

4.11.0 #

  • Zeichenraster BelegRaster (models/beleg_raster.dart), Zwilling von renderReceiptGrid im JS-Paket: der Beleg als Zeilen mit exakt 32 (58 mm) bzw. 48 (80 mm) Zeichen — Spalten in ganzen Zeichen, rechte Spalte bündig, mindestens ein Leerzeichen zwischen Spalten, wortweiser Umbruch. Golden-Vergleich gegen grid32.txt/grid48.txt des JS-Pakets.
  • PrintPaper.setBelegLayout druckt jetzt genau diese Rasterzeilen (keine eigene Spaltenrechnung mehr, kein ESC $) — dieselben Zeilen wie Browser-Kasse, Labor und Beleg-PDF. Verhalten von setKeckReceipt unverändert.
  • 58-mm-Regeln des Rasters (wie JS-Paket 0.6.7): läuft in einer Spaltenzeile nur eine Spalte über die erste Zeile hinaus, bekommt ihr Rest die volle Breite (lange Artikelnamen); überlange Wörter brechen am Bindestrich; geschütztes Leerzeichen bricht nie („je 0,79“ bleibt zusammen). Fixtures auf Stand 0.6.7.

4.10.0 #

  • Beleg-Zeilenmodell des Backends (rein additiv): KasseneckReceipt.layout (BelegLayout, aus getReceiptlayout), dazu testKasse, testSignatur, kopfId. Das Backend friert Kopf/Fuß je Beleg ein und baut das Zeilenmodell mit dem Belegart-Aufdruck (STORNOBELEG, TRAININGSBELEG, NULLBELEG/STARTBELEG/MONATSBELEG/JAHRESBELEG/SCHLUSSBELEG — RKSV § 11 Abs. 3), reduziertem Nullbeleg und TESTKASSE/TESTSIGNATUR-Warnrahmen.
    • KeckReceiptLinesWidget(layout:, qrCovered:) zeichnet das Modell in der App; PrintPaper.setBelegLayout(layout) druckt es (Banner fett, doppelt hoch, Warnungen invers).
    • Damit zeigen App, Bondrucker, Browser-Kasse und Beleg-PDF dieselben Zeilen; die 17 Golden-Belege des JS-Pakets liegen als Kopie unter test/fixtures/belege und werden per Prüfsumme gegen dessen Manifest gehalten.
    • Empfehlung: layout != null → Zeilenmodell zeichnen/drucken; sonst wie bisher KeckReceiptWidget/setKeckReceipt (Altbelege, altes Backend).

4.9.0 #

  • KeckReceiptWidget.qrCovered (Vorgabe false, rein additiv): der RKSV-QR wird zunächst weichgezeichnet und nicht scannbar gezeigt (mit Hinweis qrCoveredText, Vorgabe „Antippen zum Anzeigen"); ein Tipp macht ihn lesbar, ein zweiter verdeckt ihn wieder. Für Bildschirme, auf denen der Beleg nur zur Kontrolle steht — der Signatur-QR gehört dem Kunden und wird erst auf Verlangen freigegeben. Druck und Belegdaten sind unberührt.

4.8.0 #

  • hobex HPS: lokale Terminal-Abfragen ergänzt (rein additiv, keine bestehende Signatur geändert). Zwei neue read-only Methoden im HpsClient:
    • terminalStatus()GET /api/terminals/{tid}/status: leichter Readiness-Check. true bei HTTP 200 (bereit), false bei 503 (nicht betriebsbereit); dieser Endpoint hat keinen Response-Body (im Gegensatz zum feldreichen diagnosis()).
    • terminals()GET /api/terminals: Liste der konfigurierten Terminals als neues Modell TerminalInfo (tid, company/merchantName, Beleg-header-Zeilen, terminalType, active, …). Alle Felder optional, da Spec-Tabelle und Beispiel-Response divergieren; unmodellierte Keys bleiben über TerminalInfo.raw erreichbar.
    • Intern wurde das Senden in _send() extrahiert (frische-Verbindung-Logik aus 4.7.0 unverändert), damit terminalStatus() den 503-Status als Wert statt als Ausnahme behandeln kann.
  • Bewusst NICHT enthalten: Beleg-Download (/api/transaction/download) und die Profil-Flags canVoid/canRefund sind reine Cloud-Endpoints (online.hobex.at, JWT-Auth) und gehören nicht in den authlosen lokalen HPS-Client. Ein responseCode-Fehlercode-Katalog ist nicht enthalten, da die REST-Spec v1.13 außer "0" (=OK) keine Codes enumeriert (weitere Codes stammen vom Acquirer).

4.7.0 #

  • hobex HPS: Storno/Refund-Robustheit. Zwei Fehler behoben, die Rückabwicklungen am HPS-Terminal unzuverlässig machten:
    • Verbindungs-Wiederverwendung: HpsClient hielt eine Keep-Alive-Verbindung offen und verwendete sie wieder; das Terminal schließt inaktive Sockets aber, wodurch der nächste Request (typisch: Storno/Refund nach einer Bedien-Pause) mit „Connection closed before full header" abbrach — ohne Retry. Selbst erzeugte Clients bauen jetzt pro Request eine frische Verbindung auf (injizierte Clients für Tests bleiben unverändert; bewusst kein Auto-Retry wegen Doppelbuchungsgefahr bei Zahlung/Refund). close() ist damit ein No-op.
    • Void ohne Betrag (400 Missing amount): cancel() sendete keinen Betrag. Signatur jetzt cancel({required transactionId, required amount, currency, language, technicalCancel}) — sendet amount/currency (+ optional language) als Query-Parameter (wie in der hobex-Postman-Collection; die REST-PDF v1.13 listet den Parameter nicht, die Firmware verlangt ihn). Breaking: amount ist jetzt Pflicht.

4.6.0 #

  • Zahlungsart-Zeile auf jedem Beleg (Druck + KeckReceiptWidget): direkt unter dem Gesamtbetrag steht Zahlungsart: Barzahlung/Kartenzahlung/Onlinezahlung/… (KeckPaymentMethod.label, Labels identisch zum Backend-Beleg-PDF) — auch wenn zusätzlich ein Provider-Kartenblock folgt.

4.5.0 #

  • ViennaTime: Geschäftszeitzone Europe/Vienna (package:kasseneck_api/services/vienna_time.dart). Der Kasseneck-Server liefert Beleg-Timestamps als Wiener Wanduhrzeit ohne Offset; auf Geräten mit fremder Zeitzone (z. B. im Ausland) verrutschten dadurch Beleg- und Buchungs-Tage gegeneinander. ViennaTime rechnet deterministisch per EU-Sommerzeitregel (letzter Sonntag März/Oktober, 01:00 UTC — kein tz-Paket nötig): fromWallClock/toWallClock, parseServerTimeStamp, dayKey, now/today, deviceDiffersFromVienna.
  • KasseneckReceipt.timeStamp ist jetzt immer ein echter Zeitpunkt (UTC): Server-Timestamps werden beim Parsen als Wiener Wanduhrzeit interpretiert (ViennaTime.parseServerTimeStamp); toReceiptJson serialisiert UTC mit Z (Roundtrip-kompatibel, alte naive Strings werden weiterhin korrekt gelesen). readableTime zeigt unverändert Wiener Zeit — jetzt auch bei fremder Geräte-Zeitzone.
  • ReportMonth.now() bestimmt den aktuellen Monat nach Wiener Zeit.
  • CreditCardProvider.stripe: Kartenblock für Online-Zahlungen (Stripe-Payment-Link). Das Backend hängt an online-Belege creditCardProvider: 'stripe', cardPaymentId (PaymentIntent-ID) und cardPaymentData an; Beleg-Druck (print_paper.dart) und App-Viewer (KeckReceiptWidget) rendern daraus einen zentrierten Block identisch zum Backend-Beleg-PDF: Titel „Online-Zahlung (Stripe)", Kartenmarke + Kartenart (Debit/Kredit/Prepaid) + Wallet (Apple/Google Pay), maskierte Kartennummer, „3-D Secure: ja", bei EPS die Bank, Gesamtbetrag, Zahlungszeitpunkt („Bezahlt:", Wiener Zeit via ViennaTime), Abrechnungstext und PaymentIntent-Referenz. Beide Renderer speisen sich aus einer gemeinsamen stripeReceiptLines() — Druck und Widget können nicht auseinanderlaufen (abgesichert durch stripe_render_consistency_test.dart). Fehlende Felder lassen ihre Zeile entfallen; kaputte Daten brechen den Druck nicht ab.

4.4.0 #

  • Unified KeckPrinter with a PrinterTransport abstraction. New high-level printer facade that separates ESC/POS byte building from transport, replacing reliance on global printer state for new code. Ships two transports — WifiTransport (raw TCP, port 9100) and BluetoothTransport (its own BluetoothDevice, no shared global device) — and is USB-ready via the public PrinterTransport interface. Convenience factories KeckPrinter.wifi(...) / KeckPrinter.bluetooth(...), plus DI-friendly KeckPrinter(transport). High-level ops printReceipt/printText/printQr/printBarcode/cut/openDrawer/feed/printJob/printRawBytes each return a KeckPrintResult (never throw). All exported from package:kasseneck_api/printing.dart. Backward compatible: the static KeckPrinterService API is unchanged — the Bluetooth send logic (MTU negotiation, discovery, chunking, flow control) was extracted into a shared helper (writeToBluetoothDevice), behaviour identical.

4.3.0 #

  • 1D barcode support in the vendored ESC/POS engine: EscPosGenerator.barcode(type, data, {align, height, width, hri}) and the fluent CustomPrintJob.barcode(...). Emits GS k form 2 (length-prefixed) with the symbologies UPC-A, UPC-E, EAN-13, EAN-8, CODE39, ITF, CODABAR, CODE93 and CODE128 (auto code-set B — {B is prepended unless the data already starts a code-set sequence), plus height (GS h), width (GS w) and HRI position (GS H) options. BarcodeType and BarcodeHri are exported from package:kasseneck_api/printing.dart. This closes the last gap vs. esc_pos_utils_plus — the print stack is now fully self-contained.

4.2.0 #

  • Crisp image QR: the image-based QR (addQrCodeAsImage, used by the imageRaster/bitImage print modes) is now rasterized directly from the QR module matrix with an integer per-module scale and no anti-aliasing — pure black/white pixels, sharp by construction. This replaces the previous QrPainter → PNG encode/decode path, whose non-integer pixel size (280 px over a variable module count) produced fringed edges and a larger image that was slow over Bluetooth; the new bitmap is smaller and faster to send. The native QR command (addQrCode) and the Bluetooth send path are unchanged.

4.1.1 #

  • Cleans up the WiFi raw-print API from 4.1.0: dropped the unused size parameter (raw bytes are already rendered), renamed the result type PrintResultKeckPrintResult (avoids clashing with app-level PrintResult types), documented that success means sent (bytes written to the socket) — not guaranteed printed (raw TCP to a thermal printer has no application ACK), and de-duplicated the socket send behind a shared internal helper. Final shape: KeckPrinterService.printRawBytesWifi(bytes, {required ip, port = 9100, timeout = 5s}) → Future<KeckPrintResult>.

4.1.0 #

  • Direct WiFi raw printing: KeckPrinterService.printRawBytesWifi(...) sends finished ESC/POS bytes straight to a network printer over a short-lived socket, without touching the globally initialized printer (ipAddress/port/the active device stay untouched). It never throws — the outcome is reported as a result object, so callers can retry or show a hint. Exported from package:kasseneck_api/printing.dart. (Superseded by 4.1.1, which finalizes the signature and result type.)

4.0.0 #

  • Decoupled print stack. The ESC/POS generator and the (correct) rasterizer from esc_pos_utils 1.1.0 are now vendored internally under lib/src/printing/; PNG de/encoding runs via dart:ui. The runtime no longer depends on esc_pos_utils_plus, and image is a dev-only dependency — apps that use this package are no longer version-locked to image and pull in no print dependencies at runtime.
  • Fixes the Bluetooth/thermal print failure for image widths that are not a multiple of 8 (a crash in the old esc_pos_utils_plus rasterizer). QR and logo printing are unchanged in behaviour (native QR is byte-identical, image-QR keeps its white background and quiet zone).
  • New custom-print API on KeckPrinterService, additive and sending to whichever printer is currently initialized (Bluetooth or WiFi, same send path as the receipt printers): printRawBytes(List<int>), plus the high-level helpers printText, printQr, cut, openDrawer and feed. A CustomPrintJob batch builder (text/qr/cut/drawer/feed/raw, fluent) accumulates several commands into a single byte stream printed in one send via printJob (preferred for Bluetooth). The vendored EscPosGenerator and the required types (PosStyles, PosAlign, PosCutMode, PosDrawer, QRSize, CapabilityProfile, …) are exported from the new package:kasseneck_api/printing.dart barrel so integrators can build bytes themselves and send them with printRawBytes.
  • Branded API base URL: the client now talks to https://api.kasseneck.at/v1 instead of europe-west1-kasseneck.cloudfunctions.net. The receipt-download base URL is unchanged.
  • v2 item shape: KasseneckItem.toJson() now sends { name, quantity, unitPriceCents, vatRate } with the unit price as integer cents (no floating-point amounts). fromJson() reads both the new v2 form and the legacy v1 form (priceOneCents/priceOne, amount, vat), preferring cents for exactness — old stored receipts keep parsing.
  • Breaking: KeckPaperSize.paperSize is now typed EscPaperSize (internal) instead of PaperSize from esc_pos_utils.

3.3.0 #

  • Receipt download links now use the branded path-based URL https://beleg.kasseneck.at/<token> instead of https://receipt.kreiseck.com/downloadReceipt?fullReceiptId=<token>. The backend serves both the new path form and the old query form, so links on already printed or shared receipts keep working.

3.2.1 #

  • GP Tom card details render correctly with gptom_aidl_plugin ≥ 0.1.0: cardPaymentData amounts arrive as integer cents and are now formatted as such (older stored receipts with euro doubles keep working); applies to thermal print and KeckReceiptWidget
  • GP Tom transaction type is recognized via both transactionType and the plugin's transacitonType key; Refund (type 3) is now labelled

3.2.0 #

  • Kreiseck branding on receipts: when the backend metadata flag kreiseck_logo is set (Firestore users/{uid}.branding.kreiseck_logo), the receipt ends with "powered by" and the Kreiseck logo — on thermal prints (85 % paper width), in KeckReceiptWidget and survives JSON round-trips for reprints
  • The logo ships as a package asset (printing works offline; the backend only sends the flag); branding can never break receipt printing
  • Fixed an ESC/POS rasterizer crash for images whose width is not a multiple of 8

3.1.1 #

  • Zero analyzer issues: debug-only logging, migrated deprecated APIs (License.nonprofit, QR eyeStyle/dataModuleStyle), removed redundant imports; FinanzOnline status enum names are intentionally kept verbatim (they must match the rkdbMessage values)
  • CI runs flutter analyze in strict mode again

3.1.0 #

  • Comprehensive test suite (~100 new tests): money & receipt math, JSON round-trips and fallback parsing, voucher rules, mocked API client, and a print↔widget consistency check that guards the two independent VAT-table renderers against drift
  • KasseneckApi and LogoService accept an injectable http.Client (useful for testing/mocking; default behaviour unchanged)
  • financeWebService requests now also time out after 30 s
  • Continuous integration: analyze + tests run on every push

3.0.1 #

  • getReceipts no longer fails wholesale when a single receipt can't be parsed — broken receipts are skipped (and logged in debug builds)
  • Zero receipts (no items) parse correctly; item quantities also accept 1.0
  • All HTTP requests time out after 30 s instead of hanging silently forever
  • Odd voucher amounts are displayed exactly (e.g. € 1,50 instead of ~2)

3.0.0 #

Breaking: money is now integer cents — exact arithmetic, no floating-point drift. No backend update is required: requests carry BOTH representations (priceOne/value in euro for the current backend and priceOneCents/valueCents/singlePriceCents, preferred by newer backends). When reading, the cents fields are preferred; euro-only data (old receipts) still parses — the euro↔cents round-trip is lossless (verified by property tests).

Migration:

  • KasseneckItem(singlePrice: 19.99)KasseneckItem(priceCents: 1999) or KasseneckItem.euro(singlePrice: 19.99)
  • KeckVoucher(value: 5.0)KeckVoucher(valueCents: 500) or KeckVoucher.euro(value: 5.0)
  • KeckInvoiceItem(singlePrice: …)priceCents / KeckInvoiceItem.euro(…)
  • Reading: item.singlePrice, voucher.value, receipt.sum / subSum still exist as euro views; for arithmetic use priceCents / valueCents / sumCents / subSumCents / totalCents
  • Terminal APIs (hobexPay, HpsClient, SumUp) keep euro amounts — they mirror the external providers' formats

2.1.3 #

  • Reliable Bluetooth thermal printing: flow control via write-with-response (backpressure), negotiated MTU with matching chunk size, and pacing for write-without-response printers
  • Fixed garbled QR output: the QR image is now composited onto a white background with a quiet zone (QrPainter renders on transparent, which ESC/POS rasterization printed as solid black)
  • New QrPrintMode (imageRaster / imageBitImage / native) on printReceiptBluetooth / getPrintBytes to pick the command your printer supports (replaces qrAsImage)

2.1.2 #

  • Cleaner static analysis (0 warnings): KeckVoucher.value is now nullable; removed dead null-aware code and unused SumUp leftovers
  • Loosened the my_pos version constraint to ^0.3.0

2.1.1 #

  • New README, a runnable example/ and inline API documentation (dartdoc)
  • Added repository & issue tracker metadata

2.1.0 #

  • hobex Payment Service (HPS): lokaler Terminal-Client (HpsClient) mit Zahlung, Pre-Auth, Capture, Refund, Storno, Status, Abbruch, AVT und Diagnose
  • HobexReceipt.fromHps + Karten-Beleg-Rendering (Provider hobexHps); HobexReceipt aus dem hobex_hps-Barrel exportiert

2.0.0 #

  • image plugin update
  • voucher logic implemented

1.1.0 #

  • Added new endpoints

1.0.0 #

  • Initial release of kasseneck_api
0
likes
150
points
1.92k
downloads

Documentation

API reference

Publisher

verified publisherkreiseck.com

Weekly Downloads

RKSV-Registrierkasse für Flutter: signierte Belege, hobex- und Stripe-Kartenzahlung, Bondruck. Austrian cash register by Kreiseck.

Repository (GitHub)
View/report issues

Topics

#rksv #point-of-sale #payments #receipts #austria

License

MIT (license)

Dependencies

flutter, flutter_blue_plus, http, kreiseck_design, my_pos, path_provider, qr, qr_flutter, sumup

More

Packages that depend on kasseneck_api