154 lines
5.1 KiB
TypeScript
154 lines
5.1 KiB
TypeScript
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("—");
|
||
});
|
||
});
|