kasseneck_api 6.13.0
kasseneck_api: ^6.13.0 copied to clipboard
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.isNotFoundbenennt 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 neuKasseneckApi.belegSenden(API-Schluessel-Zugang):fullReceiptId,anund optionalsprache. Welche Kasse gemeint ist, entscheidet die Sitzung bzw. dercashregister-token-- es gibt bewusst keincashregisterIdam Aufruf. Zwilling:sendReceiptEmailin @kreiseck/kasseneck-api 0.13.0. - Neu
Belegmailergebnismitto(normalisierte Adresse),at(ISO in Wiener Zeit) undvia(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
belegMailFehlercodesundistBelegMailFehlercode: die vier stabilen Codes des Backends (adresse_ungueltig,beleg_nicht_gefunden,zu_oft,versand_fehlgeschlagen). Sie kommen unveraendert alsKasseneckApiError.codeheraus -- 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(mitBelegmailergebnis.aus),belegMailFehlercodes,istBelegMailFehlercode,Aufrufe.sendReceiptEmail-- erreichbar auskasse.dartundkasseneck_api.dart. - Die Anheftung
npm_versioninzwillinge.yamlsteht wieder auf dem veroeffentlichten Stand (0.10.0 -> 0.12.0); die Vertragsdateien untertest/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 EndpunktcancelReceiptmit Bezug, Grund (stornogruende), optionalem Teilstorno (Stornoposition) und Anmerkung. LiefertStornoergebnis(Storno-Beleg, Bezug, Restmengen). Fachliche Ablehnungen kommen alsKasseneckApiErrormitcodeausstornoFehlercodes. - 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,stornoFehlercodesundistStornoFehlercodesind jetzt auch auskasseneck_api.darterreichbar.- Die Abkuendigung von
cancelReceipt/createCancelReceiptnenntstornierenals 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.isConclusiveliest 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. Dazu100029: das Terminal storniert laut hobex selbst. -
Neu: "ungewiss" (
HpsCodeEffect.hostUncertain,TransactionResponse.isHostUncertain) fuer100006,100007,100023,100024,100026,100027und100999. Der Host war beteiligt, das Terminal storniert nicht selbst. Die Zwei-9027-Regel greift hier nicht. Ein9027danach heisst nur, dass das Terminal nichts gespeichert hat, nicht, dass nichts belastet ist. Die Klaerung endet nach zwei Abfragen ohne Neues alsunresolved; sagt der Status0, 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 unveraendertes0auf 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 fuer100108). Die Klaerung liest den Status deshalb ueberisConclusiveAsStatus; wer selbst Statusabfragen auswertet, sollte das auch tun. Ohne diese Unterscheidung haette ein gesperrtes Terminal (100022) eine verlorene Zahlung als "nicht belastet" ausgewiesen. -
HpsResult.reasontraegt fuer jeden Ausgang den Grund, mit einem Satz fuer den Bediener (HpsCodeReason.hint). Er wird dort gesetzt, wo entschieden wurde: ein bestaetigter Abbruch istaborted, eine ueber die Zwei-9027-Regel geklaerte Zahlung traegt den Grund ihres eigenen Codes, HTTP409istterminalBusy.HpsResult.isHostUncertainsagt einer spaeteren Nachfrage, dass ein9027dort nicht "nicht belastet" heisst. -
100011 "Not Found"ist keine Aussage, traegt aber nicht die Zwei-9027-Regel: sie ist nur fuer9027gemessen. 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 Textabgelehnt (<code>)woertlich prueft, muss nachziehen. -
TransactionResponse.isUnknownCodeheisst 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).
.pubignoreschliesst jetzttest/integration/credentials.local.json,*.local.json,*.envund.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.
-
QrMassrechnet die Modulgröße, statt sie zu raten (neu, auspackage: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 mitimageWidth(296/504) — die lässt absichtlich Rand, und wer rechnet, ob ein Symbol passt, braucht die echte Kopfbreite. -
Ohne Wahl ändert sich nichts.
QrModulGroesse.autodeckelt 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:ansetKeckReceipt,setBelegLayout,KeckPrinterService.getPaperFromReceipt/getBytesFromReceipt/printReceiptBluetooth,KeckPrinter.printReceipt,KasseneckReceipt.getPrintBytes/printReceiptBluetooth—auto(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.qrAusweichsagt, dass ausgewichen wurde und warum; es reicht bisKeckPrintResult.qrAusweichundKeckPrinterService.letzterQrAusweich. Getrennt vonqrFehler, weil die Handlung eine andere ist:qrFehlerheißt „Beleg ohne QR",qrAusweichheiß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 Parameterbyte0x30des Druckbefehls, das es nicht als Befehl erkennt.QrPrintMode.nativeschickt weiterhin gar keinen Modellbefehl und bleibt damit byteidentisch. WerQrPrintModeerschöpfend auswertet, bekommt einen vierten Fall. -
PrintPaper.addQrCodenimmtsizejetzt 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.qrcodekennen dafür den neuen Schaltermodell1.
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.autoist 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. -
druckmodusistdruckmodusOder(QrPrintMode vorgabe)gewichen. Die Vorgabe ist Pflicht und hat keinen Standardwert: wasautobedeutet, weiss nur der Aufrufer (App: Rasterbild, Browser-Kasse: nativer Befehl). Ein Standardwert an dieser Stelle haette eine der beiden Kassen still umgestellt. -
Wer
KasseQrModuserschoepfend 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) undKasseSettingsGeraet.qrModus, Vorgaberaster. Am Geraet und nicht am Betrieb: welchen Befehl ein Thermodrucker versteht, entscheidet das Modell an dieser einen Kasse. Bestehende Geraete drucken unveraendert weiter. -
KasseQrModusDruck.druckmodusuebersetzt die Einstellung in denQrPrintMode, denKeckPrinterService.getPaperFromReceiptnimmt; beide sind jetzt auspackage:kasseneck_api/printing.dartbenennbar. Ohne diesen Weg liesse sich der gewaehlte Modus zwar speichern, aber nie drucken. -
Vertrag auf npm 0.9.3 nachgezogen (
zwillinge.yaml), Kopie untertest/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.
-
pinOkwird jetzt mit== truegelesen (PrintPaper._gpTom). Vorher stand dort eine Ternary auf dem Rohwert.InquireResult.pinOkistbool?und fehlt bei jeder kontaktlosen Zahlung ohne PIN — dem Normalfall unter 50 €. Der Zugriff warf, und dercatch (_)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. -
formatGpTomAmountantwortet 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,NaNundInfinityebenso.
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 alscreditCardProviderunter 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:CreditCardProvidersamtkartenblockUeberschrift. Wer mit Karte kassiert, benennt den Anbieter am Verkauf und brauchte dafür bisher einen zweiten Import. -
Ein Anbieter ohne
zahlungsart: creditCardwirft einenKasseneckValidationError(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 einecardPaymentIdbei jedem Anbieter außercustomPflicht 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. -
stornierenbekommt bewusst kein solches Argument: den Storno-Beleg baut der Server, undcancelReceiptnimmt nuritems,noteundpaymentMethodentgegen — 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.dartrendert sie durch das Dart-Raster und vergleicht zeichengenau gegengrid32.txtundgrid48.txtdes 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 undsetKeckReceipt. Wo sie sich unterschieden, zeigte derselbe Beleg je nach Oberfläche etwas anderes.KeckPrinterService.getPaperFromReceiptnimmtsetBelegLayout, sobald ein Layout vorliegt. -
setBelegLayoutfolgt jetzt dem QR-Modus. Es rief fest den nativen BefehlGS ( 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.setBelegLayoutist dadurchasync. -
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 mit55, 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 danach9027antwortet. 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 beiunresolved. 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.responsebleibt beiunresolvedweiterhinnull(kein Beleg aus einer Nicht-Aussage);lastResponseist Material fuer Anzeige und Katalog, nie ein Beleg. Bei schluessigem Ausgang ist esnull. -
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 beilistRegisterUsersForDevice. - 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.cancelReceiptundKasseneckApi.createCancelReceipt(@Deprecated). Das ist der alte Storno-Weg übercreateReceiptohne Bezug zum Original — keine Restmengen, kein Schutz vor doppeltem Storno, Gutscheine werden nicht zurückgenommen. Das Backend nimmt ihn weiter an und legtdeprecationin die Antwort. StattdessenRegisterReceiptClient.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): dercodeder Antworthülle, wenn der Endpunkt einen legt — heutecancelReceipt. Nur ein nicht leerer Text zählt; sonstnull. Entscheide am Code, nie am Text.stornoFehlercodesundistStornoFehlercodeinkasse.dart: der Katalog der vierzehn Codes, dieselbe Liste wiefunctions/storno-core.jsund@kreiseck/kasseneck-api.- Storno-Einträge (
KasseneckReceipt.cancellations) könnenpromoAdjustmentCentstragen (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,stilfarbenundmarkenfarbeentfallen. Die vier Farbtabellen der Stile (klar/warm/nacht/kontrast) und die feste Markenkonstante waren handgetippte Zwillinge dessen, waskreiseck_designjetzt als Rollen liefert. - Breaking:
Kassenthema.farbenentfällt, ebenso die fünf Konstruktor-Parameterfarben,marke,markeTief,markeHell,aufMarke— an ihrer Stelle steht der neue Parametermodus(KdMode). WerKassenthemabisher selbst konstruiert hat statt überKassenthema.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 vonkreiseck_designfür den Modus des jeweiligen Stils (klar→light,warm→warm,nacht→dark,kontrast→contrast), 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:
radius14,radiusKachel/radiusKlein10,linie1,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), dazuradiusKachel/radiusKleinvon 12/8 auf 10 undlinievon 1 auf 1,5 in jedem Stil, im Kontraststil zusätzlichradiusvon 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.ausHexverlangt jetztersatz. Der bisherige Vorgabewert war das Panel-Blau (#1B46F5) — ein stilles Detail dieses Pakets, das an jeder Aufrufstelle unbemerkt mitlief. Wer bisher ohneersatzaufgerufen 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, undkreiseck_designselbst verlangt 3.44.
5.2.1 #
-
Behoben: eine zu lange Spalte verlor auf dem Bon still ihren Rest.
EscPosGenerator.rowsplittet zu langen Spalteninhalt korrekt in eine Fortsetzungszeile ab, rief sie dann aber nur auf und verwarf das Ergebnis (row(nextRow);stattbytes += row(nextRow);). Alles, was ueber die Spaltenbreite hinausging, fiel damit ersatzlos weg.Herkunft: der Erzeuger wurde mit 4.0.0 aus
esc_pos_utils1.1.0 uebernommen — dort steht der Fehler.esc_pos_utils_plushatte ihn behoben, und genau diese Fassung war bis 3.3.0 im Einsatz. Beim Loesen vonesc_pos_utils_plusging 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,90wurde3 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".
declinedentstand nur aus den Codes der Positivliste. Jeder andere galt als Wissensluecke und wurdeunresolved— 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") und100108(„Invalid TID"). Alle drei weist das Terminal ab, bevor es eine Karte verlangt —9003gemessen 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
9027beim Pollen einen Aussagewert. Gemessen antwortet eine genehmigte Zahlung mit0(Beleg 408811, dreimal wiederholt), eine abgelehnte mit9027(bei100003zweimal, bei9003einmal). Zwei9027in Folge ergeben deshalbdeclined.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:
discoverHpsTerminalsfindet 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: truebricht 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/terminalsantwortet auf der gemessenen Firmware mit404 "Endpoint not implemented", ebenso/api/status,/api/versionund/api/terminals/{tid}/status. Eine Suche, die nur darauf baut, findet ein voll funktionsfaehiges Terminal nicht. Zweites Merkmal istGET /api/terminals/0/diagnosis— dasselbe, daskasseneck-connectbenutzt.Fuer iOS ab 14 noetig:
NSLocalNetworkUsageDescriptionin derInfo.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/terminalskennt. Auf der gemessenen tut sie das nicht; die TID muss dort bei der Einrichtung eingetragen werden.
5.1.1 #
-
HobexCloudPaymentsist abgekuendigt (@Deprecated). Der Cloud-Weg wird nicht mehr weiterentwickelt; neue Einbindungen nutzenHpsPaymentsmit einem lokalen hobex-HPS-Terminal. Nichts wird entfernt: Bestandsbelege tragenCreditCardProvider.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" uebernullaushobexGetStatus, 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 mit9900, 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, wasCreditCardProvider.customausdrücklich erlaubt. - Behoben:
HpsClientsetzt den dokumentierten Vertrag jetzt durch.transactionIdundoriginalTransactionId(beirefund, 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ß wirftArgumentError, bevor irgendetwas hinausgeht. Breaking: ein Aufrufer, der bisher eine nicht rein numerische Kennung übergeben hat (nur überCreditCardProvider.customerreichbar), bekommt jetzt einen Wurf statt eines stillschweigend angenommenen, aber am Terminal unauffindbaren Vorgangs. - Der eigentliche Fehler liegt tiefer als
9900und ist jetzt behoben:TransactionResponse.isConclusiveist 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 zuunresolved, nie zudeclined. Breaking (stille Bedeutungsänderung ohne Namensänderung):isConclusiveundisNoStatementbleiben als Symbole bestehen, aberisConclusiveliefert für einen Code, der zuvor als schlüssig galt (jeder Code ungleichnull/9027, etwa ein erfundener oder unbekannter Ablehnungscode), jetztfalse. Wer sich auf das alte Verhalten verlassen hat, bekommt für einen nie gemessenen Codeunresolvedstattdeclined— 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 bei9900, aber mit einer REIN NUMERISCHEN, unbekannten Original-Kennung: eine Gutschrift darauf antwortet nach 1,2 Sekunden mit9002— kein Kartenfluss, keine Auszahlung, die Karte wurde nie angefordert. Anders als9027und9900ist 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 zudeclined. Die Kontrollprobe zu9900zugleich: dieselbe Anfrage lieferte mit einer nicht rein numerischen Kennung9900, mit einer numerischen, aber unbekannten Kennung9002— 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, kommt409nach 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:HpsPaymentsliest ein409auf die ERZEUGENDE Anfrage (Zahlung, Gutschrift, der direkte Aufhebungs-Request voncancel) jetzt alsdeclined, ohne Abbruchversuch oder Polling.409ist ein HTTP-Status, keinresponseCode— er entsteht, bevor überhaupt ein Antwortrumpf gelesen wird, und ist deshalb bewusst NICHT Teil vonTransactionResponse.isConclusive, sondern eine eigene Prüfung (HpsHttpException.terminalBusyStatusCode,.isTerminalBusy, ausgewertet inHpsPayments._fromTerminalBusy). Die positive Aussage gilt AUSSCHLIESSLICH für die erzeugende Anfrage: ein409auf einenHpsClient.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 —truefür einen vorhandenen Code, der weder schlüssig noch eine der beiden benannten WissenslückenisNoStatement/isTechnicalErrorist). - 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,refundund der direkte Antwortweg voncancel(HpsPayments). doc/kartenzahlung.mdum die Befunde zu9900,9002und409ergä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) undHobexCloudPayments(pay, Hobex-Cloud) legen die Transaktionskennung fest, bevor der erste Request hinausgeht, und liefern ein dreiwertigesCardPaymentOutcomestatt eines Booleans:approved(Geld ist geflossen),declined(definitiv kein Geld geflossen — Wiederholung gefahrlos) oderunresolved(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 beiunresolved. - 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 mit9027„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 mit0, und dieser Wert bleibt erhalten.9027ist 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:aborteinmalig versuchen →responseCode == '0'ergibt beweisbardeclined(ohne jede Statusabfrage) → jeder andere Code oder ein Transportfehler beim Abbruch führt ins Pollen der Statusabfrage, die nun eine echte Aussage liefert →9027beim Pollen ist kein Ergebnis, sondern ein Grund weiterzumachen → Budget erschöpft ergibtunresolved. Die vorherige Reihenfolge (erst pollen, abbrechen nur wenn der Status „läuft noch" meldet) war wirkungslos: diesen Zustand meldet die Statusabfrage nie. Damit wirdunresolvedzum 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.refundfolgt demselben Weg,cancelnicht: beirefundist 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 beiunresolved. Beicancelist 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:
declinedentsteht ausschließlich aus einer positiven Aussage — einem echten Ergebniscode des Terminals ungleich'0'und ungleich9027, 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 eineTransactionResponsestattString?. Das Terminal meldet ein gescheitertes Abbrechen mit HTTP 200 und100010im Rumpf. Die bisherige Fassung las nurjson['transactionId']und warf nur bei Nicht-2xx — ein gescheiterter Abbruch hätte damit ausgesehen wie ein geglückter, und aus einer echten Belastung wäredeclinedgeworden. Aufrufer lesen die Kennung jetzt überresponse.transactionIdund den Ausgang überresponse.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 iststatejedoch in jeder bisher gesehenen Antwortnull, bei genehmigten, abgebrochenen, unbekannten und aufgehobenen Vorgängen gleichermaßen. Die Bedingung wurde nie wahr; jede Storno-Klärung lief ins Budget und endete alsunresolved, egal was tatsächlich geschah. Gemessen antwortet die Statusabfrage auf die Kennung der Originalzahlung nach einem erfolgreichen Void mit9011„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);9027und alles andere → weiter klären, am Endeunresolved. Der direkte Antwortweg voncancelwird unverändert über denresponseCodedieser Antwort eingeordnet, die die Aufhebung selbst betrifft — derresponseCodeder 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 beiunresolved. Und:9011auf dem direkten Antwortweg voncancelwird nicht mehr alsdeclinedgelesen (das hätte „weiterhin belastet" für einen aufgehobenen Vorgang gemeldet), sondern als nicht schlüssig — was9011dort genau heißt, ist ungemessen, also wird der Zustand der Originalzahlung abgefragt statt geraten. - Cloud-Weg neu im Paket:
KasseneckApi.hobexGetStatusfragt den Stand einer Hobex-Cloud-Transaktion ab und ist die Klärstufe hinterHobexCloudPayments— 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 mitunresolvedstatt den Mitarbeiter das volle Budget warten zu lassen. DazuKasseneckApi.cardTimeout(3 Minuten, fürhobexPay/hobexRefund) getrennt vonreadTimeout(30 s) — die bisher pauschale 30-Sekunden-Frist war für einen Belege-Cache gedacht und meldete am Terminal eine durchgelaufene Zahlung als Fehlschlag.hobexGetStatusselbst bleibt beireadTimeout: 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; ihrjson.decodestand außerdem ungesichert und gab bei HTML im Rumpf eine roheFormatException. - Cloud-Rückerstattung bleibt roh, bewusst.
HobexCloudPaymentsdeckt nurpayab — keinrefund. Eine Rückerstattung über die Hobex-Cloud läuft weiterhin unverändert überKasseneckApi.hobexRefundund bleibt damit ungeklärt: keinCardPaymentOutcome, keine Klärschleife. Bewusst nicht nachgebaut, denn ungeprüft ist, woraufhobexGetStatusmit der Kennung einer Rückerstattung überhaupt antwortet — genau diese Frage hat beim HPS-canceleinen Fehler aufgedeckt (die dort übergebene Kennung ist die der Originalzahlung, derenresponseCodebleibt 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 aufcardTimeout(siehe oben) isthobexRefundtrotzdem: 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.HpsPaymentsnimmt ihn wie bisher entgegen, jetzt zusätzlich auchHobexCloudPayments.HpsEvent,HpsEventKindundHpsObserverwerden dafür zusätzlich auskasseneck_api.dartexportiert (bisher nur aushobex_hps.dart). - Breaking:
HpsClient.cancel()verlangt jetztamount. Das Terminal weist einen Void ohne Betrag mit400 Missing amountab. - Der dreiwertige Ausgang heißt
CardPaymentOutcomeund liegt inlib/src/payments/card_payment_outcome.dartstatt im HPS-Baum, weil er jetzt auch vom Cloud-Weg geteilt wird. Die Werteapproved/declined/unresolvedsind unverändert; er ist der Typ vonHpsResult.outcomeund wird aushobex_hps.dartexportiert. - 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, dieclose()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 ein400vom 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. tidwird im Client normalisiert (führende Null abgestreift) statt in jeder aufrufenden App einzeln.- Beispiel und Bibliotheks-Doku zeigen den sicheren Weg.
example/example.dartdemonstrierte den Kartenverkauf bis zuletzt überHpsClient.payment()ohnetransactionIdund mitif (!res.isApproved) return;— Zeile für Zeile der Vorfallsmechanismus vom 24.08. Jetzt überHpsPayments: Kennung vor dem Aufruf, dreiwertiger Ausgang, undunresolvedausdrücklich anders behandelt alsdeclined. Dasselbe überholte Snippet stand im Bibliotheks-Doc vonhobex_hps.dart, und der Klassenkopf vonTransactionResponselehrte nochresponseCode != "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
200schickt 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.loadLogoriefhttpClient.get(...)ohne jede Frist auf eine Adresse, die der Server in der Belegantwort mitgibt — und zwar inKasseneckReceipt.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/zeroReceiptnie 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 blockiertegetReceiptsfür einen ganzen Zeitraum. Der Abruf hat jetzt eine harte Frist (LogoService.frist, VorgabeLogoService.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;getReceiptsfeuerte deshalb bei N Belegen mit derselben Adresse N gleichzeitige Requests. - Neue öffentliche Symbole:
LogoService.frist,LogoService.standardFrist. - Breaking (stille Verhaltensänderung):
sellReceipt,cancelReceipt,createCancelReceiptundzeroReceiptwarten jetzt bis zu 90 statt 30 Sekunden. Neu:KasseneckApi.signatureTimeout(Vorgabe 90 s), getrennt vonreadTimeout. 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 eineTimeoutExceptionbekommt 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 demapi_key-Weg waren ein Rest. Eine eigene, benannte Frist statt eines höherenreadTimeout, damit eine Belegliste nicht mitwächst: die drei Fristen (readTimeout30 s,signatureTimeout90 s,cardTimeout3 min) benennen jeweils, was wartet. financeWebServicebeachtetreadTimeout.getCashboxStatusundgetSignatureStatusliefen über eine fest verdrahtete halbe Minute; werreadTimeoutim Konstruktor setzte, erreichte sie damit nicht.- Breaking: ein unlesbares Antwortfeld verliert den signierten Beleg nicht mehr.
KasseneckReceipt.create/.fromJsonwiesen ungeprüftedynamic-Werte an nicht nullbare Felder zu (qr,sig,receiptId,timeStamp,taxnr,footer1/2,is_small_business…). Ein einziges fehlendes Feld erzeugte einen rohenTypeErrornach der Signatur — für den Aufrufer nicht von „Verkauf fehlgeschlagen" zu unterscheiden, und mit der verworfenen Antwort ging diereceiptIdverloren: 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:KasseneckReceiptFormatErrormitfield,receiptIdundcauseType, 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 demgetReceipt(receiptId)bzw.RegisterReceiptClient.holen(receiptId)den Beleg nachholt. Dieselbe Regel wie am Kartenterminal: die Kennung steht vor allem anderen fest.causeTypeträgt nur den Typ der zugrunde liegenden Ausnahme, nie ihre Meldung — eineFormatExceptionfü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 —nullund''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
companyergab bis hierher stumm einen Pflichtbeleg mit leerer erster Zeile. Zu werfen kostet den bereits signierten Beleg, der in der Kette steht — deshalb neuKasseneckReceipt.fehlendePflichtangaben(Feldnamen, nie Werte) undpflichtangabenVollstaendig: 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 demapi_key-Weg sind weg (createReceipt,listMyTipRecipients,getReceipt,getReportV2,getCashboxStatus,getSignatureStatus,createPaymentLinkStripe,stripeCaptureIntent,hobexPay,hobexRefund, dazugetFirstReceiptDate). Sie liefen bei einem200mit Array, Skalar odernullin einen rohenTypeError, bei einem200mit HTML im Rumpf (Captive Portal, CDN-Fehlerseite) in eine roheFormatException— beicreateReceiptmitten im Verkauf. Beides wirft jetztKasseneckHttpError— denselben Typ mit denselbenreason-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 blankesExceptionhä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. getReceiptsunterscheidet „im Zeitraum nichts verkauft" von „Antwort kaputt".data.metadataunddata.receiptswurden hart gecastet — einNoSuchMethodErrorbzw.TypeErrorals Hülle um einen sorgfältig abgesicherten Per-Beleg-try/catchherum. JetztKasseneckValidationError(kind: 'response'), wortgleich mit derselben Prüfung inbelege.dartauf dem Kassen-Weg; ebenso die fehlende Empfängerliste inlistTipRecipients, die bis hierher ebenfalls nur ein blankesExceptionwar.- Die Zählzeile im Protokoll von
getReceiptslief vor der Prüfung. EindebugPrintlasdata['receipts']mit einem String-Index, bevordataüberhaupt als Objekt geprüft war: beidata: [...]oderdata: "text"griff der Index in eine Liste bzw. einen Text, beireceipts: {…}scheiterte der Cast — jedes Mal genau der roheTypeError, 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.stornierenprüftdata.cancellationOfunddata.remainingvor dem Einlesen des Belegs und warf dabeiKasseneckValidationErrorohne 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.KasseneckValidationErrorträgt jetzt ein optionalesreceiptId, und die drei Prüfungen dieses Abschnitts geben es mit. - Neue öffentliche Symbole:
KasseneckReceipt.fehlendePflichtangaben,KasseneckReceipt.pflichtangabenVollstaendig,KasseneckReceiptFormatError(auskasseneck_api.dartundkasse.dart), dazuKasseneckHttpErrorundKasseneckValidationErrorauskasseneck_api.dart— wer die Antwortfehler desapi_key-Wegs fangen soll, muss ihre Typen auch benennen können, ohneregister.dartzu importieren.KasseneckValidationError.receiptId.KasseneckReceipt.fromMetadatanimmt den Belegteil jetzt alsObject?entgegen statt alsMap<String, dynamic>und prüft ihn selbst. - Zeitablauf und Verbindungsfehler sind auf dem Kassen-Weg nicht mehr derselbe Fehler.
RegisterTransport.rufenundRegisterClient._rufenfingen miton Objectalles ab und meldetenKasseneckHttpError(name, 0, 'network')— der Ursprungsfehler wurde restlos verworfen. ÜbercreateReceiptsind 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:reasonistKasseneckHttpError.zeitablauf('timeout') oderKasseneckHttpError.netz('network'), undcauseTypeträ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:httptrennt „Verbindung kam nie zustande" nicht von „Verbindung riss, nachdem die Anfrage draußen war"; eindeclinedaus 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
jsonEncodedes Rumpfes stand im selbentrywie der Request; ein nicht serialisierbarer Parameter kam alsnetworkheraus — also als Fehler, nach dem ein Beleg entstanden sein könnte, obwohl nichts hinausgegangen war. - Ein kaputtes
datageht nicht mehr als „nichts eingestellt" durch.RegisterTransport.rufengab beistatus: successund einemdata, 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/geraetSpeichernmeldeten „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: fehlendesdatableibt ein leeres Objekt (nicht jeder Aufruf hat eine Nutzlast), eindata, das da ist und kein Objekt ist, wirftKasseneckHttpError(reason: 'data-not-object'), und ein Schreibaufruf ohnedata.betrieb/data.geraetwirftKasseneckValidationError(kind: 'response'). Dieselbe Grenze, diebelege.dartundkasseneck_api.dartbei 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.setKeckReceiptrechnete Netto und MwSt getrennt alsdoubleund rundete beide einzeln, undustCentsam Kassieren-Schirm rundete nachbrutto * 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 überlib/src/vat_math.dart: einmal runden, die MwSt ist die Differenz. - Breaking:
ustCentsentfällt,ustSummehat einen zweiten Parameter. An seine Stelle trittustCentsAusBrutto(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.setKeckReceiptriefdecodePng(receipt.logo!)ungesichert auf, und zwar als erste Handlung —LogoServiceprü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 inlib/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 durchlatin1.encode, das bei jedem Zeichen über 0xFF wirft;BelegQr.dataist 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 dasqr-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.
pHstand fest auf0x00undpLwurde modulo 256 abgeschnitten; bei 300 Byte Nutzlast las der Drucker 44 Byte Daten. Die Länge geht jetzt alspL/pHhinaus; 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 kanndata: ""liefern) gilt dabei als Ausfall statt als QR ohne Inhalt. - Der Ausfall erreicht den Aufrufer auf jedem Druckweg.
PrintPaper.qrFehlerallein genügte nicht:getBytesFromReceiptundgetMyPosPaperFromReceiptbauen dasPrintPaperintern und geben nurbytesbzw.myPosPaperheraus — das Objekt mit dem Ausfall wurde verworfen, und damit war er fürprintReceiptMypos(),printReceiptWifi(),printReceiptBluetooth()undKasseneckReceipt.getPrintBytes()unerreichbar. Neu:KeckPrinterService.letzterQrFehler(nur lesbar) trägt den Ausfall des zuletzt gesetzten Belegs und wird bei jedem Bau übergetBytesFromReceipt/getMyPosPaperFromReceiptgesetzt, auch aufnull— er bleibt also nie von einem früheren Beleg stehen.getPaperFromReceiptschreibt ihn ausdrücklich nicht, und damit auchKeckPrinter.printReceiptnicht: sonst überschriebe ein Druck auf Drucker A genau das Signal, das ein gleichzeitig laufenderprintReceiptMyposauf Terminal B gerade auslesen will. Ein Feld statt erweiterter Rückgabetypen, weil keiner der drei Rückgabewerte (Bytes, das fremdeMyPosPaper, die fremdePrintResponse) 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 neueKeckPrinterService.getPaperFromReceipt(liefert das fertigePrintPaper) oder den transportbasiertenKeckPrinter, dessenKeckPrintResultden 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),PrintPaperausprinting.dart. - Behoben: der Kachel-Artikel las seinen Preis abgeschnitten statt gerundet.
KasseArtikel.ausnahm(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 —, ist19.99 * 100dort exakt1998.9999999999998: der Artikel verkaufte sich dauerhaft einen Cent zu billig, und zwar auf einen signierten Beleg. Ebenso8,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 durchsubstring(0, len): ab 100.000,00 € fiel die letzte Stelle des Betrags weg („100000,00" wurde zu „100000,0"). Die Breite kommt jetzt auspaperSize.defaultCharCount, und aufgefüllt wird nur, nie gekürzt. Eine zu breite Zeile ist unschön; ein gekürzter Betrag ist falsch. - Breaking:
betragAusTextweist Beträge überhoechstbetragCent(100.000,00 €) jetzt mitnullab. 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 Ziffernnull, statt entgegen ihrer Zusage eineFormatExceptionausint.parsezu 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 Kennung — widerlegt. 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 Kennung — gemessen: in beiden Fällen9027, ebenso bei laufendem Kartenfluss und bei abgebrochenem Vorgang. Nur eine genehmigte Zahlung antwortet mit0. Siehe den Klärweg oben. - Scheitert
abort()zuverlässig, sobald die Karte aufliegt — präzisiert: die Grenze ist nicht der Kartenkontakt, sondern der Abschluss.abortgelingt (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 wird — weiterhin 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:
abortwirkt auf einer laufenden Rückerstattung genauso. Der Abbruch antwortet auch dort mitresponseCode "0", und die Gutschrift endet daraufhin mit100002„Aborted". Damit ist der Klärweg fürrefundgemessen 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 Kartenflussesnullbleibt und als Unterscheidungshilfe damit unbrauchbar ist; und/api/terminalsantwortet auf dieser Firmware mit404 Endpoint not implemented— die in 4.8.0 ergänzteterminals()-Methode läuft dort ins Leere. - Die Storno-Messung hat den Pflichtparameter aus 4.7.0 bestätigt: ein Void ohne
amountwird mit400 Missing amountabgewiesen. Die Aufhebung selbst bekommt eine eigene neuetransactionId; die ancancel()übergebene Kennung bleibt die der Originalzahlung. - Weiterhin unbestätigt: die Erreichbarkeit von
hobexGetStatusunterapi.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 wederabort()noch9027und bleibt unverändert.
4.24.0 #
- Trinkgeld-Empfänger abrufen (
listMyTipRecipients):tipEmpfaenger()am Kassen-Client und derselbe Aufruf auf demapi_key-Weg liefern die Personen, denen sich Trinkgeld zuweisen lässt — je PersonregisterUserId,nameundowner— dieselbe Gestalt wie der Empfänger am Beleg-Item. - Es ist dieselbe Menge, die der Verkauf akzeptiert: Wer in der Liste steht, wird von
createReceiptnicht zurückgewiesen. Entschieden wird das im Backend an einer Stelle, die Liste und Annahme gemeinsam benutzen. KeckTipPerson.mit(cents:)macht aus einer Person einenKeckTipRecipient. Wer die Liste benutzt, kann keine Kennung erwischen, die der Server ablehnt.ownerentscheidet die Bezeichnung am Beleg: „Trinkgeld" (Inhaber — Entgelt des Betriebs) gegen „Trinkgeld Personal" (durchlaufender Posten). Deshalb reist das Flag mit — als nicht nullbaresbool, denn ein fehlendes Flag gäbe es bei dieser Frage nicht.- Ist ein Kassen-Benutzer ohne das Recht
tipAssignangemeldet, steht nur er selbst in der Liste. - Vertrag auf
@kreiseck/kasseneck-api0.6.46 angehoben.
4.23.0 #
- Die Hobex-Transaktionskennung entsteht jetzt nach demselben Verfahren wie im JS-Zwilling (
@kreiseck/kasseneck-api,newHobexTransactionIdinsrc/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.dartund im npm-Paket intest/payments.test.tswortgleich 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 ausDateTime.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 einenRangeError. 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 inzwillinge.yaml). Die Vertragsdateien werden mittool/zwillinge.sh ziehengeholt 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 inzwillinge.yamlbenannt 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: offenmit Issue-Nummer inzwillinge.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.yamlzu 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. belegAusgabesteht standardmäßig auffragen: die Fertig-Seite bietet QR und Bon an, wie Backend und Browser-Kasse es längst tun.- Die Aufrufnamen stehen als Konstanten in
Aufrufean einer Stelle. - Zwei Standardwerte weichen weiterhin ab und stehen in
zwillinge.yamlunterwert_ausnahmen— unter derselben Strenge wie die Hauptliste: die Tastenbelegungtasten(Issue #25) undterminalPort, 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 ancreateReceiptdurch; 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 dasinhaber-Flag des Kassen-Benutzers — der Aufrufer schickt für beide dasselbe. KeckTip.fuer(registerUserId, cents: …)für den Regelfall,KeckTip.euro(…)mit einmaliger Rundung,KeckTipRecipientfü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: nurstandardundtraining. 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/tipsowie getrenntstaffTipCents(durchlaufender Posten — bei Karte der Betrag, der weitergegeben werden muss) undownerTipCents(Entgelt, insumCentsenthalten). An der Position:KasseneckItem.isOwnerTip,tipRecipientId,tipRecipientName. Gerechnet wird aus den Positionen, nicht aus dem abgeleitetentipCentsdes 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
tipgeht kein Feld hinaus; der Aufruf ist unverändert.
4.19.0 #
KasseneckItem: neue optionale Felderkind('tip'/'discount'),recipient,paymentMethod,articleId— Zwilling vonReceiptItemim JS-Paket 0.6.44. Die Kennzeichnungen reisen durchtoJson/fromJsonundnegative(Storno-Spiegelung); Zeilen ohne bleiben schlank.verteileRabattkennzeichnet seine Zeilen alskind: 'discount'— der Bon fasst sie zu einer Summenzeile mit Zwischensumme zusammen, der Bericht führt sie als „Rabatte“.articleIdan 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):RegisterReceiptClientmitverkaufen(Normalbeleg samt Belegkopf in einer Antwort),auflisten(Zusammenfassungen mit Summe in ganzen Cent, Bediener, Storno-Stand),holenundstornieren(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 Kopfzeileregister-session, Kasse als Parameter, eigene Frist je Aufruf).RegisterSessionClientlaeuft 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 vonwarenkorb.tsundkassieren.tsder 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 jetzthttps://kasse.kasseneck.at/api.
4.15.0 #
- Kassen-Einstellungen (
package:kasseneck_api/kasse.dart):KasseSettingsmit allen 38 Betriebs- und 20 Gerätefeldern als Zwilling vonkasse/settings.tsundkasse-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. DazukartenAktiv(Karte nur mit eingerichtetem Anbieter) undaktiveSaetze(feste Reihenfolge für den Bildschirm). listRegisterUsersForDeviceliefertsettingsjetzt alsKasseSettingsstatt als Rohdaten.
4.14.0 #
RegisterClient.sitzung(...)liefert denRegisterSessionClientmit 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):RegisterSessionClientmitrenewRegisterSession(liefert den neuen Ablauf) undendRegisterSession. 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 vonregister/pairing.tsim JS-Paket:RegisterClientmitpairRegisterDevice(achtstelliger Code aus dem Panel → dauerhafter Geräte-Ausweis),listRegisterUsersForDevice(Benutzer, PIN-Regel, Anmeldemodus, Standortsperre),registerUserLogin/registerPinLogin(Sitzung: Custom Token +sessionId) undunpairRegisterDevice. Diese Aufrufe laufen ohne Anmeldung — der Code bzw. das Gerätegeheimnis ist der Nachweis; sie stehen deshalb nebenKasseneckApiund nicht darin. - Rechte werden gelesen wie im Backend: Schalter als ja/nein,
cancelScope/receiptsScopeals Reichweite (none|own|all), Altbestand ohne Reichweite migriert (cancelentscheidet, 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 vonrenderReceiptGridim 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 gegengrid32.txt/grid48.txtdes JS-Pakets. PrintPaper.setBelegLayoutdruckt jetzt genau diese Rasterzeilen (keine eigene Spaltenrechnung mehr, keinESC $) — dieselben Zeilen wie Browser-Kasse, Labor und Beleg-PDF. Verhalten vonsetKeckReceiptunverä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, ausgetReceipt→layout), dazutestKasse,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/belegeund werden per Prüfsumme gegen dessen Manifest gehalten. - Empfehlung:
layout != null→ Zeilenmodell zeichnen/drucken; sonst wie bisherKeckReceiptWidget/setKeckReceipt(Altbelege, altes Backend).
4.9.0 #
KeckReceiptWidget.qrCovered(Vorgabefalse, rein additiv): der RKSV-QR wird zunächst weichgezeichnet und nicht scannbar gezeigt (mit HinweisqrCoveredText, 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.truebei HTTP 200 (bereit),falsebei 503 (nicht betriebsbereit); dieser Endpoint hat keinen Response-Body (im Gegensatz zum feldreichendiagnosis()).terminals()→GET /api/terminals: Liste der konfigurierten Terminals als neues ModellTerminalInfo(tid, company/merchantName, Beleg-header-Zeilen,terminalType,active, …). Alle Felder optional, da Spec-Tabelle und Beispiel-Response divergieren; unmodellierte Keys bleiben überTerminalInfo.rawerreichbar.- Intern wurde das Senden in
_send()extrahiert (frische-Verbindung-Logik aus 4.7.0 unverändert), damitterminalStatus()den 503-Status als Wert statt als Ausnahme behandeln kann.
- Bewusst NICHT enthalten: Beleg-Download (
/api/transaction/download) und die Profil-FlagscanVoid/canRefundsind reine Cloud-Endpoints (online.hobex.at, JWT-Auth) und gehören nicht in den authlosen lokalen HPS-Client. EinresponseCode-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:
HpsClienthielt 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 jetztcancel({required transactionId, required amount, currency, language, technicalCancel})— sendetamount/currency(+ optionallanguage) als Query-Parameter (wie in der hobex-Postman-Collection; die REST-PDF v1.13 listet den Parameter nicht, die Firmware verlangt ihn). Breaking:amountist jetzt Pflicht.
- Verbindungs-Wiederverwendung:
4.6.0 #
- Zahlungsart-Zeile auf jedem Beleg (Druck +
KeckReceiptWidget): direkt unter dem Gesamtbetrag stehtZahlungsart: 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.ViennaTimerechnet 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.timeStampist jetzt immer ein echter Zeitpunkt (UTC): Server-Timestamps werden beim Parsen als Wiener Wanduhrzeit interpretiert (ViennaTime.parseServerTimeStamp);toReceiptJsonserialisiert UTC mitZ(Roundtrip-kompatibel, alte naive Strings werden weiterhin korrekt gelesen).readableTimezeigt 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 anonline-BelegecreditCardProvider: 'stripe',cardPaymentId(PaymentIntent-ID) undcardPaymentDataan; 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 viaViennaTime), Abrechnungstext und PaymentIntent-Referenz. Beide Renderer speisen sich aus einer gemeinsamenstripeReceiptLines()— Druck und Widget können nicht auseinanderlaufen (abgesichert durchstripe_render_consistency_test.dart). Fehlende Felder lassen ihre Zeile entfallen; kaputte Daten brechen den Druck nicht ab.
4.4.0 #
- Unified
KeckPrinterwith aPrinterTransportabstraction. 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) andBluetoothTransport(its ownBluetoothDevice, no shared global device) — and is USB-ready via the publicPrinterTransportinterface. Convenience factoriesKeckPrinter.wifi(...)/KeckPrinter.bluetooth(...), plus DI-friendlyKeckPrinter(transport). High-level opsprintReceipt/printText/printQr/printBarcode/cut/openDrawer/feed/printJob/printRawByteseach return aKeckPrintResult(never throw). All exported frompackage:kasseneck_api/printing.dart. Backward compatible: the staticKeckPrinterServiceAPI 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 fluentCustomPrintJob.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 —{Bis prepended unless the data already starts a code-set sequence), plus height (GS h), width (GS w) and HRI position (GS H) options.BarcodeTypeandBarcodeHriare exported frompackage: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 previousQrPainter → PNG encode/decodepath, 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
sizeparameter (raw bytes are already rendered), renamed the result typePrintResult→KeckPrintResult(avoids clashing with app-levelPrintResulttypes), documented thatsuccessmeans 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 frompackage: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 viadart:ui. The runtime no longer depends onesc_pos_utils_plus, andimageis a dev-only dependency — apps that use this package are no longer version-locked toimageand 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_plusrasterizer). 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 helpersprintText,printQr,cut,openDrawerandfeed. ACustomPrintJobbatch builder (text/qr/cut/drawer/feed/raw, fluent) accumulates several commands into a single byte stream printed in one send viaprintJob(preferred for Bluetooth). The vendoredEscPosGeneratorand the required types (PosStyles,PosAlign,PosCutMode,PosDrawer,QRSize,CapabilityProfile, …) are exported from the newpackage:kasseneck_api/printing.dartbarrel so integrators can build bytes themselves and send them withprintRawBytes. - Branded API base URL: the client now talks to
https://api.kasseneck.at/v1instead ofeurope-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.paperSizeis now typedEscPaperSize(internal) instead ofPaperSizefrom esc_pos_utils.
3.3.0 #
- Receipt download links now use the branded path-based URL
https://beleg.kasseneck.at/<token>instead ofhttps://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:cardPaymentDataamounts arrive as integer cents and are now formatted as such (older stored receipts with euro doubles keep working); applies to thermal print andKeckReceiptWidget - GP Tom transaction type is recognized via both
transactionTypeand the plugin'stransacitonTypekey;Refund(type 3) is now labelled
3.2.0 #
- Kreiseck branding on receipts: when the backend metadata flag
kreiseck_logois set (Firestoreusers/{uid}.branding.kreiseck_logo), the receipt ends with "powered by" and the Kreiseck logo — on thermal prints (85 % paper width), inKeckReceiptWidgetand 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, QReyeStyle/dataModuleStyle), removed redundant imports; FinanzOnline status enum names are intentionally kept verbatim (they must match therkdbMessagevalues) - CI runs
flutter analyzein 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
KasseneckApiandLogoServiceaccept an injectablehttp.Client(useful for testing/mocking; default behaviour unchanged)financeWebServicerequests now also time out after 30 s- Continuous integration: analyze + tests run on every push
3.0.1 #
getReceiptsno 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)orKasseneckItem.euro(singlePrice: 19.99)KeckVoucher(value: 5.0)→KeckVoucher(valueCents: 500)orKeckVoucher.euro(value: 5.0)KeckInvoiceItem(singlePrice: …)→priceCents/KeckInvoiceItem.euro(…)- Reading:
item.singlePrice,voucher.value,receipt.sum/subSumstill exist as euro views; for arithmetic usepriceCents/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) onprintReceiptBluetooth/getPrintBytesto pick the command your printer supports (replacesqrAsImage)
2.1.2 #
- Cleaner static analysis (0 warnings):
KeckVoucher.valueis now nullable; removed dead null-aware code and unused SumUp leftovers - Loosened the
my_posversion 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