import { Prisma } from "@prisma/client"; import { describe, expect, it } from "vitest"; import { alsText, alsWirkung, bestand, betrag, betragLesen, bilanz, positionssumme, runden, summe, } from "@/lib/kasse/betrag"; /** * Das Rechnen mit Geld. * * Der Grund für diese Datei ist eine einzige Zeile, die es hier nicht geben * darf: `0.1 + 0.2`. In Fließkomma ergibt das 0.30000000000000004, und in * einer Kasse ist das kein Rundungsfehler, sondern eine Differenz in der * Kassenprüfung, die niemand erklären kann. Deshalb rechnet die gesamte * Kasse mit `Prisma.Decimal`, und deshalb prüft dieser Test genau das nach. */ describe("Beträge lesen", () => { it("nimmt Zahl, Text und Decimal entgegen", () => { expect(betrag(12.5).toFixed(2)).toBe("12.50"); expect(betrag("12.50").toFixed(2)).toBe("12.50"); expect(betrag(new Prisma.Decimal("12.5")).toFixed(2)).toBe("12.50"); }); it("macht aus fehlenden Angaben null Euro", () => { expect(betrag(null).toFixed(2)).toBe("0.00"); expect(betrag(undefined).toFixed(2)).toBe("0.00"); expect(betrag("").toFixed(2)).toBe("0.00"); }); }); describe("Summe", () => { it("rechnet ohne Fließkommafehler", () => { // Die Probe aufs Exempel: in `number` ergäbe das 0.30000000000000004. expect(summe(["0.10", "0.20"]).toFixed(2)).toBe("0.30"); }); it("addiert eine ganze Kassenseite auf den Cent", () => { const werte = ["1284.50", "412.90", "2340.00", "868.00", "1150.00", "624.00", "398.20", "36.00"]; expect(summe(werte).toFixed(2)).toBe("7113.60"); }); it("ergibt bei leerer Liste null Euro und nicht undefined", () => { expect(summe([]).toFixed(2)).toBe("0.00"); }); }); describe("Runden", () => { it("rundet kaufmännisch — ab der halben Stelle nach oben", () => { expect(runden(new Prisma.Decimal("2.345")).toFixed(2)).toBe("2.35"); expect(runden(new Prisma.Decimal("2.344")).toFixed(2)).toBe("2.34"); // Und nicht bankers rounding: 2,355 wird 2,36, nicht 2,36-oder-2,35. expect(runden(new Prisma.Decimal("2.355")).toFixed(2)).toBe("2.36"); expect(runden(new Prisma.Decimal("2.365")).toFixed(2)).toBe("2.37"); }); }); describe("Richtung", () => { it("macht aus Art und positivem Betrag ein Vorzeichen", () => { expect(alsWirkung("EINNAHME", "100.00").toFixed(2)).toBe("100.00"); expect(alsWirkung("AUSGABE", "100.00").toFixed(2)).toBe("-100.00"); }); }); describe("Bestand", () => { it("ist Anfangsbestand plus die Wirkung aller Buchungen", () => { const wert = bestand("250.00", [ { art: "EINNAHME", betrag: "3860.00" }, { art: "AUSGABE", betrag: "1420.60" }, ]); expect(wert.toFixed(2)).toBe("2689.40"); }); it("darf negativ werden — eine überzogene Barkasse ist ein Befund, kein Fehler", () => { expect(bestand("10.00", [{ art: "AUSGABE", betrag: "50.00" }]).toFixed(2)).toBe("-40.00"); }); }); describe("Bilanz", () => { it("trennt Einnahmen und Ausgaben und bildet den Saldo", () => { const werte = bilanz([ { art: "EINNAHME", betrag: "6500.00" }, { art: "EINNAHME", betrag: "780.00" }, { art: "AUSGABE", betrag: "1284.50" }, ]); expect(werte.einnahmen.toFixed(2)).toBe("7280.00"); expect(werte.ausgaben.toFixed(2)).toBe("1284.50"); expect(werte.saldo.toFixed(2)).toBe("5995.50"); }); }); describe("Positionssumme", () => { it("rechnet Viertelstunden auf den Cent", () => { // 0,25 × 47,50 = 11,875 — gerundet 11,88 und nicht 11,87. expect(positionssumme("0.25", "47.50").toFixed(2)).toBe("11.88"); }); it("rechnet ganze Mengen ohne Rest", () => { expect(positionssumme(9, "24.00").toFixed(2)).toBe("216.00"); }); }); describe("Eingaben lesen", () => { it("versteht die deutsche Schreibweise", () => { expect(betragLesen("1.234,56")?.toFixed(2)).toBe("1234.56"); expect(betragLesen("1.234,56 €")?.toFixed(2)).toBe("1234.56"); expect(betragLesen("12,50 EUR")?.toFixed(2)).toBe("12.50"); }); it("versteht die englische Schreibweise", () => { expect(betragLesen("1,234.56")?.toFixed(2)).toBe("1234.56"); expect(betragLesen("$1234.56")?.toFixed(2)).toBe("1234.56"); }); it("liest auch Beträge ohne Trennzeichen", () => { expect(betragLesen("42")?.toFixed(2)).toBe("42.00"); expect(betragLesen("-42,50")?.toFixed(2)).toBe("-42.50"); }); it("gibt bei Unlesbarem null zurück — und nicht null Euro", () => { // Der Unterschied ist der ganze Punkt: null Euro ist eine Angabe, // unlesbar ist eine Rückfrage an den, der es eingetippt hat. expect(betragLesen("etwa zwanzig")).toBeNull(); expect(betragLesen("")).toBeNull(); expect(betragLesen("1,2,3")).toBeNull(); }); }); describe("Beträge schreiben", () => { it("setzt Tausenderpunkte und ein Komma", () => { expect(alsText("1234.56")).toBe("1.234,56 €"); expect(alsText("12.50")).toBe("12,50 €"); expect(alsText("1234567.89")).toBe("1.234.567,89 €"); }); it("verwendet das typografische Minus", () => { expect(alsText("-3.20")).toBe("−3,20 €"); }); it("macht aus fehlenden Angaben einen Gedankenstrich", () => { expect(alsText(null)).toBe("—"); expect(alsText(undefined)).toBe("—"); }); });