KassenSichV in der Praxis: was in der Dokumentation nicht steht
Jede Kasse in Deutschland muss ihre Umsätze technisch signieren. Die Anbieter-Dokumentation erklärt den API-Aufruf. Die drei Dinge, die einen wirklich treffen, stehen dort nicht.
- KassenSichV
- § 146a AO
- Postgres
- Multi-Tenant
Wer in Deutschland ein Kassensystem baut, kommt an § 146a AO und der Kassensicherungsverordnung nicht vorbei. Jeder Geschäftsvorfall muss von einer zertifizierten technischen Sicherheitseinrichtung signiert werden, die Signaturen müssen lückenlos und unveränderbar aufbewahrt werden, und bei einer Kassennachschau muss das alles exportierbar sein.
Ich habe das für MenuCloud gebaut, eine mandantenfähige Plattform für Gastronomie. Der Anbieter, dessen Cloud-TSE ich nutze, hat eine ordentliche Dokumentation. Sie erklärt, wie man eine Transaktion signiert. Sie erklärt nicht die drei Dinge, an denen man tatsächlich hängenbleibt.
Erstens: Die Signatureinheit gehört dem Mandanten, nicht der Plattform
Der naheliegende Bau ist eine TSE für die Plattform, durch die alle Mandanten laufen. Das ist bequem und rechtlich falsch. Die Aufzeichnungspflicht trifft den einzelnen Steuerpflichtigen. Bei einer Prüfung wird nach den Aufzeichnungen dieses einen Betriebs gefragt, nicht nach denen einer Plattform, in der er einer von vielen ist.
Also bekommt jeder Mandant eine eigene, ihm zurechenbare Signatureinheit. Entweder bringt er eine mit, oder sie wird bei der Einrichtung unter dem Händlerkonto der Plattform für ihn angelegt. In beiden Fällen liegen tss_id und client_id am Mandanten und nicht in der Umgebung.
export interface TseOverride {
tss_id?: string | null;
client_id?: string | null;
env?: 'test' | 'live' | null;
}Das hat eine Folge, die man vorher bedenken sollte: Auch der Rückbau muss existieren. Kündigt ein Mandant, muss seine Signatureinheit sauber stillgelegt werden, und seine bisherigen Signaturen müssen trotzdem aufbewahrt bleiben. Zehn Jahre lang.
Zweitens: Zwei Bestellungen gleichzeitig zerreißen die Kette
Die Signaturen hängen in einer Hash-Kette. Jede Zeile trägt den Hash ihrer Vorgängerin. Das ist der Teil, der eine nachträgliche Änderung sichtbar macht, und damit der eigentliche Kern des Beweiswerts.
Meine erste Fassung berechnete das in der Anwendung: letzte Zeile lesen, Hash bilden, neue Zeile schreiben. Das funktioniert, solange nur eine Bestellung zur Zeit abgeschlossen wird. Im Abendgeschäft ist das nicht der Fall. Zwei gleichzeitige Abschlüsse lesen dieselbe Vorgängerzeile, berechnen denselben Vorgänger-Hash und schreiben beide. Die Kette gabelt sich.
Die Lösung liegt nicht in der Anwendung, sondern in der Datenbank. Lesen, Rechnen und Schreiben passieren in einer Funktion, die vorher eine Sperre je Betrieb nimmt. Betriebe blockieren sich dadurch nicht gegenseitig, aber innerhalb eines Betriebs wächst die Kette streng linear.
-- Sperre je Restaurant, wird am Transaktionsende freigegeben.
PERFORM pg_advisory_xact_lock(
hashtext('tse-chain:' || p_restaurant_id::text)
);
SELECT row_hash INTO v_prev_row_hash
FROM tse_transactions
WHERE restaurant_id = p_restaurant_id
ORDER BY created_at DESC
LIMIT 1;pg_advisory_xact_lock nimmt eine Zahl, keine Zeichenkette, deshalb der Umweg über hashtext. Die Sperre endet mit der Transaktion, ganz gleich ob sie erfolgreich war. Es gibt keinen Pfad, auf dem sie hängenbleibt.
Drittens: Was passiert, wenn die Signatur ausfällt
Das ist die Frage, die über die Qualität eines solchen Systems entscheidet, und die in keiner Anbieter-Dokumentation beantwortet wird. Die TSE ist ein fremder Dienst. Fremde Dienste fallen aus.
Es gibt zwei Möglichkeiten. Die Bestellung wird trotzdem gebucht, dann entsteht stiller, unsignierter Umsatz. Oder die Bestellung wird nicht gebucht, dann sieht der Gast einen Fehler. Die erste Möglichkeit ist bequemer und für den Gastronomen die schlechtere: Bei einer Prüfung ist es sein Problem, nicht meins.
if (needsTseRetry) {
// Für den Nachsignatur-Lauf vormerken. NICHT als bezahlt markieren.
await supabase.from('food_orders').update({
needs_tse_signature: true,
tse_last_error: tseFailureReason,
}).eq('id', id).eq('restaurant_id', restaurant_id);
return NextResponse.json(
{ error: 'tse_signature_required', reason: tseFailureReason },
{ status: 503 },
);
}503 statt 500, weil der Zustand vorübergehend ist. Die Bestellung ist nicht verloren, sie trägt eine Markierung, und ein Cron-Lauf holt die Signatur nach, sobald der Dienst wieder antwortet. Der Fehlergrund wird an der Bestellung gespeichert, damit hinterher nachvollziehbar ist, was los war.
Die Ausnahme, die man ehrlich benennen muss
Fail-closed gilt, wenn für den Mandanten eine TSE eingerichtet ist. Ist gar keine hinterlegt, läuft die Bestellung durch und kann später nachsigniert werden. Das ist Absicht, damit ein Betrieb während der Einrichtung nicht stillsteht, und es ist die Stelle, an der ich mich beim nächsten Ausbau selbst prüfen würde: Der Übergang von „noch nicht eingerichtet“ zu „eingerichtet“ muss eindeutig sein, sonst wird aus einer Einrichtungshilfe eine dauerhafte Lücke.
Eine Kleinigkeit, die im Abendgeschäft groß wurde
Jeder Signaturvorgang braucht ein Zugangstoken. In der ersten Fassung holte jeder Bestellabschluss ein frisches, rund 300 Millisekunden vor der eigentlichen Signatur. Bei einer Bestellung pro Minute fällt das nicht auf. Bei dreißig Bestellungen in fünf Minuten wird daraus eine Last auf dem Anmelde-Endpunkt des Anbieters, die niemand braucht.
Der Anbieter gibt Token mit 60 Minuten Gültigkeit aus. Sie werden jetzt 50 Minuten lang wiederverwendet, mit 30 Sekunden Sicherheitsabstand vor dem Ablauf. Das ist eine der Änderungen, die man nicht plant, sondern misst.
Was ich einem Team raten würde, das damit anfängt
- Die Signatureinheit gehört dem Steuerpflichtigen. Wer das später umbaut, muss die Historie mit umziehen.
- Alles, was eine Kette fortschreibt, gehört in eine Datenbanktransaktion mit Sperre. Nicht in die Anwendung.
- Den Ausfallpfad zuerst entscheiden, nicht zuletzt. Er bestimmt, wem am Ende das Risiko gehört.
- Den Aufbewahrungszeitraum von Anfang an mitdenken. Zehn Jahre überleben mehrere Umbauten des Systems.
Nichts davon ist besonders schwierig. Es steht nur in keiner Dokumentation, weil es Fragen des Entwurfs sind und nicht der Schnittstelle. Genau deshalb ist es der Teil, der am längsten dauert.
Belege
- src/lib/tse-chain.ts und supabase/migrations/20260413_tse_chain_atomic_append.sql (Sperre, Hash-Kette)
- src/lib/food-order-tse.ts (Signatur, Token-Zwischenspeicher)
- src/app/api/food-orders/route.ts, Zeile 472 ff. (fail-closed, HTTP 503)
- src/lib/fiskaly-provision.ts (Provisionierung je Mandant)