KassenSichV-Guide — Kassensicherungsverordnung mit Lyx
Dieser Guide erklärt den praktischen Einsatz der kassensichv-Bibliothek für die gesetzeskonforme TSE-Anbindung nach KassenSichV, BSI TR-03153 und DSFinV-K 2.3.
→ kassensichv (Unit-Referenz) · kassensichv.manager · Mock · REST · USB
Rechtliche Grundlagen
Die Kassensicherungsverordnung (KassenSichV) verpflichtet alle Kassensysteme in Deutschland seit dem 1. Januar 2020, jeden Kassenvorgang mit einer zertifizierten TSE (Technische Sicherheitseinrichtung) zu signieren. Jeder Bon muss diese Pflichtangaben enthalten:
- Seriennummer der zertifizierten TSE
- Fortlaufender Signaturzähler
- Start- und Endzeitpunkt laut TSE (UTC)
- Kryptografischer Signaturwert (ECDSA/SHA-256, Base64)
- QR-Code-String nach BSI TR-03153 Anhang A
Der QR-Code-String hat exakt dieses Format:
V0;{TSE-Seriennummer};{StartUTC};{EndUTC};{Signaturzähler};{AnzahlTransaktionen};{Signaturwert}
Die kassensichv-Bibliothek erzeugt diesen String automatisch in SigErgebnis.qrCode — kein manuelles Formatieren nötig.
Provider wählen
Das zentrale Konzept der Bibliothek: Die gesamte Kassensoftware interagiert nur mit dem TseManager. Welche TSE physisch angebunden ist, bestimmt ausschließlich der Provider beim Start:
| Provider | Unit | Typischer Einsatz |
|---|---|---|
TseMockNew() | kassensichv.mock | Tests, CI/CD, Bondruckvorschau — keine Hardware nötig |
TseRestNew(cfg) | kassensichv.rest | Cloud-TSE: Fiskaly, Deutsche Fiskal — billiger Einstieg |
TseFileNew(cfg) | kassensichv.file | USB-TSE: Swissbit, Epson — stationäre Kasse mit eigener Hardware |
Wechsel zwischen Providern: Nur die TseXxxNew-Zeile ändern. Der gesamte Beleg-Code bleibt identisch — das ist der Hauptvorteil der Dependency-Inversion-Architektur.
Entwicklung: Mock-Provider
Während der Entwicklung und in CI/CD-Pipelines immer den Mock-Provider verwenden. Er benötigt keine Hardware, keine Netzwerkverbindung und keine API-Keys.
import kassensichv.types;
import kassensichv.mock;
import kassensichv.manager;
fn main(): int64 {
var mgr: int64 := TseMockNew();
var beleg: BelegDaten;
beleg.prozessTyp := PROZESSTYP_KASSENBELEG;
beleg.kassenNr := "KASSE-001"c;
beleg.prozessDaten := "Kaffee;2.50_0.00_0.00_0.00_0.00"c;
beleg.umsatz := 250; // Eurocent (kein float — Rundungsfehler!)
var sig: int64 := TseProcessBeleg(mgr, addr beleg);
var s: SigErgebnis := (sig as *SigErgebnis)^;
// Mock-Ausgabe:
// V0;MOCK-TSE-0000000000000001;2026-06-12T09:30:00Z;2026-06-12T09:30:01Z;1;1;base64...
PrintLn(s.qrCode as pchar);
SigErgebnisFree(sig);
TseManagerFree(mgr);
return 0;
}
Fehlersimulation
Der Mock kann alle Fehlerzustände simulieren — ohne echte TSE-Hardware:
// Verbindungsabbruch simulieren (code=503)
TseMockSetSimulateError(mgr, 1);
var sig: int64 := TseProcessBeleg(mgr, addr beleg);
var s: SigErgebnis := (sig as *SigErgebnis)^;
// s.success == 0, s.errorMsg enthält "Simulierter Verbindungsfehler"
SigErgebnisFree(sig);
TseMockSetSimulateError(mgr, 0);
// Timeout simulieren (code=408)
TseMockSetSimulateTimeout(mgr, 1);
// ... gleiche Struktur ...
TseMockSetSimulateTimeout(mgr, 0);
Produktion: Cloud-TSE (REST)
Für Cloud-TSE-Anbieter (Fiskaly, Deutsche Fiskal). Vorteil: Keine Hardware, zentrale Verwaltung, für mobile Kassen und Online-Shops geeignet.
import kassensichv.types;
import kassensichv.rest;
import kassensichv.manager;
fn ManagerFuerFiskaly(): int64 {
// Konfiguration als JSON-String (in Produktion aus env/config laden)
var cfg: pchar := concat(
"{\"api_url\":\"https://kassensichv.io/api/v1\","c,
"\"api_key\":\""c, EnvGet("FISKALY_API_KEY"c), "\","c,
"\"client_id\":\""c, EnvGet("KASSEN_ID"c), "\"}"c
);
return TseRestNew(cfg as int64);
}
Empfohlene Umgebungsvariablen:
| Variable | Bedeutung |
|---|---|
FISKALY_API_KEY | Bearer-Token (nie im Code hardcoden!) |
KASSEN_ID | Kassen-ID beim Cloud-Anbieter |
Der REST-Provider wiederholt Requests automatisch bei HTTP 503/504 (bis zu 3 Versuche, exponentielles Backoff: 1s → 2s → 4s). TLS 1.2 wird erzwungen.
Produktion: USB-TSE (Dateiprotokoll)
Für stationäre Kassen mit USB-TSE-Stick (Swissbit, Epson). Der Stick muss als Dateisystem gemountet sein.
import kassensichv.types;
import kassensichv.file;
import kassensichv.manager;
fn ManagerFuerSwissbit(mountPunkt: pchar): int64 {
// "{\"base_path\":\"/mnt/tse\",\"timeout_ms\":5000}"
var cfg: pchar := concat(
"{\"base_path\":\""c, mountPunkt, "\","c,
"\"timeout_ms\":5000}"c
);
return TseFileNew(cfg as int64);
}
Vorbereitung unter Linux:
sudo mount /dev/sdb1 /mnt/tse
Oder dauerhaft via /etc/fstab:
UUID=xxxx-yyyy /mnt/tse vfat auto,user,rw 0 0
Workflow 1: Einfacher Kassenbon
Der Standardfall für eine einzelne Transaktion (Bezahlung an der Kasse):
import kassensichv.types;
import kassensichv.mock;
import kassensichv.manager;
fn EinfacherKassenbon(mgr: int64): void {
var beleg: BelegDaten;
beleg.prozessTyp := PROZESSTYP_KASSENBELEG;
beleg.kassenNr := "KASSE-001"c;
beleg.prozessDaten := "Laptop;999.00_0.00_0.00_0.00_0.00"c;
beleg.umsatz := 99900; // 999,00 EUR in Cent
var sig: int64 := TseProcessBeleg(mgr, addr beleg);
var s: SigErgebnis := (sig as *SigErgebnis)^;
if s.success == 0 then {
Print("[FEHLER] "c); PrintLn(s.errorMsg as pchar);
SigErgebnisFree(sig);
return;
}
// Pflichtfelder auf Bon ausgeben
Print("TSE-Seriennummer: "c); PrintLn(s.tseSerial as pchar);
Print("Signaturzähler: "c); PrintLn(IntToStr(s.sigZaehler)c);
Print("Signaturwert: "c); PrintLn(s.sigWert as pchar);
Print("QR-Code: "c); PrintLn(s.qrCode as pchar);
SigErgebnisFree(sig);
}
Workflow 2: Mehrstufige Transaktion (Tischbewirtung)
Für Restaurants, Servicebetriebe und andere Vorgänge mit Zwischenständen:
fn Tischbestellung(mgr: int64, tischNr: int64): void {
var beleg: BelegDaten;
beleg.prozessTyp := PROZESSTYP_KASSENBELEG;
beleg.kassenNr := "KASSE-001"c;
// Transaktion öffnen — TSE vergiBt Zähler + Startzeitpunkt
var transId: int64 := TseOpenBeleg(mgr, addr beleg);
// Vorspeise geliefert (Zwischenstand, optional)
beleg.prozessDaten := "Suppe;5.50_0.00_0.00_0.00_0.00"c;
beleg.umsatz := 550;
var zwi: int64 := TseUpdateBeleg(mgr, transId, addr beleg);
SigErgebnisFree(zwi);
// Hauptgang hinzu
beleg.prozessDaten := "Suppe+Schnitzel;22.50_0.00_0.00_0.00_0.00"c;
beleg.umsatz := 2250;
var zwi2: int64 := TseUpdateBeleg(mgr, transId, addr beleg);
SigErgebnisFree(zwi2);
// Rechnung — Transaktion abschließen
beleg.prozessDaten := "Suppe+Schnitzel+Dessert;30.00_0.00_0.00_0.00_0.00"c;
beleg.umsatz := 3000;
var sig: int64 := TseCloseBeleg(mgr, transId, addr beleg);
var s: SigErgebnis := (sig as *SigErgebnis)^;
if s.success == 1 then {
PrintLn(s.qrCode as pchar); // QR-Code auf Bon
} else {
// COMPLIANCE-WARNUNG: Transaktion möglicherweise offen!
Print("[COMPLIANCE] "c); PrintLn(s.errorMsg as pchar);
}
SigErgebnisFree(sig);
free(transId);
}
Workflow 3: Stornierung
Ein Stornobon ist ein eigenständiger Kassenbon mit dem Prozesstyp PROZESSTYP_STORNO. Er wird genauso signiert wie ein normaler Bon — nur der Prozesstyp und die Belegdaten unterscheiden sich:
fn Stornierung(mgr: int64, originalBetrag: int64): void {
var beleg: BelegDaten;
beleg.prozessTyp := PROZESSTYP_STORNO;
beleg.kassenNr := "KASSE-001"c;
beleg.prozessDaten := "Storno Laptop;-999.00_0.00_0.00_0.00_0.00"c;
beleg.umsatz := -99900; // negativer Betrag
var sig: int64 := TseProcessBeleg(mgr, addr beleg);
var s: SigErgebnis := (sig as *SigErgebnis)^;
if s.success == 1 then {
PrintLn(s.qrCode as pchar);
}
SigErgebnisFree(sig);
}
Workflow 4: Trainingsbetrieb
Testkäufe und Mitarbeiterschulungen müssen mit dem Prozesstyp PROZESSTYP_TRAINING signiert werden. Diese Vorgänge sind steuerlich nicht relevant, aber trotzdem TSE-pflichtig:
var beleg: BelegDaten;
beleg.prozessTyp := PROZESSTYP_TRAINING;
beleg.kassenNr := "KASSE-001"c;
beleg.prozessDaten := "Training;0.00_0.00_0.00_0.00_0.00"c;
beleg.umsatz := 0;
var sig: int64 := TseProcessBeleg(mgr, addr beleg);
// ... Bon als "TRAINING" markieren, nicht als echter Kassenbon
Workflow 5: DSFinV-K-Export (Finanzamt-Prüfung)
Finanzämter können den Export der TSE-Audit-Logs anfordern. Die Bibliothek erzeugt:
{path}/tse_export.tar— TSE-intern signiertes Archiv (nicht verändern!){path}/index.json— Exportmetadaten nach DSFinV-K 2.3
fn FinanzamtExport(mgr: int64): int64 {
var exportPfad: pchar := "/var/kassensichv/export"c;
var ok: int64 := TseExportAuditData(mgr, exportPfad as int64, "KASSE-001"c);
if ok == 0 then {
PrintLn("Export fehlgeschlagen — Verzeichnis nicht beschreibbar?"c);
return 0;
}
// Ergebnis: /var/kassensichv/export/tse_export.tar + index.json
PrintLn("DSFinV-K-Export erfolgreich"c);
return 1;
}
index.json Inhalt:
{
"dsfinvk_version": "2.3",
"tse_serial": "SWB-0123456789ABCDEF",
"kasse_id": "KASSE-001",
"export_timestamp": "2026-06-12T09:00:00Z"
}
Fehlerbehandlung
Normale Fehler
Alle Fehler werden über SigErgebnis.success und SigErgebnis.errorMsg zurückgegeben:
var sig: int64 := TseProcessBeleg(mgr, addr beleg);
var s: SigErgebnis := (sig as *SigErgebnis)^;
if s.success == 0 then {
Print("TSE-Fehler: "c);
PrintLn(s.errorMsg as pchar);
SigErgebnisFree(sig);
return 0;
}
// Bon drucken ...
SigErgebnisFree(sig);
Compliance-kritisch: Offene Transaktionen
Wenn TseCloseBeleg mit success=0 zurückkommt, ist die TSE-Transaktion möglicherweise noch offen. Das ist ein Compliance-Problem — die Kassensoftware muss reagieren:
var sig: int64 := TseCloseBeleg(mgr, transId, addr beleg);
var s: SigErgebnis := (sig as *SigErgebnis)^;
if s.success == 0 then {
// PFLICHT: Vorfall protokollieren
Print("[COMPLIANCE] Offene TSE-Transaktion — TransId: "c);
PrintLn(transId as pchar);
Print("[COMPLIANCE] Fehler: "c);
PrintLn(s.errorMsg as pchar);
// TSE-Status prüfen
var status: int64 := TseGetStatus(mgr);
Print("[COMPLIANCE] TSE-Status: "c);
PrintLn(status as pchar);
free(status);
// Bon NICHT drucken bis Situation geklärt
SigErgebnisFree(sig);
free(transId);
return 0;
}
Faustregel: Nie einen Kassenbon ausgeben, wenn success=0. Die Kassensoftware muss die offene Transaktion manuell abschließen oder beim TSE-Anbieter melden.
Fehlercode-Tabelle
| Code | Bedeutung | Empfohlene Reaktion |
|---|---|---|
| 400 | Konfigurationsfehler | Programm nicht starten, Konfiguration prüfen |
| 403 | Exportpfad nicht schreibbar | Verzeichnis anlegen, Rechte prüfen |
| 408 | Timeout | Verbindung zur TSE prüfen, USB-Stick eingesteckt? |
| 409 | Offene Transaktion | Vorfall protokollieren, Support kontaktieren |
| 500 | TSE-interner Signierfehler | TSE-Hardware defekt? TSE-Anbieter kontaktieren |
| 503 | Verbindungsfehler | Netzwerk (REST) oder USB-Mount (File) prüfen |
Initialisierung beim Programmstart
Konfigurationsfehler möglichst früh erkennen — nicht erst beim ersten Bon:
import kassensichv.rest;
import kassensichv.manager;
fn TseInitPruefen(mgr: int64): int64 {
// Status als erstes abfragen — erkennt Konfigurationsfehler sofort
var status: int64 := TseGetStatus(mgr);
if status == 0 then {
PrintLn("[FEHLER] TSE konnte nicht initialisiert werden"c);
return 0;
}
// Seriennummer prüfen — bei REST: wird von API geladen
var serial: int64 := TseGetSerial(mgr);
if serial == 0 then {
PrintLn("[FEHLER] TSE-Seriennummer nicht abrufbar"c);
free(status);
return 0;
}
Print("TSE bereit: "c); PrintLn(serial as pchar);
free(serial);
free(status);
return 1;
}
fn main(): int64 {
var mgr: int64 := TseRestNew(cfg as int64);
if TseInitPruefen(mgr) == 0 then {
TseManagerFree(mgr);
return 1; // Programm abbrechen
}
// Kassensoftware starten ...
return 0;
}
Umsatz-Berechnung: Eurocent, kein Float
Niemals f64 für Geldbeträge verwenden. IEEE-754-Gleitkommazahlen haben Rundungsfehler (0.1 + 0.2 ≠ 0.3). Das umsatz-Feld in BelegDaten ist int64 in Eurocent:
// FALSCH — Rundungsfehler bei 0.1 + 0.2
var betrag: f64 := 9.99;
beleg.umsatz := betrag as int64; // kann 998 oder 999 ergeben!
// RICHTIG — integer Eurocent
beleg.umsatz := 999; // 9,99 EUR = 999 Cent
// RICHTIG — Addition
var teilbetrag1: int64 := 550; // 5,50 EUR
var teilbetrag2: int64 := 800; // 8,00 EUR
beleg.umsatz := teilbetrag1 + teilbetrag2; // exakt 1350 Cent = 13,50 EUR
Das DSFinV-K-Format erwartet Beträge mit Punkt als Dezimaltrennzeichen (9.99) im prozessDaten-String — das ist reines Textformat und unabhängig von der internen Cent-Rechnung.
Prozessdaten-Format (DSFinV-K)
Das prozessDaten-Feld folgt dem DSFinV-K 2.3-Schema. Für einfache Kassenbons:
{Artikel};{Betrag_19%}_{Betrag_7%}_{Betrag_0%}_{Betrag_special}_{Betrag_sonstig}
Mehrwertsteuer-Zuordnung nach Steuersatz:
| Spalte | Steuersatz | Typischer Einsatz |
|---|---|---|
| Feld 1 (19%) | Normaler MwSt.-Satz | Elektronik, Kleidung, Haushaltsware |
| Feld 2 (7%) | Ermäßigter MwSt.-Satz | Lebensmittel, Bücher, ÖPNV-Tickets |
| Feld 3 (0%) | Steuerfreie Umsätze | Exportlieferungen, Versicherungen |
| Feld 4 | Besonderer Steuersatz | Spezialfälle |
| Feld 5 | Sonstige | Trinkgelder, Gutscheineinlösung |
// Laptop (19% MwSt.): 999,00 EUR
beleg.prozessDaten := "Laptop;999.00_0.00_0.00_0.00_0.00"c;
// Buch (7% MwSt.): 15,90 EUR
beleg.prozessDaten := "Buch;0.00_15.90_0.00_0.00_0.00"c;
// Gemischter Einkauf
beleg.prozessDaten := "Laptop+Buch;999.00_15.90_0.00_0.00_0.00"c;
// Restaurant-Bestellung (Speisen 7%, Getränke 19%)
beleg.prozessDaten := "Getränke+Speisen;8.50_12.00_0.00_0.00_0.00"c;
Für vollständige DSFinV-K-Anforderungen (komplexe Bonpflicht-Details, Kassenabschlüsse) die offizielle DSFinV-K 2.3-Spezifikation des BMF (Bundesministerium der Finanzen) konsultieren.
Testen
Einheitstests mit Mock
import kassensichv.types;
import kassensichv.mock;
import kassensichv.manager;
fn TestEinfacherBon(): int64 {
var mgr: int64 := TseMockNew();
var beleg: BelegDaten;
beleg.prozessTyp := PROZESSTYP_KASSENBELEG;
beleg.kassenNr := "TEST-001"c;
beleg.prozessDaten := "Test-Artikel;1.00_0.00_0.00_0.00_0.00"c;
beleg.umsatz := 100;
var sig: int64 := TseProcessBeleg(mgr, addr beleg);
var s: SigErgebnis := (sig as *SigErgebnis)^;
var ok: int64 := 1;
// success muss 1 sein
if s.success == 0 then { PrintLn("FAIL: success=0"c); ok := 0; }
// Zähler muss 1 sein (erster Bon)
if s.sigZaehler != 1 then { PrintLn("FAIL: sigZaehler != 1"c); ok := 0; }
// QR-Code muss mit V0;MOCK beginnen
// (Vergleich mit StringStartsWith aus std.string)
SigErgebnisFree(sig);
TseManagerFree(mgr);
return ok;
}
fn TestSignaturZaehlerMonoton(): int64 {
var mgr: int64 := TseMockNew();
var prevZaehler: int64 := 0;
var i: int64 := 1;
while i <= 5 do {
var b: BelegDaten;
b.prozessTyp := PROZESSTYP_KASSENBELEG;
var sig: int64 := TseProcessBeleg(mgr, addr b);
var s: SigErgebnis := (sig as *SigErgebnis)^;
if s.sigZaehler <= prevZaehler then {
PrintLn("FAIL: Zähler nicht monoton steigend"c);
SigErgebnisFree(sig);
TseManagerFree(mgr);
return 0;
}
prevZaehler := s.sigZaehler;
SigErgebnisFree(sig);
i := i + 1;
}
TseManagerFree(mgr);
return 1;
}
fn TestTimeout(): int64 {
var mgr: int64 := TseMockNew();
TseMockSetSimulateTimeout(mgr, 1);
var b: BelegDaten; b.prozessTyp := PROZESSTYP_KASSENBELEG;
var sig: int64 := TseProcessBeleg(mgr, addr b);
var s: SigErgebnis := (sig as *SigErgebnis)^;
var ok: int64 := 1;
if s.success != 0 then { PrintLn("FAIL: Timeout nicht erkannt"c); ok := 0; }
SigErgebnisFree(sig);
TseManagerFree(mgr);
return ok;
}
BSI TR-03153 QR-Code-Validierung
Die BSI TR-03153 (Anhang A) enthält offizielle Testvektoren für das QR-Code-Format. Beim Mock lässt sich das Format-Muster prüfen:
Format: V0;{7 Felder, semikolon-getrennt}
Feld 1: "V0" (fest)
Feld 2: TSE-Seriennummer (nicht leer)
Feld 3: Startzeitpunkt UTC (ISO 8601, endet auf "Z")
Feld 4: Endzeitpunkt UTC (ISO 8601, endet auf "Z")
Feld 5: Signaturzähler (positive ganze Zahl)
Feld 6: Anzahl Transaktionen (positive ganze Zahl)
Feld 7: Signaturwert (Base64, nicht leer)
Provider-Entscheidungsguide
Welche Art TSE brauche ich?
│
├── Entwicklung / Tests / CI? ──────────────────► TseMockNew()
│ (kassensichv.mock)
│
├── Stationäre Kasse, eigene Hardware gewünscht?
│ │
│ ├── USB-TSE vorhanden (Swissbit / Epson)?
│ │ └── Ja ─────────────────────────► TseFileNew(cfg)
│ │ (kassensichv.file)
│ └── Nein → weiter zu Cloud
│
└── Mobile Kasse / Online-Shop / kein Sticks-Management?
└──────────────────────────────────────────► TseRestNew(cfg)
(kassensichv.rest)
Fiskaly / Deutsche Fiskal
| Kriterium | Mock | REST (Cloud) | File (USB) |
|---|---|---|---|
| Hardware nötig | nein | nein | USB-TSE-Stick |
| Netzwerk nötig | nein | ja | nein |
| Monatliche Kosten | nein | ja (Anbietergebühr) | nein (nach Einmalkauf) |
| Geeignet für Tests | ja | Sandbox | nein (Hardware) |
| Offline-Betrieb | ja | nein | ja |
| Mobile Kasse | — | empfohlen | möglich (USB-Hub) |
| Zentrales Management | — | ja | nein |
Checkliste vor der Inbetriebnahme
- [ ] TSE beim BSI-zertifizierten Anbieter angemeldet (Fiskaly, Swissbit, …)
- [ ] TSE innerhalb von 4 Wochen nach Inbetriebnahme beim Finanzamt gemeldet (§ 146a AO)
- [ ] Alle Bonds enthalten die 7 Pflichtfelder nach KassenSichV / BSI TR-03153
- [ ]
SigErgebnis.successwird vor Bonausgabe geprüft - [ ] Offene Transaktionen (
success=0beiTseCloseBeleg) werden protokolliert und behandelt - [ ]
TseExportAuditDatawurde getestet und ist einsatzbereit - [ ] Exportverzeichnis hat die richtigen Schreibrechte
- [ ] Konfiguration (API-Key, Kassen-ID) kommt aus Umgebungsvariablen oder verschlüsseltem Config — nie im Quellcode hardcoden
- [ ] CI/CD-Tests laufen mit Mock-Provider
- [ ] BSI-Testvektoren für QR-Code-Format wurden geprüft
Haftungshinweis: Diese Bibliothek ist ein technisches Hilfsmittel. Kassenbetreiber sind für die gesetzeskonforme Integration und den ordnungsgemäßen Betrieb verantwortlich. Bei Unklarheiten zur rechtlichen Einordnung steuerlichen oder rechtlichen Rat einholen.
