import { describe, expect, it } from "vitest"; import { inventarnummerAusScan } from "@/lib/stammdaten/qr-parser"; import { abgeleiteterGeraetStatus, effektiverWert, fristzustand, lebensdauerEnde, naechstePruefung, qualifikationGueltigBis, tageBis, } from "@/lib/stammdaten/fristen"; /** * Tests für die QR-Route und die Fristenrechnung. * * Beides entscheidet darüber, ob ein Gerät als einsatzbereit gilt. Ein Fehler * in der Fristenrechnung ist gefährlicher als ein Fehler in der Oberfläche: * Er fällt niemandem auf, bis die Unfallkasse fragt. * * Alle Zeitangaben werden mit einem festen "heute" gerechnet, damit die Tests * nicht von der Uhr abhängen und um Mitternacht anders ausgehen. */ const HEUTE = new Date(Date.UTC(2026, 8, 3, 10, 0, 0)); // 3. September 2026 describe("QR-Scan auswerten", () => { it("liest die Inventarnummer aus der eigenen URL", () => { expect(inventarnummerAusScan("https://florian.beispielwehr.de/g/INV-0042")).toBe("INV-0042"); }); it("liest sie auch aus einem bloßen Pfad", () => { expect(inventarnummerAusScan("/g/INV-0042")).toBe("INV-0042"); }); it("dekodiert Sonderzeichen aus der URL", () => { expect(inventarnummerAusScan("https://florian.local/g/INV%2F12")).toBe("INV/12"); }); it("nimmt eine blanke Nummer an — alte Etiketten sollen weiter gehen", () => { expect(inventarnummerAusScan("INV-0042")).toBe("INV-0042"); expect(inventarnummerAusScan(" 12345 ")).toBe("12345"); }); it("weist fremde URLs ab", () => { expect(inventarnummerAusScan("https://www.draeger.com/produkt/pa94")).toBeNull(); expect(inventarnummerAusScan("http://beispiel.de")).toBeNull(); }); it("weist leere und unsinnige Eingaben ab", () => { expect(inventarnummerAusScan("")).toBeNull(); expect(inventarnummerAusScan(" ")).toBeNull(); expect(inventarnummerAusScan("WIFI:S=Geraetehaus;T=WPA;P=geheim;;")).toBeNull(); }); it("weist übermäßig lange Zeichenketten ab", () => { expect(inventarnummerAusScan("A".repeat(200))).toBeNull(); }); }); describe("Nächste Prüfung", () => { it("rechnet ab der letzten Prüfung", () => { const termin = naechstePruefung({ letztePruefung: new Date(Date.UTC(2026, 2, 1)), intervallMonate: 12, }); expect(termin?.getUTCFullYear()).toBe(2027); expect(termin?.getUTCMonth()).toBe(2); }); it("nimmt ersatzweise die Indienststellung, wenn noch nie geprüft wurde", () => { const termin = naechstePruefung({ indienststellung: new Date(Date.UTC(2026, 0, 15)), intervallMonate: 6, }); expect(termin?.getUTCMonth()).toBe(6); }); it("bevorzugt die letzte Prüfung vor der Indienststellung", () => { const termin = naechstePruefung({ letztePruefung: new Date(Date.UTC(2026, 5, 1)), indienststellung: new Date(Date.UTC(2020, 0, 1)), intervallMonate: 12, }); expect(termin?.getUTCFullYear()).toBe(2027); }); it("gibt ohne Intervall keinen Termin zurück — das ist kein Fehler", () => { expect(naechstePruefung({ letztePruefung: HEUTE, intervallMonate: null })).toBeNull(); expect(naechstePruefung({ letztePruefung: HEUTE, intervallMonate: 0 })).toBeNull(); }); it("gibt ohne jeden Anhaltspunkt keinen Termin zurück", () => { expect(naechstePruefung({ intervallMonate: 12 })).toBeNull(); }); }); describe("Lebensdauer", () => { it("rechnet ab Indienststellung, wenn das die Vorgabe ist", () => { const ende = lebensdauerEnde({ herstellungsdatum: new Date(Date.UTC(2010, 0, 1)), indienststellung: new Date(Date.UTC(2012, 0, 1)), lebensdauerJahre: 10, bezug: "INDIENSTSTELLUNG", }); expect(ende?.getUTCFullYear()).toBe(2022); }); it("rechnet ab Herstellung, wenn die Vorschrift das verlangt", () => { // Seile und Druckbehälter altern ab Herstellung, nicht ab Einbau. const ende = lebensdauerEnde({ herstellungsdatum: new Date(Date.UTC(2010, 0, 1)), indienststellung: new Date(Date.UTC(2012, 0, 1)), lebensdauerJahre: 10, bezug: "HERSTELLUNG", }); expect(ende?.getUTCFullYear()).toBe(2020); }); it("weicht auf das andere Datum aus, wenn eines fehlt", () => { const ende = lebensdauerEnde({ indienststellung: new Date(Date.UTC(2015, 0, 1)), lebensdauerJahre: 5, bezug: "HERSTELLUNG", }); expect(ende?.getUTCFullYear()).toBe(2020); }); it("gibt ohne Lebensdauer nichts zurück", () => { expect( lebensdauerEnde({ indienststellung: HEUTE, lebensdauerJahre: null }), ).toBeNull(); }); }); describe("Fristzustand", () => { it("erkennt überfällig, fällig und in Ordnung", () => { expect(fristzustand(new Date(Date.UTC(2026, 7, 1)), 30, HEUTE)).toBe("ueberfaellig"); expect(fristzustand(new Date(Date.UTC(2026, 8, 3)), 30, HEUTE)).toBe("faellig"); expect(fristzustand(new Date(Date.UTC(2026, 8, 20)), 30, HEUTE)).toBe("bald_faellig"); expect(fristzustand(new Date(Date.UTC(2027, 0, 1)), 30, HEUTE)).toBe("in_ordnung"); }); it("achtet auf die Vorwarnzeit", () => { const in60Tagen = new Date(Date.UTC(2026, 10, 2)); expect(fristzustand(in60Tagen, 30, HEUTE)).toBe("in_ordnung"); expect(fristzustand(in60Tagen, 90, HEUTE)).toBe("bald_faellig"); }); it("meldet ohne Frist genau das", () => { expect(fristzustand(null, 30, HEUTE)).toBe("ohne_frist"); expect(fristzustand(undefined, 30, HEUTE)).toBe("ohne_frist"); }); it("zählt Tage in beide Richtungen", () => { expect(tageBis(new Date(Date.UTC(2026, 8, 13)), HEUTE)).toBe(10); expect(tageBis(new Date(Date.UTC(2026, 7, 24)), HEUTE)).toBe(-10); expect(tageBis(null, HEUTE)).toBeNull(); }); }); describe("Abgeleiteter Gerätestatus", () => { it("setzt auf Prüfung fällig, wenn der Termin vorbei ist", () => { expect( abgeleiteterGeraetStatus({ aktuellerStatus: "EINSATZBEREIT", naechstePruefung: new Date(Date.UTC(2026, 7, 1)), heute: HEUTE, }), ).toBe("PRUEFUNG_FAELLIG"); }); it("setzt zurück auf einsatzbereit, wenn der Termin wieder in der Zukunft liegt", () => { expect( abgeleiteterGeraetStatus({ aktuellerStatus: "PRUEFUNG_FAELLIG", naechstePruefung: new Date(Date.UTC(2027, 0, 1)), heute: HEUTE, }), ).toBe("EINSATZBEREIT"); }); it("sperrt ein Gerät mit abgelaufener Lebensdauer", () => { expect( abgeleiteterGeraetStatus({ aktuellerStatus: "EINSATZBEREIT", naechstePruefung: new Date(Date.UTC(2027, 0, 1)), lebensdauerBis: new Date(Date.UTC(2026, 0, 1)), heute: HEUTE, }), ).toBe("GESPERRT"); }); it("lässt die Lebensdauer schwerer wiegen als eine fällige Prüfung", () => { expect( abgeleiteterGeraetStatus({ aktuellerStatus: "EINSATZBEREIT", naechstePruefung: new Date(Date.UTC(2026, 0, 1)), lebensdauerBis: new Date(Date.UTC(2026, 0, 1)), heute: HEUTE, }), ).toBe("GESPERRT"); }); it("fasst von Hand gesetzte Zustände nicht an", () => { for (const status of ["GESPERRT", "AUSGESONDERT", "IN_REPARATUR"] as const) { expect( abgeleiteterGeraetStatus({ aktuellerStatus: status, naechstePruefung: new Date(Date.UTC(2027, 0, 1)), heute: HEUTE, }), status, ).toBe(status); } }); it("lässt ein Gerät ohne Fristen einsatzbereit", () => { expect( abgeleiteterGeraetStatus({ aktuellerStatus: "EINSATZBEREIT", heute: HEUTE }), ).toBe("EINSATZBEREIT"); }); }); describe("Vorgabe des Gerätetyps", () => { it("lässt den Wert am Gerät gewinnen", () => { expect(effektiverWert(6, 12)).toBe(6); }); it("greift auf den Typ zurück, wenn am Gerät nichts steht", () => { expect(effektiverWert(null, 12)).toBe(12); expect(effektiverWert(undefined, 12)).toBe(12); }); it("gibt null zurück, wenn beide leer sind", () => { expect(effektiverWert(null, null)).toBeNull(); }); it("behandelt die 0 als gesetzten Wert, nicht als fehlend", () => { expect(effektiverWert(0, 12)).toBe(0); }); }); describe("Gültigkeit einer Qualifikation", () => { it("rechnet die Gültigkeitsdauer auf das Erwerbsdatum", () => { const bis = qualifikationGueltigBis(new Date(Date.UTC(2026, 2, 1)), 36); expect(bis?.getUTCFullYear()).toBe(2029); expect(bis?.getUTCMonth()).toBe(2); }); it("gibt bei unbefristeten Qualifikationen kein Ablaufdatum zurück", () => { expect(qualifikationGueltigBis(HEUTE, null)).toBeNull(); expect(qualifikationGueltigBis(HEUTE, 0)).toBeNull(); }); });