import { describe, expect, it } from 'vitest' import { alsIsoTag, tag } from '@/lib/fristen/datum' import { betragZuCent, centZuEingabe } from '@/lib/sozialhilfe/betrag' import { nochNichtUebernommen, STANDARDUNTERLAGEN } from '@/lib/sozialhilfe/checkliste' import { aufWerktagSchieben, bayerischerFeiertag, bussUndBettag, istWerktag } from '@/lib/sozialhilfe/feiertage' import { abwesenheitSchaetzen, heimkostenAuswerten, jahresverlaufBilden, leistungszuschlagCent, leistungszuschlagProzent, tageImMonat, verweildauerMonate, } from '@/lib/sozialhilfe/heimkosten' import { LEISTUNGSKATALOG, nochNichtAngelegt } from '@/lib/sozialhilfe/leistungskatalog' import { trackerFortschritt } from '@/lib/sozialhilfe/nachforderungen' import { rechtsbehelfsfristBerechnen, zugangsvermutung } from '@/lib/sozialhilfe/rechtsbehelf' import { ARGUMENTATIONSHILFEN, VORBEHALT } from '@/lib/sozialhilfe/schonvermoegen' import { freibetragVergleichen, uebersichtBilden } from '@/lib/sozialhilfe/vermoegensuebersicht' /** * Diese Tests beschreiben, was fachlich gelten soll - nicht, welche * Funktion aufgerufen wird. Keine Datenbank, keine Uhrzeiten, keine * Zufälligkeiten: Fristen müssen auch am 29. Februar stimmen. */ // ============================================================================= // Widerspruchsfrist // ============================================================================= describe('Widerspruchsfrist gegen einen Bescheid', () => { it('endet einen Monat nach dem Zugang, gerechnet vom Zugangstag', () => { // Zugang 15.03.2026 (Sonntag ist der 15.03.2026 nicht - siehe unten), // ein Monat: der Tag mit derselben Zahl im Folgemonat. const ergebnis = rechtsbehelfsfristBerechnen({ zugegangenAm: tag(2026, 4, 15), fristMonate: 1, heute: tag(2026, 4, 20), }) expect(ergebnis.berechenbar).toBe(true) expect(alsIsoTag(ergebnis.rechnerischesEnde!)).toBe('2026-05-15') expect(alsIsoTag(ergebnis.widerspruchMoeglichBis!)).toBe('2026-05-15') expect(ergebnis.verschoben).toBe(false) }) it('rechnet über die Monatsgrenze: 31.01. plus ein Monat ist der 28.02.', () => { // Der klassische Stolperstein. § 188 Abs. 3 BGB: Fehlt der Tag mit // derselben Zahl im Zielmonat, endet die Frist am letzten Tag des // Monats. Der 28.02.2025 ist ein Freitag, es wird nichts verschoben. const ergebnis = rechtsbehelfsfristBerechnen({ zugegangenAm: tag(2025, 1, 31), fristMonate: 1, heute: tag(2025, 2, 1), }) expect(alsIsoTag(ergebnis.rechnerischesEnde!)).toBe('2025-02-28') expect(alsIsoTag(ergebnis.widerspruchMoeglichBis!)).toBe('2025-02-28') expect(ergebnis.verschoben).toBe(false) }) it('schiebt ein Fristende am Wochenende auf den nächsten Werktag', () => { // Zugang 31.01.2026 -> rechnerisch 28.02.2026, das ist ein Samstag. // § 64 Abs. 3 SGG: Ende erst mit Ablauf des nächsten Werktags, // also am Montag, dem 02.03.2026. const ergebnis = rechtsbehelfsfristBerechnen({ zugegangenAm: tag(2026, 1, 31), fristMonate: 1, heute: tag(2026, 2, 1), }) expect(alsIsoTag(ergebnis.rechnerischesEnde!)).toBe('2026-02-28') expect(alsIsoTag(ergebnis.widerspruchMoeglichBis!)).toBe('2026-03-02') expect(ergebnis.verschoben).toBe(true) expect(ergebnis.schritte.some((s) => s.rechtsgrundlage.includes('§ 64 Abs. 3 SGG'))).toBe(true) }) it('schiebt ein Fristende an einem bayerischen Feiertag auf den nächsten Werktag', () => { // Zugang 01.10.2027 -> rechnerisch 01.11.2027. Das ist ein Montag und // zugleich Allerheiligen, in ganz Bayern gesetzlicher Feiertag. const ergebnis = rechtsbehelfsfristBerechnen({ zugegangenAm: tag(2027, 10, 1), fristMonate: 1, heute: tag(2027, 10, 2), }) expect(alsIsoTag(ergebnis.rechnerischesEnde!)).toBe('2027-11-01') expect(alsIsoTag(ergebnis.widerspruchMoeglichBis!)).toBe('2027-11-02') expect(ergebnis.verschoben).toBe(true) expect(ergebnis.schritte.some((s) => s.text.includes('Allerheiligen'))).toBe(true) }) it('läuft ein Jahr, wenn die Rechtsbehelfsbelehrung fehlt oder fehlerhaft ist', () => { // § 66 Abs. 2 SGG. Zugang 15.03.2026 -> 15.03.2027 (ein Montag). const ergebnis = rechtsbehelfsfristBerechnen({ zugegangenAm: tag(2026, 3, 15), fristMonate: 12, heute: tag(2026, 3, 20), }) expect(ergebnis.belehrung).toBe('FEHLT_ODER_FEHLERHAFT') expect(alsIsoTag(ergebnis.widerspruchMoeglichBis!)).toBe('2027-03-15') expect(ergebnis.schritte.some((s) => s.rechtsgrundlage === '§ 66 Abs. 2 SGG')).toBe(true) }) it('kappt die Jahresfrist bei einem Zugang am 29. Februar auf den 28. Februar', () => { const ergebnis = rechtsbehelfsfristBerechnen({ zugegangenAm: tag(2028, 2, 29), fristMonate: 12, heute: tag(2028, 3, 1), }) expect(alsIsoTag(ergebnis.rechnerischesEnde!)).toBe('2029-02-28') }) it('rechnet nicht ohne Zugangsdatum und sagt, warum', () => { const ergebnis = rechtsbehelfsfristBerechnen({ zugegangenAm: null, bescheidVom: tag(2026, 5, 4), }) expect(ergebnis.berechenbar).toBe(false) expect(ergebnis.widerspruchMoeglichBis).toBeNull() expect(ergebnis.hinweise.join(' ')).toContain('Bekanntgabe') }) it('erkennt eine abgelaufene Frist und zählt die Tage darüber hinaus', () => { const ergebnis = rechtsbehelfsfristBerechnen({ zugegangenAm: tag(2026, 4, 15), fristMonate: 1, heute: tag(2026, 5, 20), }) expect(ergebnis.abgelaufen).toBe(true) expect(ergebnis.tageVerbleibend).toBe(-5) }) it('schlägt als Zugangsdatum den dritten Tag nach der Post vor', () => { // § 37 Abs. 2 SGB X. Bescheid vom Mittwoch, 06.05.2026 -> 09.05.2026 // ist ein Samstag, also weiter auf Montag, den 11.05.2026. const vorschlag = zugangsvermutung(tag(2026, 5, 6)) expect(alsIsoTag(vorschlag.datum)).toBe('2026-05-11') expect(vorschlag.hinweis).toContain('§ 37 Abs. 2 SGB X') }) }) // ============================================================================= // Bayerische Feiertage // ============================================================================= describe('Bayerische Feiertage in der Fristberechnung', () => { it('behandelt Allerheiligen als landesweiten Feiertag', () => { const feiertag = bayerischerFeiertag(tag(2027, 11, 1)) expect(feiertag?.name).toBe('Allerheiligen') expect(feiertag?.landesweit).toBe(true) expect(istWerktag(tag(2027, 11, 1))).toBe(false) }) it('verschiebt an Mariä Himmelfahrt nicht, warnt aber ausdrücklich', () => { // Sonderfall: nur in überwiegend katholischen Gemeinden gesetzlicher // Feiertag (Art. 1 Abs. 1 Satz 2 BayFTG). Bayreuth, Sitz des Bezirks // Oberfranken, gehört nicht dazu. Der 15.08.2028 ist ein Dienstag. const feiertag = bayerischerFeiertag(tag(2028, 8, 15)) expect(feiertag?.name).toBe('Mariä Himmelfahrt') expect(feiertag?.landesweit).toBe(false) expect(istWerktag(tag(2028, 8, 15))).toBe(true) const ergebnis = rechtsbehelfsfristBerechnen({ zugegangenAm: tag(2028, 7, 15), fristMonate: 1, heute: tag(2028, 7, 20), }) expect(alsIsoTag(ergebnis.widerspruchMoeglichBis!)).toBe('2028-08-15') expect(ergebnis.verschoben).toBe(false) expect(ergebnis.hinweise.join(' ')).toContain('Mariä Himmelfahrt') }) it('behandelt den Buß- und Bettag in Bayern als normalen Arbeitstag', () => { // Sonderfall: seit 1995 nur noch in Sachsen gesetzlicher Feiertag. // 2026 fällt er auf Mittwoch, den 18.11. expect(alsIsoTag(bussUndBettag(2026))).toBe('2026-11-18') const feiertag = bayerischerFeiertag(tag(2026, 11, 18)) expect(feiertag?.name).toBe('Buß- und Bettag') expect(feiertag?.landesweit).toBe(false) expect(istWerktag(tag(2026, 11, 18))).toBe(true) const geschoben = aufWerktagSchieben(tag(2026, 11, 18)) expect(alsIsoTag(geschoben.datum)).toBe('2026-11-18') expect(geschoben.verschoben).toBe(false) }) it('überspringt eine Feiertagskette am Jahreswechsel', () => { // 01.01.2027 ist Neujahr und ein Freitag; danach Samstag und Sonntag. // Nächster Werktag ist Montag, der 04.01.2027. const geschoben = aufWerktagSchieben(tag(2027, 1, 1)) expect(alsIsoTag(geschoben.datum)).toBe('2027-01-04') expect(geschoben.verschoben).toBe(true) }) it('erkennt die beweglichen Feiertage über die Osterformel', () => { // Ostersonntag 2026 ist der 05.04. expect(bayerischerFeiertag(tag(2026, 4, 3))?.name).toBe('Karfreitag') expect(bayerischerFeiertag(tag(2026, 4, 6))?.name).toBe('Ostermontag') expect(bayerischerFeiertag(tag(2026, 5, 14))?.name).toBe('Christi Himmelfahrt') expect(bayerischerFeiertag(tag(2026, 6, 4))?.name).toBe('Fronleichnam') }) }) // ============================================================================= // Nachforderungs-Tracker // ============================================================================= describe('Fortschritt des Nachforderungs-Trackers', () => { const heute = tag(2026, 6, 15) const zeilen = [ { id: '1', bezeichnung: 'Rentenbescheide', status: 'ANERKANNT' as const, fristBis: null }, { id: '2', bezeichnung: 'Kontoauszüge', status: 'UEBERSANDT' as const, fristBis: null }, { id: '3', bezeichnung: 'Mietvertrag der früheren Wohnung', status: 'NICHT_VORHANDEN' as const, fristBis: null, }, { id: '4', bezeichnung: 'Rückkaufswerte', status: 'IN_BESCHAFFUNG' as const, fristBis: tag(2026, 6, 20), }, { id: '5', bezeichnung: 'Grundbuchauszug', status: 'OFFEN' as const, fristBis: tag(2026, 6, 1) }, ] it('zählt Übersandtes, Anerkanntes und begründete Fehlanzeigen als geliefert', () => { // Eine begründete Fehlanzeige ist eine Antwort an den Träger und // damit erledigt - sonst bliebe der Balken für immer unvollständig. const fortschritt = trackerFortschritt(zeilen, heute) expect(fortschritt.gesamt).toBe(5) expect(fortschritt.geliefert).toBe(3) expect(fortschritt.offen).toBe(2) expect(fortschritt.prozent).toBe(60) expect(fortschritt.vollstaendig).toBe(false) expect(fortschritt.beschriftung).toBe('3 von 5 Unterlagen geliefert') }) it('meldet überfällige und diese Woche fällige Unterlagen getrennt', () => { const fortschritt = trackerFortschritt(zeilen, heute) expect(fortschritt.ueberfaellig.map((z) => z.id)).toEqual(['5']) expect(fortschritt.dieseWoche.map((z) => z.id)).toEqual(['4']) expect(alsIsoTag(fortschritt.naechsteFrist!)).toBe('2026-06-01') }) it('ist erst vollständig, wenn keine Zeile mehr offen ist', () => { const alleGeliefert = zeilen.map((z) => ({ ...z, status: 'ANERKANNT' as const })) const fortschritt = trackerFortschritt(alleGeliefert, heute) expect(fortschritt.vollstaendig).toBe(true) expect(fortschritt.prozent).toBe(100) expect(fortschritt.ueberfaellig).toHaveLength(0) }) it('kommt mit einem leeren Tracker zurecht, ohne durch null zu teilen', () => { const fortschritt = trackerFortschritt([], heute) expect(fortschritt.prozent).toBe(0) expect(fortschritt.vollstaendig).toBe(false) expect(fortschritt.beschriftung).toBe('Noch keine Unterlagen erfasst') }) }) describe('Checkliste der Standardunterlagen', () => { it('enthält die Unterlagen, die der Bezirk regelmäßig anfordert', () => { const schluessel = STANDARDUNTERLAGEN.map((p) => p.schluessel) expect(schluessel).toContain('rentenbescheide') expect(schluessel).toContain('kontoauszuege') expect(schluessel).toContain('notarielle-uebertragungen') expect(schluessel).toContain('unterhaltspflichtige') expect(new Set(schluessel).size).toBe(schluessel.length) }) it('erklärt zu jeder Position, warum der Träger sie will', () => { for (const position of STANDARDUNTERLAGEN) { expect(position.erlaeuterung.length).toBeGreaterThan(60) } }) it('bietet nur an, was noch nicht im Tracker steht', () => { const schonDa = [STANDARDUNTERLAGEN[0]!.bezeichnung.toUpperCase()] const offen = nochNichtUebernommen(schonDa) expect(offen).toHaveLength(STANDARDUNTERLAGEN.length - 1) expect(offen.map((p) => p.schluessel)).not.toContain(STANDARDUNTERLAGEN[0]!.schluessel) }) }) // ============================================================================= // Leistungszuschlag § 43c SGB XI // ============================================================================= describe('Leistungszuschlag nach der Verweildauer', () => { it('folgt der Staffel 15 / 30 / 50 / 75 Prozent', () => { // "mehr als 12 Monate" heißt: ab dem 13. Monat, also sobald zwölf // Monate vollendet sind. expect(leistungszuschlagProzent(0)).toBe(15) expect(leistungszuschlagProzent(11)).toBe(15) expect(leistungszuschlagProzent(12)).toBe(30) expect(leistungszuschlagProzent(23)).toBe(30) expect(leistungszuschlagProzent(24)).toBe(50) expect(leistungszuschlagProzent(35)).toBe(50) expect(leistungszuschlagProzent(36)).toBe(75) expect(leistungszuschlagProzent(240)).toBe(75) }) it('rechnet den Zuschlag nur auf den pflegebedingten Eigenanteil', () => { // 1.200,00 EUR Eigenanteil, zweites Jahr im Heim: 30 Prozent. expect(leistungszuschlagCent(120_000n, 14)).toBe(36_000n) // Erstes Jahr: 15 Prozent. expect(leistungszuschlagCent(120_000n, 3)).toBe(18_000n) }) it('rundet kaufmännisch auf ganze Cent', () => { // 1.234,57 EUR mal 15 Prozent sind 185,1855 EUR -> 185,19 EUR. expect(leistungszuschlagCent(123_457n, 0)).toBe(18_519n) }) it('zählt vollendete Monate im Heim, nicht angebrochene', () => { expect(verweildauerMonate(tag(2026, 1, 20), tag(2026, 2, 19))).toBe(0) expect(verweildauerMonate(tag(2026, 1, 20), tag(2026, 2, 20))).toBe(1) expect(verweildauerMonate(tag(2026, 1, 20), tag(2027, 1, 19))).toBe(11) expect(verweildauerMonate(tag(2026, 1, 20), tag(2027, 1, 20))).toBe(12) }) }) // ============================================================================= // Heimkosten // ============================================================================= describe('Heimkosten-Gegenüberstellung', () => { const grundfall = { jahr: 2026, monat: 3, pflegeEigenanteilCent: 120_000n, unterkunftVerpflegungCent: 90_000n, investitionskostenCent: 45_000n, ausbildungsumlageCent: 6_000n, zusatzleistungenCent: 0n, pflegekasseCent: 131_900n, leistungszuschlagCent: 36_000n, renteCent: 90_000n, sonstigeEinkuenfteCent: 0n, sozialhilfeCent: 0n, eigenmittelCent: 0n, barbetragCent: 15_201n, bekleidungspauschaleCent: 0n, abwesenheitstage: 0, } it('zählt Barbetrag und Bekleidungspauschale zum Bedarf', () => { // Wer sie herauslässt, hält die Deckung für besser, als sie ist - // und plündert am Monatsende das Taschengeld. const ergebnis = heimkostenAuswerten(grundfall) expect(ergebnis.heimkostenCent).toBe(261_000n) expect(ergebnis.verbleibendCent).toBe(15_201n) expect(ergebnis.gesamtbedarfCent).toBe(276_201n) }) it('weist die Deckungslücke aus', () => { const ergebnis = heimkostenAuswerten(grundfall) expect(ergebnis.deckungCent).toBe(257_900n) expect(ergebnis.deckungslueckeCent).toBe(18_301n) expect(ergebnis.ueberdeckungCent).toBe(0n) expect(ergebnis.gedeckt).toBe(false) }) it('erkennt eine Überdeckung und weist keine negative Lücke aus', () => { const ergebnis = heimkostenAuswerten({ ...grundfall, renteCent: 200_000n }) expect(ergebnis.deckungslueckeCent).toBe(0n) expect(ergebnis.ueberdeckungCent).toBe(91_699n) expect(ergebnis.gedeckt).toBe(true) }) it('mindert erst ab dem vierten Abwesenheitstag', () => { // Die ersten drei Tage sind nach dem Rahmenvertrag voll zu zahlen. const kurz = heimkostenAuswerten({ ...grundfall, abwesenheitstage: 3 }) expect(kurz.abwesenheit.minderungstage).toBe(0) expect(kurz.abwesenheit.geschaetzteMinderungCent).toBe(0n) expect(kurz.deckungslueckeNachAbwesenheitCent).toBe(18_301n) }) it('schätzt die Minderung bei längerer Abwesenheit und verkleinert die Lücke', () => { // 10 Tage im Krankenhaus, davon 7 mindernd. 25 Prozent von // Pflegeanteil plus Unterkunft und Verpflegung (2.100,00 EUR), // tageweise auf die 31 Tage des März umgelegt. const ergebnis = heimkostenAuswerten({ ...grundfall, abwesenheitstage: 10 }) expect(ergebnis.abwesenheit.minderungstage).toBe(7) expect(ergebnis.abwesenheit.geschaetzteMinderungCent).toBe(11_854n) expect(ergebnis.deckungslueckeNachAbwesenheitCent).toBe(6_447n) // Die erfasste Lücke bleibt davon unberührt - maßgeblich ist die // Heimrechnung, die Schätzung steht nur daneben. expect(ergebnis.deckungslueckeCent).toBe(18_301n) }) it('lässt die Investitionskosten bei Abwesenheit außen vor', () => { // Der Platz wird freigehalten, das Gebäude kostet weiter // (§ 87a Abs. 1 SGB XI). const mitInvest = abwesenheitSchaetzen({ abwesenheitstage: 10, pflegeEigenanteilCent: 120_000n, unterkunftVerpflegungCent: 90_000n, tageImMonat: 31, }) expect(mitInvest.geschaetzteMinderungCent).toBe(11_854n) expect(mitInvest.erklaerung).toContain('Investitionskosten laufen weiter') }) it('warnt, wenn die Platzfreihaltung überschritten wird', () => { const ergebnis = heimkostenAuswerten({ ...grundfall, abwesenheitstage: 30, monat: 1 }) expect(ergebnis.abwesenheit.ueberPlatzfreihaltung).toBe(false) const lang = abwesenheitSchaetzen({ abwesenheitstage: 45, pflegeEigenanteilCent: 120_000n, unterkunftVerpflegungCent: 90_000n, tageImMonat: 31, }) expect(lang.ueberPlatzfreihaltung).toBe(true) }) it('kennt die Länge der Monate, auch im Schaltjahr', () => { expect(tageImMonat(2026, 2)).toBe(28) expect(tageImMonat(2028, 2)).toBe(29) expect(tageImMonat(2026, 4)).toBe(30) expect(tageImMonat(2026, 12)).toBe(31) }) it('summiert den Jahresverlauf und findet den schlimmsten Monat', () => { const monate = [ heimkostenAuswerten({ ...grundfall, monat: 1 }), heimkostenAuswerten({ ...grundfall, monat: 2, renteCent: 50_000n }), heimkostenAuswerten({ ...grundfall, monat: 3 }), ] const verlauf = jahresverlaufBilden(2026, monate) expect(verlauf.monate.map((m) => m.monat)).toEqual([1, 2, 3]) expect(verlauf.summeDeckungslueckeCent).toBe(18_301n + 58_301n + 18_301n) expect(verlauf.groessteLuecke?.monat).toBe(2) }) }) // ============================================================================= // Vermögensbewertung // ============================================================================= describe('Vermögensbewertung für das Sozialamt', () => { const posten = [ { id: 'a', bezeichnung: 'Girokonto', herkunft: 'VERMOEGEN' as const, artBeschriftung: 'Konto', bewertung: 'VERWERTBAR' as const, wertCent: 450_000n, wertStichtag: null, begruendung: 'Frei verfügbares Guthaben, kein Schutztatbestand ersichtlich.', href: '#', }, { id: 'b', bezeichnung: 'Hausgrundstück Bayreuth', herkunft: 'VERMOEGEN' as const, artBeschriftung: 'Immobilie', bewertung: 'STRITTIG' as const, wertCent: 18_000_000n, wertStichtag: null, begruendung: 'Verwertung nur als Darlehen nach § 91 SGB XII zumutbar.', href: '#', }, { id: 'c', bezeichnung: 'Bestattungsvorsorge', herkunft: 'VERTRAG' as const, artBeschriftung: 'Bestattungsvorsorge', bewertung: 'GESCHUETZT' as const, wertCent: 620_000n, wertStichtag: null, begruendung: 'Zweckgebunden, an das Bestattungsunternehmen abgetreten.', href: '#', }, { id: 'd', bezeichnung: 'Lebensversicherung', herkunft: 'VERTRAG' as const, artBeschriftung: 'Lebensversicherung', bewertung: 'UNGEPRUEFT' as const, wertCent: null, wertStichtag: null, begruendung: null, href: '#', }, ] it('summiert je Bewertung und merkt sich Posten ohne Wert', () => { const uebersicht = uebersichtBilden(posten) expect(uebersicht.jeBewertung.VERWERTBAR.summeCent).toBe(450_000n) expect(uebersicht.jeBewertung.GESCHUETZT.summeCent).toBe(620_000n) expect(uebersicht.jeBewertung.STRITTIG.summeCent).toBe(18_000_000n) expect(uebersicht.jeBewertung.UNGEPRUEFT.ohneWert).toBe(1) expect(uebersicht.gesamtCent).toBe(19_070_000n) expect(uebersicht.ungeprueft.map((p) => p.id)).toEqual(['d']) expect(uebersicht.ohneBegruendung).toHaveLength(0) }) it('stellt verwertbares Vermögen dem Freibetrag gegenüber', () => { // 4.500 EUR liegen unter dem Freibetrag von 10.000 EUR. const knapp = freibetragVergleichen(450_000n) expect(knapp.freibetragCent).toBe(1_000_000n) expect(knapp.ausgeschoepft).toBe(false) expect(knapp.ueberFreibetragCent).toBe(0n) const darueber = freibetragVergleichen(1_250_000n) expect(darueber.ueberFreibetragCent).toBe(250_000n) expect(darueber.ausgeschoepft).toBe(true) }) it('erhöht den Freibetrag für überwiegend unterhaltene Personen', () => { expect(freibetragVergleichen(0n, 2).freibetragCent).toBe(1_100_000n) }) it('meldet eine Bewertung ohne Begründung als Lücke', () => { const ohne = uebersichtBilden([{ ...posten[0]!, begruendung: ' ' }]) expect(ohne.ohneBegruendung).toHaveLength(1) }) }) describe('Argumentationshilfen zum Schonvermögen', () => { it('nennt zu jeder Hilfe eine Rechtsgrundlage und einen Vorbehalt', () => { // Die Anwendung entscheidet nichts. Jeder Textbaustein sagt das auch. for (const hilfe of ARGUMENTATIONSHILFEN) { expect(hilfe.rechtsgrundlage).toMatch(/§/) expect(hilfe.textbaustein).toContain(VORBEHALT) } }) it('deckt die vier Kernfälle ab', () => { const schluessel = ARGUMENTATIONSHILFEN.map((h) => h.schluessel) expect(schluessel).toContain('vermoegensfreibetrag') expect(schluessel).toContain('hausgrundstueck-selbst-bewohnt') expect(schluessel).toContain('bestattungsvorsorge') expect(schluessel).toContain('haertefall') }) }) // ============================================================================= // Leistungskatalog // ============================================================================= describe('Katalog weiterer Leistungen', () => { it('führt stabile, eindeutige Schlüssel', () => { const schluessel = LEISTUNGSKATALOG.map((l) => l.katalogSchluessel) expect(new Set(schluessel).size).toBe(schluessel.length) expect(schluessel).toContain('landespflegegeld-bayern') expect(schluessel).toContain('zuzahlungsbefreiung') expect(schluessel).toContain('rundfunkbeitrag') expect(schluessel).toContain('schwerbehindertenausweis') expect(schluessel).toContain('grundsicherung') expect(schluessel).toContain('hilfe-zur-pflege') expect(schluessel).toContain('wohngeld') expect(schluessel).toContain('telefon-sozialtarif') }) it('markiert die jährlich zu wiederholenden Anträge', () => { const landespflegegeld = LEISTUNGSKATALOG.find( (l) => l.katalogSchluessel === 'landespflegegeld-bayern', )! expect(landespflegegeld.jaehrlichNeuBeantragen).toBe(true) expect(landespflegegeld.antragsschluss).toContain('31.12.') const zuzahlung = LEISTUNGSKATALOG.find((l) => l.katalogSchluessel === 'zuzahlungsbefreiung')! expect(zuzahlung.jaehrlichNeuBeantragen).toBe(true) }) it('nennt zu jeder Leistung Träger, Grundlage und benötigte Unterlagen', () => { for (const leistung of LEISTUNGSKATALOG) { expect(leistung.traeger.length).toBeGreaterThan(3) expect(leistung.rechtsgrundlage.length).toBeGreaterThan(3) expect(leistung.benoetigt.length).toBeGreaterThan(0) } }) it('bietet nur an, was noch nicht angelegt ist', () => { const offen = nochNichtAngelegt(['landespflegegeld-bayern', null]) expect(offen.map((l) => l.katalogSchluessel)).not.toContain('landespflegegeld-bayern') expect(offen).toHaveLength(LEISTUNGSKATALOG.length - 1) }) }) // ============================================================================= // Beträge // ============================================================================= describe('Beträge aus Formularfeldern', () => { it('liest die deutsche Schreibweise verlustfrei in Cent', () => { expect(betragZuCent('1.234,56')).toBe(123_456n) expect(betragZuCent('1234,56')).toBe(123_456n) expect(betragZuCent('1234')).toBe(123_400n) expect(betragZuCent('0,01')).toBe(1n) expect(betragZuCent('1.234.567,89')).toBe(123_456_789n) expect(betragZuCent('12,5')).toBe(1_250n) }) it('kommt mit Euro-Zeichen, Leerzeichen und Vorzeichen zurecht', () => { expect(betragZuCent(' 1.234,56 € ')).toBe(123_456n) expect(betragZuCent('-45,00')).toBe(-4_500n) }) it('behandelt Leeres als „keine Angabe“, nicht als null Euro', () => { expect(betragZuCent('')).toBeNull() expect(betragZuCent(null)).toBeNull() expect(betragZuCent(' ')).toBeNull() }) it('weist Unsinn zurück, statt still zu raten', () => { expect(betragZuCent('abc')).toBeNull() expect(betragZuCent('12,345')).toBeNull() }) it('rechnet für die Vorbelegung zurück', () => { expect(centZuEingabe(123_456n)).toBe('1234,56') expect(centZuEingabe(5n)).toBe('0,05') expect(centZuEingabe(null)).toBe('') }) })