Phase 9: Kassenverwaltung (#14)
This commit was merged in pull request #14.
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
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("—");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user