feat: fortlaufende Rechnungsnummer statt Zufallszahl
§ 14 Abs. 4 Nr. 4 UStG verlangt eine FORTLAUFENDE Rechnungsnummer. Bisher
stand dort "RE-2026-<8 Hex>": eindeutig ja, fortlaufend nein. Bei einer
Betriebsprüfung ließe sich damit nicht zeigen, dass keine Rechnung fehlt —
aus einer Lücke in einer Zufallsfolge lässt sich nichts ablesen, aus einer
Lücke in 0001, 0002, 0004 sehr wohl. Das war der letzte offene Rechtspunkt
vor dem Live-Gang, und der einzige, der sich ohne Absprache schließen ließ.
Die Nummer heißt jetzt RE-<Jahr>-<laufende Nummer>, also RE-2026-0001. Die
Zählung beginnt jedes Jahr neu bei 0001; das ist zulässig, solange das Jahr
Teil der Nummer ist.
Die laufende Nummer kommt aus der neuen Tabelle invoice_counters, eine Zeile
je Jahr. Der naheliegende Weg "höchste vorhandene Nummer + 1" wäre falsch:
zwei gleichzeitig eintreffende Stripe-Webhooks läsen denselben Höchstwert und
vergäben dieselbe Nummer. Das upsert mit { increment: 1 } sperrt die Zeile,
solange es zählt, und serialisiert damit die Vergabe.
Vor dem Hochzählen prüft dieselbe Transaktion, ob zur Bestellung bereits eine
Rechnung existiert. Stripe stellt denselben Webhook erneut zu, wenn die
Bestätigung ausbleibt — ohne diese Prüfung verbrauchte jeder Wiederholungs-
versuch eine Nummer und risse genau die Lücke, die hier vermieden werden
soll.
Vier Stellen halten die Nummern gleich lang und damit sortierbar; ab der
zehntausendsten Rechnung eines Jahres wächst die Nummer mit, statt
abgeschnitten zu werden — eine abgeschnittene Nummer wäre nicht mehr
eindeutig.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
1 parent
7fe1a01550
commit
e525db84ef
8 files changed
+144
-22
No files matched your search
@@ -0,0 +1,34 @@
|
||||
import { describe, test, expect } from "vitest";
|
||||
import { formatInvoiceNumber } from "./invoice-number";
|
||||
|
||||
describe("formatInvoiceNumber", () => {
|
||||
test("beginnt jedes Jahr bei 0001", () => {
|
||||
// Arrange & Act & Assert
|
||||
expect(formatInvoiceNumber(2026, 1)).toBe("RE-2026-0001");
|
||||
expect(formatInvoiceNumber(2027, 1)).toBe("RE-2027-0001");
|
||||
});
|
||||
|
||||
test("zaehlt fortlaufend weiter", () => {
|
||||
// Arrange & Act & Assert: genau darum geht es — § 14 Abs. 4 Nr. 4 UStG
|
||||
// verlangt eine fortlaufende, nicht bloss eine eindeutige Nummer.
|
||||
expect(formatInvoiceNumber(2026, 2)).toBe("RE-2026-0002");
|
||||
expect(formatInvoiceNumber(2026, 42)).toBe("RE-2026-0042");
|
||||
});
|
||||
|
||||
test("haelt die Nummern gleich lang, damit sie sortierbar bleiben", () => {
|
||||
// Arrange
|
||||
const numbers = [1, 9, 10, 999].map((n) => formatInvoiceNumber(2026, n));
|
||||
|
||||
// Act & Assert: alphabetisch sortiert ergibt sich dieselbe Reihenfolge wie
|
||||
// numerisch — bei ungepolsterten Nummern kaeme "RE-2026-10" vor
|
||||
// "RE-2026-9".
|
||||
expect([...numbers].sort()).toEqual(numbers);
|
||||
});
|
||||
|
||||
test("waechst ueber vier Stellen hinaus, statt abzuschneiden", () => {
|
||||
// Arrange & Act & Assert: eine abgeschnittene Nummer waere nicht mehr
|
||||
// eindeutig — lieber eine Stelle mehr als eine doppelte Rechnungsnummer.
|
||||
expect(formatInvoiceNumber(2026, 9999)).toBe("RE-2026-9999");
|
||||
expect(formatInvoiceNumber(2026, 10000)).toBe("RE-2026-10000");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user