Files

201 lines
6.4 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
STANDARDKATEGORIEN,
aehnlichkeit,
kategorieVorschlagen,
woerter,
} from "@/lib/kasse/vorschlag";
/**
* Der Kategorievorschlag.
*
* Sein Nutzen liegt nicht in den gesparten Sekunden, sondern darin, dass
* dieselbe Sache immer unter derselben Kategorie landet — erst das macht die
* Jahresauswertung brauchbar. Zwei Kategorien für Diesel, weil einmal
* „Kraftstoff" und einmal „Fahrzeuge" gewählt wurde, sind in der
* Jahreshauptversammlung nicht zu erklären.
*
* Ebenso wichtig ist, wann er schweigt: ein falscher Vorschlag wird
* übernommen, ohne gelesen zu werden. Deshalb die Schwelle bei der Hälfte
* gemeinsamer Wörter — und deshalb prüft dieser Test beide Seiten.
*/
const KATEGORIEN = [
{ id: "k-kraftstoff", name: "Kraftstoff", art: "AUSGABE" as const },
{ id: "k-reparatur", name: "Reparatur", art: "AUSGABE" as const },
{ id: "k-strom", name: "Wasser und Strom", art: "AUSGABE" as const },
{ id: "k-zuschuss", name: "Zuschüsse", art: "EINNAHME" as const },
{ id: "k-beitrag", name: "Mitgliedsbeiträge", art: "EINNAHME" as const },
];
describe("Wörter", () => {
it("wirft Füllwörter, kurze Wörter und reine Zahlen weg", () => {
expect(woerter("Rechnung Nr. 4711 vom 12.03. für die Feuerwehr GmbH")).toEqual([]);
});
it("behält, was unterscheidet", () => {
expect(woerter("Tankstelle Musterhausen Diesel LF 20")).toEqual([
"tankstelle",
"musterhausen",
"diesel",
]);
});
it("lässt Umlaute stehen", () => {
expect(woerter("Prüfung Drehleiter")).toEqual(["prüfung", "drehleiter"]);
});
});
describe("Ähnlichkeit", () => {
it("misst am kürzeren der beiden Texte", () => {
// "stadtwerke musterhausen" steckt vollständig in beiden.
const wert = aehnlichkeit(
"Stadtwerke Musterhausen Abschlag Strom",
"Stadtwerke Musterhausen Jahresabrechnung Strom und Wasser",
);
expect(wert).toBeGreaterThanOrEqual(0.5);
});
it("ergibt null bei nichts Gemeinsamem", () => {
expect(aehnlichkeit("Diesel Tankstelle", "Einsatzjacken Beschaffung")).toBe(0);
});
it("ergibt null, wenn nach dem Aussieben nichts übrig ist", () => {
expect(aehnlichkeit("Rechnung vom", "Rechnung Nr. 12")).toBe(0);
});
});
describe("Vorschlag aus dem eigenen Bestand", () => {
const vorbilder = [
{
kategorieId: "k-kraftstoff",
kategorieName: "Kraftstoff",
verwendungszweck: "Tankstelle Musterhausen Diesel LF 20 Sammelrechnung",
art: "AUSGABE" as const,
},
{
kategorieId: "k-reparatur",
kategorieName: "Reparatur",
verwendungszweck: "Werkstatt Müller Kupplung MTW",
art: "AUSGABE" as const,
},
];
it("nimmt die Kategorie, unter der dasselbe zuletzt gebucht wurde", () => {
const vorschlag = kategorieVorschlagen(
"Tankstelle Musterhausen Diesel MTW",
"AUSGABE",
vorbilder,
KATEGORIEN,
);
expect(vorschlag?.kategorieId).toBe("k-kraftstoff");
expect(vorschlag?.grund).toMatch(/zuletzt so gebucht/);
});
it("geht dem Bestand vor den Stichwörtern nach — die Wehr kennt sich selbst am besten", () => {
const eigen = [
{
kategorieId: "k-strom",
kategorieName: "Wasser und Strom",
// Diese Wehr bucht Diesel unter Wasser und Strom. Merkwürdig, aber
// ihre Entscheidung — und sie soll dabei bleiben dürfen.
verwendungszweck: "Tankstelle Musterhausen Diesel LF 20",
art: "AUSGABE" as const,
},
];
const vorschlag = kategorieVorschlagen(
"Tankstelle Musterhausen Diesel LF 20",
"AUSGABE",
eigen,
KATEGORIEN,
);
expect(vorschlag?.kategorieId).toBe("k-strom");
});
it("schweigt bei zufälliger Übereinstimmung, statt zu raten", () => {
const vorschlag = kategorieVorschlagen(
"Möbelhaus Sonnleitner Tischbeschaffung Schulungsraum",
"AUSGABE",
vorbilder,
KATEGORIEN,
);
expect(vorschlag).toBeNull();
});
it("mischt Einnahmen und Ausgaben nicht", () => {
const vorschlag = kategorieVorschlagen(
"Tankstelle Musterhausen Diesel LF 20 Sammelrechnung",
"EINNAHME",
vorbilder,
KATEGORIEN,
);
// Der Vorgänger ist eine Ausgabe und kommt deshalb nicht in Frage; ein
// Stichwort für Einnahmen trifft hier ebenso wenig.
expect(vorschlag?.kategorieId).not.toBe("k-kraftstoff");
});
});
describe("Vorschlag aus Stichwörtern", () => {
it("greift, solange es keinen Bestand gibt", () => {
const vorschlag = kategorieVorschlagen("Diesel für das LF 20", "AUSGABE", [], KATEGORIEN);
expect(vorschlag?.kategorieId).toBe("k-kraftstoff");
expect(vorschlag?.grund).toMatch(/Verwendungszweck/);
// Niedrigere Sicherheit als ein Treffer aus dem Bestand: ein Stichwort
// weiß nichts über die Gepflogenheiten dieser Wehr.
expect(vorschlag?.sicherheit).toBeLessThan(1);
});
it("erkennt den Gemeindezuschuss als Einnahme", () => {
const vorschlag = kategorieVorschlagen(
"Zuschuss der Gemeinde für den laufenden Betrieb",
"EINNAHME",
[],
KATEGORIEN,
);
expect(vorschlag?.kategorieId).toBe("k-zuschuss");
});
it("schlägt nichts vor, wenn es die Kategorie in dieser Wehr nicht gibt", () => {
const vorschlag = kategorieVorschlagen("Neue Einsatzjacken", "AUSGABE", [], KATEGORIEN);
expect(vorschlag).toBeNull();
});
it("schweigt bei leerem Verwendungszweck", () => {
expect(kategorieVorschlagen(" ", "AUSGABE", [], KATEGORIEN)).toBeNull();
});
});
describe("Standardkontenrahmen", () => {
it("verweist nur auf Oberkategorien, die es auch gibt", () => {
const namen = new Set(STANDARDKATEGORIEN.map((eintrag) => eintrag.name));
for (const eintrag of STANDARDKATEGORIEN) {
if (eintrag.oberkategorie) {
expect(namen.has(eintrag.oberkategorie)).toBe(true);
}
}
});
it("gibt jeden Namen nur einmal — die Datenbank verlangt es so", () => {
const namen = STANDARDKATEGORIEN.map((eintrag) => eintrag.name);
expect(new Set(namen).size).toBe(namen.length);
});
it("stellt jede Unterkategorie in eine Oberkategorie derselben Richtung", () => {
const nachName = new Map(STANDARDKATEGORIEN.map((eintrag) => [eintrag.name, eintrag]));
for (const eintrag of STANDARDKATEGORIEN) {
if (eintrag.oberkategorie) {
expect(nachName.get(eintrag.oberkategorie)?.art).toBe(eintrag.art);
}
}
});
});