Inhaltsverzeichnis

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:

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:

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

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.

Weiterführend

Letzte Aktualisierung: 2026-06-12