import { describe, expect, it } from 'vitest' import { darfStatusWechseln, GENEHMIGUNGSKETTE, moeglicheStatusWechsel, naechsterStatus, RECHTSGRUNDLAGEN, stufeVon, } from '@/lib/vorgaenge/genehmigungskette' import { VORLAGEN, vorlageAnwenden, vorlageFinden } from '@/lib/vorgaenge/vorlagen' import { zeitstrahlBauen } from '@/lib/vorgaenge/zeitstrahl' import { nachfassenFaellig, sachstandsanfrageErzeugen, } from '@/lib/vorgaenge/sachstandsanfrage' /** * Diese Tests beschreiben, was fachlich gelten soll - nicht, welche * Funktion aufgerufen wird. Keine Datenbank: alles hier ist reine Logik. */ // ============================================================================= describe('Genehmigungskette', () => { it('läuft in der vorgeschriebenen Reihenfolge vom Plan bis zum Vollzug', () => { expect(GENEHMIGUNGSKETTE).toEqual([ 'GEPLANT', 'MITGETEILT', 'BEANTRAGT', 'ANGEHOERT', 'ERTEILT', 'RECHTSKRAEFTIG', 'VOLLZOGEN', ]) }) it('lässt jeweils genau eine Stufe vorwärts zu', () => { for (let i = 0; i < GENEHMIGUNGSKETTE.length - 1; i++) { const von = GENEHMIGUNGSKETTE[i]! const nach = GENEHMIGUNGSKETTE[i + 1]! // Der letzte Schritt braucht den Beschluss - der wird unten geprüft. const beschluss = nach === 'VOLLZOGEN' ? 'dok-1' : null expect(darfStatusWechseln({ status: von, beschlussDokumentId: beschluss }, nach).erlaubt).toBe( true, ) expect(naechsterStatus(von)).toBe(nach) } }) it('verweigert das Überspringen von Stufen und nennt die übersprungenen', () => { const pruefung = darfStatusWechseln( { status: 'BEANTRAGT', beschlussDokumentId: 'dok-1' }, 'VOLLZOGEN', ) expect(pruefung.erlaubt).toBe(false) expect(pruefung.grund).toContain('Anhörung erfolgt') expect(pruefung.grund).toContain('§ 1858 BGB') }) it('erlaubt eine Stufe zurück als Korrektur, aber nicht zwei', () => { expect(darfStatusWechseln({ status: 'ERTEILT' }, 'ANGEHOERT').erlaubt).toBe(true) expect(darfStatusWechseln({ status: 'ERTEILT' }, 'BEANTRAGT').erlaubt).toBe(false) }) // --- die abnahmerelevante Regel ------------------------------------------- it('sperrt den Vollzug, solange kein Beschlussdokument hinterlegt ist', () => { const ohneBeschluss = darfStatusWechseln( { status: 'RECHTSKRAEFTIG', beschlussDokumentId: null }, 'VOLLZOGEN', ) expect(ohneBeschluss.erlaubt).toBe(false) expect(ohneBeschluss.grund).toContain('Beschluss') const mitBeschluss = darfStatusWechseln( { status: 'RECHTSKRAEFTIG', beschlussDokumentId: 'dok-42' }, 'VOLLZOGEN', ) expect(mitBeschluss.erlaubt).toBe(true) }) it('bietet den Vollzug ohne Beschluss gar nicht erst zur Auswahl an', () => { expect(moeglicheStatusWechsel({ status: 'RECHTSKRAEFTIG', beschlussDokumentId: null })).not.toContain( 'VOLLZOGEN', ) expect(moeglicheStatusWechsel({ status: 'RECHTSKRAEFTIG', beschlussDokumentId: 'dok-42' })).toContain( 'VOLLZOGEN', ) }) it('behandelt den Vollzug als endgültig', () => { for (const status of [...GENEHMIGUNGSKETTE, 'VERSAGT'] as const) { if (status === 'VOLLZOGEN') continue expect( darfStatusWechseln({ status: 'VOLLZOGEN', beschlussDokumentId: 'dok-1' }, status).erlaubt, ).toBe(false) } }) it('lässt die Versagung nur zu, solange etwas anhängig und nichts rechtskräftig ist', () => { for (const status of ['MITGETEILT', 'BEANTRAGT', 'ANGEHOERT', 'ERTEILT'] as const) { expect(darfStatusWechseln({ status }, 'VERSAGT').erlaubt).toBe(true) } for (const status of ['GEPLANT', 'RECHTSKRAEFTIG'] as const) { expect(darfStatusWechseln({ status }, 'VERSAGT').erlaubt).toBe(false) } }) it('führt nach einer Versagung zurück auf den Anfang - und nur dorthin', () => { expect(darfStatusWechseln({ status: 'VERSAGT' }, 'GEPLANT').erlaubt).toBe(true) expect(darfStatusWechseln({ status: 'VERSAGT' }, 'BEANTRAGT').erlaubt).toBe(false) expect(stufeVon('VERSAGT')).toBe(-1) }) it('meldet einen Wechsel auf den bereits gesetzten Status als unnötig', () => { const pruefung = darfStatusWechseln({ status: 'BEANTRAGT' }, 'BEANTRAGT') expect(pruefung.erlaubt).toBe(false) expect(pruefung.grund).toContain('bereits') }) it('erklärt jede wählbare Rechtsgrundlage im Klartext', () => { const werte = RECHTSGRUNDLAGEN.map((r) => r.wert) for (const paragraph of [ '§ 1850 BGB', '§ 1833 BGB', '§ 1854 BGB', '§ 1851 BGB', '§ 1849 BGB', '§ 1859 BGB', ]) { expect(werte).toContain(paragraph) } for (const grundlage of RECHTSGRUNDLAGEN) { expect(grundlage.erklaerung.length).toBeGreaterThan(40) expect(grundlage.beispiele.length).toBeGreaterThan(0) } }) }) // ============================================================================= describe('Vorgangs-Vorlagen', () => { it('bringt die neun Vorlagen mit, auf die sich die anderen Module verlassen', () => { const schluessel = VORLAGEN.map((v) => v.schluessel) expect(schluessel).toEqual( expect.arrayContaining([ 'heimeinzug', 'antrag-hilfe-zur-pflege', 'wohnungsaufloesung', 'hausverkauf', 'darlehen-91-sgb-xii', 'versicherungsverwertung', 'pflegegrad-hoeherstufung', 'steuererklaerung', 'widerspruch-bescheid', ]), ) expect(new Set(schluessel).size).toBe(schluessel.length) }) it('gibt jeder Vorlage eine vorbefüllte Aufgabenliste', () => { for (const vorlage of VORLAGEN) { expect(vorlage.aufgaben.length).toBeGreaterThanOrEqual(4) expect(vorlage.titel.length).toBeGreaterThan(3) expect(vorlage.beschreibung.length).toBeGreaterThan(20) } }) it('nennt bei jeder genehmigungspflichtigen Vorlage die Vorschrift', () => { for (const vorlage of VORLAGEN.filter((v) => v.genehmigungspflichtig)) { expect(vorlage.rechtsgrundlage).toBeTruthy() } expect(vorlageFinden('wohnungsaufloesung')?.rechtsgrundlage).toBe('§ 1833 BGB') expect(vorlageFinden('hausverkauf')?.rechtsgrundlage).toBe('§ 1850 Nr. 1 BGB') expect(vorlageFinden('darlehen-91-sgb-xii')?.genehmigungspflichtig).toBe(true) }) it('stellt bei der Wohnungsauflösung die Mitteilung ans Gericht vor die Kündigung', () => { const aufgaben = vorlageFinden('wohnungsaufloesung')!.aufgaben.map((a) => a.titel) const mitteilung = aufgaben.findIndex((t) => t.includes('Mitteilung an das Betreuungsgericht')) const genehmigung = aufgaben.findIndex((t) => t.includes('Genehmigung des Gerichts abwarten')) const kuendigung = aufgaben.findIndex((t) => t.includes('Mietverhältnis kündigen')) expect(mitteilung).toBeGreaterThanOrEqual(0) expect(mitteilung).toBeLessThan(genehmigung) expect(genehmigung).toBeLessThan(kuendigung) }) it('holt beim Hausverkauf die Genehmigung vor die Beurkundung', () => { const aufgaben = vorlageFinden('hausverkauf')!.aufgaben.map((a) => a.titel) const einreichen = aufgaben.findIndex((t) => t.includes('Kaufvertragsentwurf beim')) const rechtskraft = aufgaben.findIndex((t) => t.includes('Rechtskraft prüfen')) const beurkundung = aufgaben.findIndex((t) => t.includes('Beurkundung')) expect(einreichen).toBeLessThan(rechtskraft) expect(rechtskraft).toBeLessThan(beurkundung) }) it('setzt den Widerspruch fristwahrend an den Anfang und legt die Monatsfrist an', () => { const vorlage = vorlageFinden('widerspruch-bescheid')! expect(vorlage.aufgaben[0]?.titel).toBe( 'Widerspruch fristwahrend einlegen, Begründung nachreichen', ) const frist = vorlage.fristen?.find((f) => f.rechtsgrundlage === '§ 84 SGG') expect(frist?.tageBisFaellig).toBe(30) }) it('rechnet die Fälligkeiten vom Anlegedatum aus, ohne Uhrzeit', () => { const ergebnis = vorlageAnwenden('heimeinzug', new Date(2026, 0, 15, 17, 42)) expect(ergebnis.aufgaben).toHaveLength(8) expect(ergebnis.aufgaben[0]?.titel).toBe('Heimvertrag prüfen und gegenzeichnen') // 15.01.2026 + 7 Tage = 22.01.2026, Mitternacht UTC (@db.Date). expect(ergebnis.aufgaben[0]?.faelligAm?.toISOString()).toBe('2026-01-22T00:00:00.000Z') expect(ergebnis.aufgaben[1]?.faelligAm?.toISOString()).toBe('2026-01-29T00:00:00.000Z') // Die Reihenfolge der Vorlage wird festgeschrieben. expect(ergebnis.aufgaben.map((a) => a.reihenfolge)).toEqual([0, 1, 2, 3, 4, 5, 6, 7]) expect(ergebnis.fristen).toHaveLength(1) expect(ergebnis.fristen[0]?.datum.toISOString()).toBe('2026-03-16T00:00:00.000Z') }) it('rechnet über einen Monatswechsel hinweg richtig', () => { const ergebnis = vorlageAnwenden('widerspruch-bescheid', new Date(2026, 1, 20)) // 20.02.2026 + 30 Tage = 22.03.2026 expect(ergebnis.fristen[0]?.datum.toISOString()).toBe('2026-03-22T00:00:00.000Z') }) it('weist einen erfundenen Vorlagenschlüssel zurück', () => { expect(() => vorlageAnwenden('gibt-es-nicht')).toThrowError(/Vorlage/) }) }) // ============================================================================= describe('Zeitstrahl der Fallakte', () => { const grundlage = { vorgang: { titel: 'Hausverkauf', erstelltAm: new Date('2026-01-10T09:00:00.000Z'), erledigtAm: null, }, } it('sortiert alle Quellen zusammen, neueste zuerst', () => { const eintraege = zeitstrahlBauen({ ...grundlage, dokumente: [ { id: 'd1', titel: 'Kaufvertragsentwurf', eingangAm: new Date('2026-03-01T00:00:00.000Z'), versandAm: null, erstelltAm: new Date('2026-03-05T00:00:00.000Z'), absender: 'Notariat Bayreuth', empfaenger: null, }, ], kontaktprotokolle: [ { id: 'k1', datum: new Date('2026-02-01T10:00:00.000Z'), art: 'TELEFONAT', gespraechspartner: 'Rechtspfleger Meier', mitBetreuter: false, inhalt: 'Ablauf der Genehmigung besprochen.', }, ], fristen: [ { id: 'f1', titel: 'Wertgutachten anfordern', datum: new Date('2026-04-01T00:00:00.000Z'), erledigtAm: null, ausschlussfrist: false, rechtsgrundlage: null, }, ], buchungen: [ { id: 'b1', datum: new Date('2026-05-01T00:00:00.000Z'), betragCent: 150_000, richtung: 'AUSGABE', gegenstelle: 'Gutachterbüro', verwendungszweck: 'Verkehrswertgutachten', }, ], }) expect(eintraege.map((e) => e.id)).toEqual([ 'BUCHUNG:b1', 'FRIST:f1', 'DOKUMENT:d1', 'KONTAKT:k1', 'ANGELEGT:vorgang', ]) }) it('kann auch von hinten gelesen werden', () => { const eintraege = zeitstrahlBauen( { ...grundlage, fristen: [ { id: 'f1', titel: 'Frist A', datum: new Date('2026-04-01T00:00:00.000Z'), erledigtAm: null, ausschlussfrist: true, rechtsgrundlage: '§ 1878 BGB', }, ], }, { reihenfolge: 'aelteste-zuerst' }, ) expect(eintraege[0]?.id).toBe('ANGELEGT:vorgang') expect(eintraege[1]?.titel).toBe('Ausschlussfrist: Frist A') }) it('nimmt beim Dokument den Eingang, sonst den Versand', () => { const eintraege = zeitstrahlBauen({ ...grundlage, dokumente: [ { id: 'd1', titel: 'Antrag', eingangAm: null, versandAm: new Date('2026-02-10T00:00:00.000Z'), erstelltAm: new Date('2026-02-20T00:00:00.000Z'), absender: null, empfaenger: 'Bezirk Oberfranken', }, ], }) const dokument = eintraege.find((e) => e.id === 'DOKUMENT:d1') expect(dokument?.datum.toISOString()).toBe('2026-02-10T00:00:00.000Z') expect(dokument?.detail).toBe('Versand an Bezirk Oberfranken') }) it('macht aus den Datumsfeldern einer Genehmigung die Chronik des Verfahrens', () => { const eintraege = zeitstrahlBauen({ ...grundlage, genehmigungen: [ { id: 'g1', titel: 'Verkauf Hausgrundstück', rechtsgrundlage: '§ 1850 BGB', status: 'RECHTSKRAEFTIG', mitteilungAm: new Date('2026-02-01T00:00:00.000Z'), antragAm: new Date('2026-02-15T00:00:00.000Z'), anhoerungAm: null, beschlussAm: new Date('2026-03-20T00:00:00.000Z'), rechtskraftAm: new Date('2026-04-05T00:00:00.000Z'), vollzugAm: null, }, ], }) const genehmigung = eintraege.filter((e) => e.art === 'GENEHMIGUNG') expect(genehmigung).toHaveLength(4) expect(genehmigung[0]?.titel).toBe('Genehmigung: Rechtskräftig') expect(genehmigung.at(-1)?.titel).toBe('Genehmigung: Dem Gericht mitgeteilt') }) it('zeigt Ausgaben mit negativem Vorzeichen', () => { const eintraege = zeitstrahlBauen({ ...grundlage, buchungen: [ { id: 'b1', datum: new Date('2026-05-01T00:00:00.000Z'), betragCent: 4200, richtung: 'AUSGABE', gegenstelle: null, verwendungszweck: null, }, { id: 'b2', datum: new Date('2026-05-02T00:00:00.000Z'), betragCent: 9900, richtung: 'EINNAHME', gegenstelle: null, verwendungszweck: null, }, ], }) expect(eintraege.find((e) => e.id === 'BUCHUNG:b1')?.betragCent).toBe(-4200) expect(eintraege.find((e) => e.id === 'BUCHUNG:b2')?.betragCent).toBe(9900) }) it('hält die Reihenfolge bei gleichem Datum stabil', () => { const gleich = new Date('2026-06-01T00:00:00.000Z') const einmal = zeitstrahlBauen({ ...grundlage, fristen: [ { id: 'f1', titel: 'A', datum: gleich, erledigtAm: null, ausschlussfrist: false, rechtsgrundlage: null, }, ], dokumente: [ { id: 'd1', titel: 'B', eingangAm: gleich, versandAm: null, erstelltAm: gleich, absender: null, empfaenger: null, }, ], }) // Dokument vor Frist: was passiert ist, steht über dem, was ansteht. expect(einmal.map((e) => e.id).slice(0, 2)).toEqual(['DOKUMENT:d1', 'FRIST:f1']) }) }) // ============================================================================= describe('Sachstandsanfrage', () => { const eingabe = { vorgangTitel: 'Antrag auf Hilfe zur Pflege', gegenstelle: 'Bezirk Oberfranken', aktenzeichen: 'HzP 2026/4711', letzterKontaktAm: new Date(2026, 4, 12), betreute: { vorname: 'Erika', nachname: 'Mustermann', geburtsdatum: new Date(1948, 2, 3), }, betreuer: { name: 'Florian Wilde', aktenzeichenGericht: 'XVII 123/25', betreuungsgericht: 'Amtsgericht Bayreuth', }, heute: new Date(2026, 5, 30), } it('nennt Aktenzeichen, Gegenstelle und das Datum des letzten Kontakts', () => { const anfrage = sachstandsanfrageErzeugen(eingabe) expect(anfrage.text).toContain('Bezirk Oberfranken') expect(anfrage.text).toContain('HzP 2026/4711') expect(anfrage.text).toContain('12.05.2026') expect(anfrage.text).toContain('30.06.2026') expect(anfrage.text).toContain('Erika Mustermann') expect(anfrage.text).toContain('XVII 123/25') }) it('bittet ausdrücklich um den Sachstand und schließt mit dem Namen des Betreuers', () => { const anfrage = sachstandsanfrageErzeugen(eingabe) expect(anfrage.text).toContain('Sachstand') expect(anfrage.text).toContain('Mit freundlichen Grüßen') expect(anfrage.text.trimEnd().endsWith('Rechtlicher Betreuer')).toBe(true) }) it('rechnet die Tage seit dem letzten Kontakt aus', () => { // 12.05.2026 bis 30.06.2026 sind 49 Tage. const anfrage = sachstandsanfrageErzeugen(eingabe) expect(anfrage.tageSeitKontakt).toBe(49) expect(anfrage.text).toContain('49 Tage') }) it('kommt ohne erfassten letzten Kontakt aus', () => { const anfrage = sachstandsanfrageErzeugen({ ...eingabe, letzterKontaktAm: null }) expect(anfrage.tageSeitKontakt).toBeNull() expect(anfrage.text).toContain('bisher nicht eingegangen') }) it('baut einen Betreff, der die Sache eindeutig macht', () => { const anfrage = sachstandsanfrageErzeugen(eingabe) expect(anfrage.betreff).toBe( 'Sachstandsanfrage: Antrag auf Hilfe zur Pflege · Erika Mustermann, geb. 03.03.1948 · Az. HzP 2026/4711', ) }) it('spricht einen bekannten Ansprechpartner direkt an', () => { const anfrage = sachstandsanfrageErzeugen({ ...eingabe, ansprechpartner: { name: 'Petra Schmidt', anrede: 'FRAU' }, }) expect(anfrage.text).toContain('Sehr geehrte Frau Schmidt,') // Der Name steht ueber der Stelle - so landet der Brief bei ihr. const zeilen = anfrage.text.split('\n') expect(zeilen[0]).toBe('Petra Schmidt') expect(zeilen[1]).toBe('Bezirk Oberfranken') }) it('bleibt ohne Ansprechpartner bei der allgemeinen Anrede', () => { const anfrage = sachstandsanfrageErzeugen(eingabe) expect(anfrage.text).toContain('Sehr geehrte Damen und Herren,') }) it('hält Nachfassen erst nach sechs Wochen für angebracht', () => { const heute = new Date(2026, 5, 30) expect(nachfassenFaellig(new Date(2026, 5, 20), heute)).toBe(false) expect(nachfassenFaellig(new Date(2026, 4, 12), heute)).toBe(true) expect(nachfassenFaellig(null, heute)).toBe(true) }) })