import { describe, expect, it } from 'vitest' import { fristLage, fristStufe, fristenEinordnen, heimkostenlageBerechnen, nachforderungenAuswerten, tagAusDatenbank, vermoegenBerechnen, verfallwarnung, wartendeEinordnen, WARTEZEIT_DRINGEND_TAGE, type FristRoh, type HeimkostenmonatRoh, type NachforderungRoh, type PostenRoh, type WartendRoh, } from '@/lib/dashboard/bewertung' /** * Das Dashboard entscheidet, was heute dringend ist. Diese Tests * beschreiben, was fachlich gelten soll - nicht, welche Funktion gerufen * wird. */ /** Mittwoch, 19.08.2026. Die Woche endet am Sonntag, dem 23.08. */ const HEUTE = new Date(2026, 7, 19) /** Ein @db.Date kommt als UTC-Mitternacht aus der Datenbank. */ function dbTag(text: string): Date { return new Date(`${text}T00:00:00.000Z`) } function frist(teil: Partial & { datum: Date }): FristRoh { return { id: 'f1', titel: 'Irgendeine Frist', typ: 'GESETZLICH', rechtsgrundlage: null, ausschlussfrist: false, vorgangId: null, ...teil, } } describe('Fristen-Ampel', () => { it('meldet ein Datum in der Vergangenheit als überfällig', () => { expect(fristStufe(dbTag('2026-08-10'), HEUTE)).toBe('rot') }) it('färbt orange, was bis zum Sonntag dieser Woche fällig wird', () => { expect(fristStufe(dbTag('2026-08-19'), HEUTE)).toBe('orange') expect(fristStufe(dbTag('2026-08-23'), HEUTE)).toBe('orange') }) it('färbt gelb, was noch in diesem Monat fällig wird', () => { expect(fristStufe(dbTag('2026-08-24'), HEUTE)).toBe('gelb') expect(fristStufe(dbTag('2026-08-31'), HEUTE)).toBe('gelb') }) it('lässt alles Spätere grün', () => { expect(fristStufe(dbTag('2026-09-01'), HEUTE)).toBe('gruen') }) it('stellt überfällige Fristen nach oben und nennt die Tage', () => { const daten = fristenEinordnen( [ frist({ id: 'kommend', datum: dbTag('2026-08-28') }), frist({ id: 'alt', datum: dbTag('2026-07-30') }), frist({ id: 'aeltest', datum: dbTag('2026-06-01') }), ], HEUTE, ) expect(daten.ueberfaellig.map((e) => e.id)).toEqual(['aeltest', 'alt']) expect(daten.ueberfaellig[0]?.tage).toBe(-79) expect(daten.diesenMonat.map((e) => e.id)).toEqual(['kommend']) }) it('zählt Fristen jenseits des Monats gesondert, statt sie zu verschweigen', () => { const daten = fristenEinordnen( [frist({ id: 'spaet', datum: dbTag('2027-03-01') })], HEUTE, ) expect(daten.weitereOffen).toBe(1) expect(daten.gesamtOffen).toBe(1) expect(daten.ueberfaellig).toHaveLength(0) }) it('hebt Ausschlussfristen unabhängig vom Fälligkeitstopf hervor', () => { const daten = fristenEinordnen( [ frist({ id: 'normal', datum: dbTag('2026-08-20') }), frist({ id: 'pauschale', titel: 'Aufwandspauschale 2026', datum: dbTag('2026-12-31'), ausschlussfrist: true, rechtsgrundlage: '§ 1878 BGB', }), ], HEUTE, ) expect(daten.ausschlussfristen.map((e) => e.id)).toEqual(['pauschale']) }) it('warnt bei einer Ausschlussfrist ausdrücklich vor dem ersatzlosen Verfall', () => { const text = verfallwarnung({ tage: 12, rechtsgrundlage: '§ 1878 BGB' }) expect(text).toContain('§ 1878 BGB') expect(text).toContain('ersatzlos weg') }) it('sagt bei einer abgelaufenen Ausschlussfrist, wie lange sie schon abgelaufen ist', () => { const text = verfallwarnung({ tage: -3, rechtsgrundlage: null }) expect(text).toContain('seit 3 Tagen abgelaufen') expect(text).toContain('ersatzlos weg') }) it('schreibt die Lage in Worten, damit Farbe nie die einzige Information ist', () => { expect(fristLage(-5)).toBe('seit 5 Tagen überfällig') expect(fristLage(-1)).toBe('seit gestern überfällig') expect(fristLage(0)).toBe('heute fällig') expect(fristLage(1)).toBe('morgen fällig') expect(fristLage(9)).toBe('in 9 Tagen fällig') }) it('verschiebt ein Datum ohne Uhrzeit nicht über die Zeitzone hinweg', () => { const tag = tagAusDatenbank(dbTag('2026-08-31')) expect(tag.getDate()).toBe(31) expect(tag.getMonth()).toBe(7) }) }) describe('Wartet auf andere', () => { function vorgang(teil: Partial): WartendRoh { return { id: 'v1', titel: 'Antrag Hilfe zur Pflege', kategorie: 'SOZIALHILFE', gegenstelle: 'Bezirk Oberfranken', aktenzeichen: null, naechsterSchritt: null, letzterKontaktAm: null, erstelltAm: new Date(2026, 7, 1), ...teil, } } it('sortiert die längste Wartezeit nach oben', () => { const daten = wartendeEinordnen( [ vorgang({ id: 'kurz', letzterKontaktAm: dbTag('2026-08-15') }), vorgang({ id: 'lang', letzterKontaktAm: dbTag('2026-06-01') }), ], HEUTE, ) expect(daten.eintraege.map((e) => e.id)).toEqual(['lang', 'kurz']) expect(daten.eintraege[0]?.tageOhneRueckmeldung).toBe(79) }) it(`hebt ab ${WARTEZEIT_DRINGEND_TAGE} Tagen ohne Rückmeldung hervor`, () => { const grenze = wartendeEinordnen( [vorgang({ letzterKontaktAm: dbTag('2026-07-29') })], // genau 21 Tage HEUTE, ) expect(grenze.eintraege[0]?.tageOhneRueckmeldung).toBe(WARTEZEIT_DRINGEND_TAGE) expect(grenze.eintraege[0]?.dringend).toBe(true) expect(grenze.dringende).toBe(1) const knapp = wartendeEinordnen([vorgang({ letzterKontaktAm: dbTag('2026-07-30') })], HEUTE) expect(knapp.eintraege[0]?.dringend).toBe(false) }) it('rechnet ab dem Anlegen, wenn nie ein Kontakt vermerkt wurde', () => { const daten = wartendeEinordnen([vorgang({ letzterKontaktAm: null })], HEUTE) expect(daten.eintraege[0]?.tageOhneRueckmeldung).toBe(18) expect(daten.eintraege[0]?.letzterKontaktAm).toBeNull() }) }) describe('Nachforderungen des Trägers', () => { function zeile(teil: Partial): NachforderungRoh { return { id: 'n1', bezeichnung: 'Kontoauszüge der letzten 12 Monate', status: 'OFFEN', fristBis: null, reihenfolge: 0, antragId: 'a1', antragTraeger: 'Bezirk Oberfranken', antragAktenzeichen: 'HzP 12345', ...teil, } } it('zählt übersandte, anerkannte und begründet fehlende Unterlagen als erledigt', () => { const daten = nachforderungenAuswerten( [ zeile({ id: '1', status: 'UEBERSANDT' }), zeile({ id: '2', status: 'ANERKANNT' }), zeile({ id: '3', status: 'NICHT_VORHANDEN' }), zeile({ id: '4', status: 'OFFEN' }), zeile({ id: '5', status: 'IN_BESCHAFFUNG' }), ], HEUTE, ) expect(daten.gesamt).toBe(5) expect(daten.erledigt).toBe(3) expect(daten.offen.map((o) => o.id)).toEqual(['4', '5']) }) it('reiht die dringendste Frist zuoberst und meldet abgelaufene gesondert', () => { const daten = nachforderungenAuswerten( [ zeile({ id: 'ohne', fristBis: null }), zeile({ id: 'spaet', fristBis: dbTag('2026-09-30') }), zeile({ id: 'abgelaufen', fristBis: dbTag('2026-08-10') }), ], HEUTE, ) expect(daten.offen.map((o) => o.id)).toEqual(['abgelaufen', 'spaet', 'ohne']) expect(daten.offen[0]?.tageBisFrist).toBe(-9) expect(daten.ueberfaellig).toBe(1) }) it('nimmt Träger und Aktenzeichen aus dem Antrag mit', () => { const daten = nachforderungenAuswerten([zeile({})], HEUTE) expect(daten.traeger).toBe('Bezirk Oberfranken') expect(daten.aktenzeichen).toBe('HzP 12345') expect(daten.antragId).toBe('a1') }) it('kommt mit einer leeren Liste zurecht', () => { const daten = nachforderungenAuswerten([], HEUTE) expect(daten.gesamt).toBe(0) expect(daten.traeger).toBeNull() }) }) describe('Vermögen zum letzten Stichtag', () => { function posten(teil: Partial): PostenRoh { return { id: 'p1', art: 'KONTO', wertstaende: [], ...teil } } it('nimmt je Posten den jüngsten Wertstand und zieht Verbindlichkeiten ab', () => { const lage = vermoegenBerechnen([ posten({ id: 'giro', art: 'KONTO', wertstaende: [ { stichtag: dbTag('2026-01-01'), wertCent: 100_000n }, { stichtag: dbTag('2026-07-01'), wertCent: 250_000n }, ], }), posten({ id: 'lebensversicherung', art: 'VERSICHERUNG', wertstaende: [{ stichtag: dbTag('2026-06-01'), wertCent: 1_248_000n }], }), posten({ id: 'heimrechnung', art: 'VERBINDLICHKEIT', wertstaende: [{ stichtag: dbTag('2026-07-01'), wertCent: 340_000n }], }), ]) expect(lage.aktivaCent).toBe(1_498_000n) expect(lage.verbindlichkeitenCent).toBe(340_000n) expect(lage.nettoCent).toBe(1_158_000n) }) it('weist den jüngsten tatsächlich verwendeten Stichtag aus', () => { const lage = vermoegenBerechnen([ posten({ wertstaende: [ { stichtag: dbTag('2026-03-15'), wertCent: 1n }, { stichtag: dbTag('2026-05-20'), wertCent: 2n }, ], }), ]) expect(lage.stichtag?.getDate()).toBe(20) expect(lage.stichtag?.getMonth()).toBe(4) }) it('zählt Posten ohne Wert gesondert, statt sie stillschweigend als 0 zu führen', () => { const lage = vermoegenBerechnen([ posten({ id: 'a', wertstaende: [{ stichtag: dbTag('2026-05-01'), wertCent: 500n }] }), posten({ id: 'b', wertstaende: [] }), posten({ id: 'c', wertstaende: [] }), ]) expect(lage.postenGesamt).toBe(3) expect(lage.postenOhneWert).toBe(2) expect(lage.nettoCent).toBe(500n) }) it('liefert bei einer frischen Installation keinen Stichtag', () => { const lage = vermoegenBerechnen([]) expect(lage.stichtag).toBeNull() expect(lage.nettoCent).toBe(0n) }) }) describe('Heimkosten und Deckungslücke', () => { function monat(teil: Partial): HeimkostenmonatRoh { return { jahr: 2026, monat: 7, pflegeEigenanteilCent: 0n, unterkunftVerpflegungCent: 0n, investitionskostenCent: 0n, ausbildungsumlageCent: 0n, zusatzleistungenCent: 0n, pflegekasseCent: 0n, leistungszuschlagCent: 0n, renteCent: 0n, sonstigeEinkuenfteCent: 0n, sozialhilfeCent: 0n, eigenmittelCent: 0n, ...teil, } } it('stellt die Kostenseite der Deckungsseite gegenüber', () => { const lage = heimkostenlageBerechnen( monat({ pflegeEigenanteilCent: 130_000n, unterkunftVerpflegungCent: 95_000n, investitionskostenCent: 45_000n, ausbildungsumlageCent: 5_000n, renteCent: 120_000n, sozialhilfeCent: 100_000n, }), ) expect(lage.gesamtkostenCent).toBe(275_000n) expect(lage.deckungCent).toBe(220_000n) expect(lage.deckungsluecke).toBe(55_000n) }) it('weist eine Überdeckung als negative Lücke aus', () => { const lage = heimkostenlageBerechnen( monat({ unterkunftVerpflegungCent: 100_000n, renteCent: 150_000n }), ) expect(lage.deckungsluecke).toBe(-50_000n) }) it('rechnet in Cent und nicht in Fließkomma', () => { const lage = heimkostenlageBerechnen( monat({ pflegeEigenanteilCent: 1n, renteCent: 0n }), ) expect(typeof lage.deckungsluecke).toBe('bigint') expect(lage.deckungsluecke).toBe(1n) }) })