import { describe, test, expect, vi, afterEach } from "vitest"; import { splitGross, vatRatePercent, vatRateForCategory, vatBreakdown, 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 }); }); }); describe("vatRateForCategory", () => { test("nimmt den Satz der jeweiligen Kategorie", () => { // Arrange const env = { VAT_RATE_PERCENT_HONIG: "7", VAT_RATE_PERCENT_KERZEN: "19", VAT_RATE_PERCENT_PROPOLIS: "19", }; // Act & Assert: Honig ist ein Lebensmittel und ermaessigt besteuert, eine // Bienenwachskerze nicht. expect(vatRateForCategory("HONIG", env)).toBe(7); expect(vatRateForCategory("KERZEN", env)).toBe(19); expect(vatRateForCategory("PROPOLIS", env)).toBe(19); }); test("faellt auf den gemeinsamen Satz zurueck, wenn die Kategorie keinen eigenen hat", () => { // Arrange & Act & Assert: eine bestehende Konfiguration mit nur // VAT_RATE_PERCENT bleibt dadurch gueltig. expect(vatRateForCategory("KERZEN", { VAT_RATE_PERCENT: "19" })).toBe(19); expect(vatRateForCategory("HONIG", { VAT_RATE_PERCENT: "7" })).toBe(7); }); test("behandelt eine unbekannte Kategorie wie eine ohne eigenen Satz", () => { // Arrange & Act & Assert: eine spaeter ergaenzte Kategorie soll den // gemeinsamen Wert erben, statt geraten zu werden. expect(vatRateForCategory("SEIFE", { VAT_RATE_PERCENT: "19" })).toBe(19); }); test("greift ohne jede Angabe auf den ermaessigten Satz zurueck", () => { expect(vatRateForCategory("HONIG", {})).toBe(DEFAULT_VAT_RATE_PERCENT); }); }); describe("vatBreakdown", () => { test("weist bei nur einem Satz eine einzige Zeile aus", () => { // Arrange & Act const lines = vatBreakdown({ items: [{ totalCents: 1000, vatRatePercent: 7 }], shippingCents: 70, discountCents: 0, }); // Assert expect(lines).toEqual([{ ratePercent: 7, grossCents: 1070, netCents: 1000, vatCents: 70 }]); }); test("trennt Honig und Kerze nach Saetzen", () => { // Arrange: ein Glas Honig und eine Kerze im selben Paket. const lines = vatBreakdown({ items: [ { totalCents: 1070, vatRatePercent: 7 }, { totalCents: 1190, vatRatePercent: 19 }, ], shippingCents: 0, discountCents: 0, }); // Act & Assert: je Satz eine eigene Zeile, kein Mischsatz. expect(lines).toEqual([ { ratePercent: 7, grossCents: 1070, netCents: 1000, vatCents: 70 }, { ratePercent: 19, grossCents: 1190, netCents: 1000, vatCents: 190 }, ]); }); test("verteilt Versand und Rabatt im Verhaeltnis der Warenwerte", () => { // Arrange: 3/4 Honig, 1/4 Kerze — der Versand teilt sich genauso auf, // denn als Nebenleistung folgt er der jeweiligen Hauptleistung. const lines = vatBreakdown({ items: [ { totalCents: 3000, vatRatePercent: 7 }, { totalCents: 1000, vatRatePercent: 19 }, ], shippingCents: 400, discountCents: 0, }); // Act & Assert expect(lines[0].grossCents).toBe(3300); expect(lines[1].grossCents).toBe(1100); }); test("die Summe der Zeilen ergibt exakt den Rechnungsbetrag", () => { // Arrange: krumme Betraege, bei denen eine naive Rundung Cents // verschwinden oder entstehen liesse. const items = [ { totalCents: 1333, vatRatePercent: 7 }, { totalCents: 777, vatRatePercent: 19 }, { totalCents: 99, vatRatePercent: 0 }, ]; const shippingCents = 495; const discountCents = 317; // Act const lines = vatBreakdown({ items, shippingCents, discountCents }); // Assert const expectedTotal = items.reduce((sum, item) => sum + item.totalCents, 0) + shippingCents - discountCents; expect(lines.reduce((sum, line) => sum + line.grossCents, 0)).toBe(expectedTotal); for (const line of lines) { expect(line.netCents + line.vatCents).toBe(line.grossCents); } }); test("fasst mehrere Positionen desselben Satzes zusammen", () => { // Arrange & Act const lines = vatBreakdown({ items: [ { totalCents: 500, vatRatePercent: 7 }, { totalCents: 570, vatRatePercent: 7 }, ], shippingCents: 0, discountCents: 0, }); // Assert: eine Rechnung listet je Satz eine Zeile, nicht je Position. expect(lines).toHaveLength(1); expect(lines[0].grossCents).toBe(1070); }); test("liefert ohne Positionen keine Aufstellung", () => { // Arrange & Act & Assert: betrifft Rechnungen aus der Zeit vor der // Steuerausweisung — dort bleibt der Block weg statt falsch zu rechnen. expect(vatBreakdown({ items: [], shippingCents: 500, discountCents: 0 })).toEqual([]); }); });