feat: Umsatzsteuer ueber VAT_RATE_PERCENT konfigurierbar und auf der Rechnung ausgewiesen
Die Rechnung wies bisher gar keine Steuer aus — weder Satz und Betrag noch
den Pflichthinweis nach § 19 UStG. Der Satz kommt jetzt aus
VAT_RATE_PERCENT und steht auf PDF und HTML-Ansicht: 7 (ermaessigt, Honig
als Lebensmittel), 19 (voll) oder 0 fuer die Kleinunternehmerregelung, bei
der anstelle der Steuerzeilen der vorgeschriebene Hinweis erscheint.
DIE SHOP-PREISE SIND BRUTTOPREISE. Gegenueber Verbrauchern ist der
Gesamtpreis inklusive Umsatzsteuer anzugeben (§ 1 PAngV), und genau dieser
Betrag steht in Product.priceCents und Order.totalCents. Die Steuer wird
deshalb herausgerechnet, nicht aufgeschlagen — ein Aufschlag ergaebe einen
anderen Endpreis als den, den die Kundschaft im Warenkorb gesehen und
bezahlt hat.
Gerechnet wird auf der Gesamtsumme statt je Position, damit sich Rundungen
einzelner Zeilen nicht aufsummieren, und der Steuerbetrag ergibt sich als
Rest statt aus einer zweiten Rundung: nur so gilt immer netto + steuer ===
brutto. Eine Rechnung, die sich nicht aufaddiert, ist keine brauchbare
Rechnung. Der Versand traegt den Satz der Ware, er ist umsatzsteuerlich
eine Nebenleistung.
Der Satz wird als Invoice.vatRatePercent FESTGESCHRIEBEN statt bei jeder
Anzeige aus der Umgebung gelesen. Wechselt der Steuerstatus des Betriebs,
muessen alte Rechnungen weiterhin den damals ausgewiesenen Satz zeigen — ein
Beleg ist die Aufzeichnung eines vergangenen Vorgangs, keine Ansicht auf die
heutige Konfiguration. Bestehende Rechnungen tragen null und zeigen wie
bisher keine Steuerzeilen, weil rueckwirkend nicht feststellbar ist, welcher
Satz gegolten haette.
Ein unbrauchbarer Wert faellt auf 7% zurueck und wird laut geloggt, statt
eine bereits bezahlte Bestellung scheitern zu lassen. Damit das nicht
unbemerkt bleibt, prueft "pnpm preflight" den Wert jetzt als Blocker und
warnt zusaetzlich bei einem in Deutschland unueblichen Satz.
Der bisherige legalNote war ein Platzhalter ("Kleinunternehmerregelung /
Umsatzsteuerausweis gemaess tatsaechlichem Status ergaenzen") und ist
ersetzt, da die Angabe nun tatsaechlich erfolgt.
Bekannte Grenze, im README vermerkt: es gibt genau EINEN Satz. Honig ist
ermaessigt besteuert, Kerzen nicht — werden beide verkauft, braucht es einen
Satz je ProductCategory.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
1 parent
70ede77dde
commit
7c59283ede
15 files changed
+334
-7
No files matched your search
@@ -0,0 +1,93 @@
|
||||
import { describe, test, expect, vi, afterEach } from "vitest";
|
||||
import { splitGross, vatRatePercent, DEFAULT_VAT_RATE_PERCENT } from "./vat";
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("vatRatePercent", () => {
|
||||
test("nimmt einen gueltigen Satz", () => {
|
||||
expect(vatRatePercent("7")).toBe(7);
|
||||
expect(vatRatePercent("19")).toBe(19);
|
||||
expect(vatRatePercent(" 19 ")).toBe(19);
|
||||
});
|
||||
|
||||
test("laesst 0 zu — das ist die Kleinunternehmerregelung", () => {
|
||||
// Arrange & Act & Assert: 0 ist ein gueltiger Wert und darf NICHT als
|
||||
// "nicht gesetzt" behandelt werden, sonst waere § 19 UStG nicht abbildbar.
|
||||
expect(vatRatePercent("0")).toBe(0);
|
||||
});
|
||||
|
||||
test("faellt ohne Angabe auf den ermaessigten Satz zurueck", () => {
|
||||
expect(vatRatePercent(undefined)).toBe(DEFAULT_VAT_RATE_PERCENT);
|
||||
expect(vatRatePercent("")).toBe(DEFAULT_VAT_RATE_PERCENT);
|
||||
expect(vatRatePercent(" ")).toBe(DEFAULT_VAT_RATE_PERCENT);
|
||||
});
|
||||
|
||||
test("meldet einen unbrauchbaren Wert laut, statt still zu rechnen", () => {
|
||||
// Arrange: ein falscher Steuersatz darf nicht unbemerkt bleiben.
|
||||
const logged = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
|
||||
// Act
|
||||
const rate = vatRatePercent("7,5");
|
||||
|
||||
// Assert
|
||||
expect(rate).toBe(DEFAULT_VAT_RATE_PERCENT);
|
||||
expect(logged).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
test("weist Kommazahlen, negative Werte und Unsinn ab", () => {
|
||||
// Arrange
|
||||
vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
|
||||
// Act & Assert
|
||||
expect(vatRatePercent("7.5")).toBe(DEFAULT_VAT_RATE_PERCENT);
|
||||
expect(vatRatePercent("-19")).toBe(DEFAULT_VAT_RATE_PERCENT);
|
||||
expect(vatRatePercent("101")).toBe(DEFAULT_VAT_RATE_PERCENT);
|
||||
expect(vatRatePercent("neunzehn")).toBe(DEFAULT_VAT_RATE_PERCENT);
|
||||
});
|
||||
});
|
||||
|
||||
describe("splitGross", () => {
|
||||
test("rechnet die Steuer aus dem Bruttobetrag heraus", () => {
|
||||
// Arrange & Act: 10,70 EUR brutto bei 7% sind 10,00 netto und 0,70 Steuer.
|
||||
const split = splitGross(1070, 7);
|
||||
|
||||
// Assert
|
||||
expect(split).toEqual({ netCents: 1000, vatCents: 70 });
|
||||
});
|
||||
|
||||
test("rechnet auch beim vollen Satz heraus, statt aufzuschlagen", () => {
|
||||
// Arrange & Act & Assert: 11,90 brutto bei 19% sind 10,00 netto. Ein
|
||||
// Aufschlag ergaebe 14,16 und waere ein anderer Endpreis als der, den die
|
||||
// Kundschaft im Warenkorb gesehen hat.
|
||||
expect(splitGross(1190, 19)).toEqual({ netCents: 1000, vatCents: 190 });
|
||||
});
|
||||
|
||||
test("netto plus Steuer ergibt immer exakt den Bruttobetrag", () => {
|
||||
// Arrange: krumme Betraege, bei denen zwei getrennte Rundungen
|
||||
// auseinanderlaufen wuerden.
|
||||
const amounts = [1, 99, 999, 1234, 4567, 98765];
|
||||
|
||||
for (const rate of [7, 19]) {
|
||||
for (const gross of amounts) {
|
||||
// Act
|
||||
const { netCents, vatCents } = splitGross(gross, rate);
|
||||
|
||||
// Assert: eine Rechnung, die sich nicht aufaddiert, ist keine
|
||||
// brauchbare Rechnung.
|
||||
expect(netCents + vatCents).toBe(gross);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("weist bei Satz 0 keine Steuer aus", () => {
|
||||
// Arrange & Act & Assert: Kleinunternehmerregelung — der Bruttobetrag ist
|
||||
// zugleich der Nettobetrag.
|
||||
expect(splitGross(1070, 0)).toEqual({ netCents: 1070, vatCents: 0 });
|
||||
});
|
||||
|
||||
test("kommt mit dem Betrag null zurecht", () => {
|
||||
expect(splitGross(0, 19)).toEqual({ netCents: 0, vatCents: 0 });
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user