import { describe, expect, it } from "vitest"; import { erinnerungsstufe, fensterGrenzen, innerhalbPlanungshorizont, naechsteFaelligkeit, protokollNummer, protokollNummerLesen, terminFenster, } from "@/lib/wartung/fristen"; /** * Fristenlogik der Wartung. * * Alle Tests rechnen mit einem festen "heute", nicht mit der Systemuhr. Sonst * wären sie am 31. Dezember rot und am 1. Januar wieder grün — und niemand * wüsste, warum. */ /** Ein Kalendertag als UTC-Mitternacht, so wie er aus einer DATE-Spalte kommt. */ function tag(jahr: number, monat: number, tagImMonat: number): Date { return new Date(Date.UTC(jahr, monat - 1, tagImMonat)); } /** Mittags, damit die Zeitzonenumrechnung nicht zufällig aufgeht. */ const HEUTE = new Date("2026-06-15T12:00:00.000Z"); describe("terminFenster", () => { it("erkennt Überfälligkeit ab dem Vortag", () => { expect(terminFenster(tag(2026, 6, 14), HEUTE)).toBe("ueberfaellig"); expect(terminFenster(tag(2026, 1, 1), HEUTE)).toBe("ueberfaellig"); }); it("zählt den heutigen Tag zur laufenden Woche, nicht zur Überfälligkeit", () => { expect(terminFenster(tag(2026, 6, 15), HEUTE)).toBe("diese_woche"); }); it("staffelt Woche, Monat und Quartal", () => { expect(terminFenster(tag(2026, 6, 22), HEUTE)).toBe("diese_woche"); expect(terminFenster(tag(2026, 6, 23), HEUTE)).toBe("dieser_monat"); expect(terminFenster(tag(2026, 7, 15), HEUTE)).toBe("dieser_monat"); expect(terminFenster(tag(2026, 7, 16), HEUTE)).toBe("naechste_90_tage"); expect(terminFenster(tag(2026, 9, 13), HEUTE)).toBe("naechste_90_tage"); expect(terminFenster(tag(2026, 9, 14), HEUTE)).toBe("spaeter"); }); }); describe("fensterGrenzen", () => { it("liefert die Grenzen als UTC-Kalendertage", () => { const grenzen = fensterGrenzen(HEUTE); expect(grenzen.heute.toISOString()).toBe("2026-06-15T00:00:00.000Z"); expect(grenzen.inSiebenTagen.toISOString()).toBe("2026-06-22T00:00:00.000Z"); expect(grenzen.inDreissigTagen.toISOString()).toBe("2026-07-15T00:00:00.000Z"); expect(grenzen.inNeunzigTagen.toISOString()).toBe("2026-09-13T00:00:00.000Z"); }); it("rechnet auch kurz nach Mitternacht Berliner Zeit den richtigen Tag", () => { // 00:30 Uhr in Berlin ist nach UTC noch der Vortag. const kurzNachMitternacht = new Date("2026-06-15T22:30:00.000Z"); expect(fensterGrenzen(kurzNachMitternacht).heute.toISOString()).toBe( "2026-06-16T00:00:00.000Z", ); }); }); describe("naechsteFaelligkeit", () => { it("rechnet ab der letzten Prüfung", () => { const faellig = naechsteFaelligkeit({ letztePruefung: tag(2026, 3, 10), intervallMonate: 12, }); expect(faellig?.toISOString()).toBe("2027-03-10T00:00:00.000Z"); }); it("nimmt ersatzweise die Indienststellung", () => { const faellig = naechsteFaelligkeit({ indienststellung: tag(2024, 5, 1), intervallMonate: 6, }); expect(faellig?.toISOString()).toBe("2024-11-01T00:00:00.000Z"); }); it("klemmt überzählige Tage auf das Monatsende", () => { const faellig = naechsteFaelligkeit({ letztePruefung: tag(2026, 1, 31), intervallMonate: 1, }); expect(faellig?.toISOString()).toBe("2026-02-28T00:00:00.000Z"); }); it("gibt ohne Anhaltspunkt oder Intervall nichts zurück", () => { expect(naechsteFaelligkeit({ intervallMonate: 12 })).toBeNull(); expect( naechsteFaelligkeit({ letztePruefung: tag(2026, 1, 1), intervallMonate: 0 }), ).toBeNull(); }); }); describe("erinnerungsstufe", () => { it("meldet Überfälligkeit", () => { expect(erinnerungsstufe(tag(2026, 6, 14), 30, HEUTE)).toBe("UEBERFAELLIG"); }); it("meldet den Fälligkeitstag selbst", () => { expect(erinnerungsstufe(tag(2026, 6, 15), 30, HEUTE)).toBe("T_0"); }); it("staffelt die Vorwarnungen", () => { expect(erinnerungsstufe(tag(2026, 6, 20), 30, HEUTE)).toBe("T_7"); expect(erinnerungsstufe(tag(2026, 7, 5), 30, HEUTE)).toBe("T_30"); expect(erinnerungsstufe(tag(2026, 8, 1), 30, HEUTE)).toBeNull(); }); it("richtet die erste Stufe nach der Vorwarnzeit der Vorlage", () => { // 60 Tage Vorwarnzeit: ein Termin in 45 Tagen wird schon erinnert. expect(erinnerungsstufe(tag(2026, 7, 30), 60, HEUTE)).toBe("T_30"); // Mit der Standardvorwarnzeit dagegen noch nicht. expect(erinnerungsstufe(tag(2026, 7, 30), 30, HEUTE)).toBeNull(); }); it("erinnert auch bei spät angelegten Terminen — kleiner-gleich statt genau", () => { // Der Tag T-30 ist längst vorbei, die erste Erinnerung darf trotzdem raus. expect(erinnerungsstufe(tag(2026, 6, 30), 30, HEUTE)).toBe("T_30"); }); }); describe("innerhalbPlanungshorizont", () => { it("plant nur, was in den Horizont fällt", () => { expect(innerhalbPlanungshorizont(tag(2026, 9, 1), 180, HEUTE)).toBe(true); expect(innerhalbPlanungshorizont(tag(2027, 6, 1), 180, HEUTE)).toBe(false); }); it("bezieht Vergangenes immer ein — Überfälliges muss geplant bleiben", () => { expect(innerhalbPlanungshorizont(tag(2025, 1, 1), 180, HEUTE)).toBe(true); }); }); describe("Protokollnummern", () => { it("füllt die laufende Nummer auf vier Stellen", () => { expect(protokollNummer(2026, 1)).toBe("2026-0001"); expect(protokollNummer(2026, 137)).toBe("2026-0137"); }); it("liest die Nummer wieder auseinander", () => { expect(protokollNummerLesen("2026-0042")).toEqual({ jahr: 2026, laufend: 42 }); expect(protokollNummerLesen("kaputt")).toBeNull(); expect(protokollNummerLesen(null)).toBeNull(); }); });