Files
betreuungs-cockpit/tests/sozialhilfe.test.ts
T
Florian Wilde 1ed19eb76e Zeilenenden der 24 verbliebenen Dateien normalisieren
Ausschliesslich Zeilenenden. Nachgewiesen mit
"git diff --cached --ignore-cr-at-eol": Der Diff ist leer, es hat sich
kein Zeichen Inhalt geaendert.

Deshalb ein eigener Commit: 7448 geaenderte Zeilen in einem Commit mit
echten Aenderungen zu verstecken, macht die Historie an dieser Stelle
unlesbar. So laesst er sich beim Durchsehen ueberspringen.

Refs #26
2026-08-23 23:37:02 +02:00

638 lines
25 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { alsIsoTag, tag } from '@/lib/fristen/datum'
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', () => {
/**
* Lesart EIGENANTEIL_NETTO: pflegeEigenanteilCent ist der Betrag von der
* Heimrechnung und damit schon um die Pauschale der Pflegekasse gekürzt.
* pflegekasseCent bleibt deshalb 0 — sonst stünde dasselbe Geld zweimal
* in der Rechnung.
*/
const grundfall = {
jahr: 2026,
monat: 3,
pflegeEigenanteilCent: 120_000n,
unterkunftVerpflegungCent: 90_000n,
investitionskostenCent: 45_000n,
ausbildungsumlageCent: 6_000n,
zusatzleistungenCent: 0n,
pflegekasseCent: 0n,
leistungszuschlagCent: 36_000n,
renteCent: 130_000n,
sonstigeEinkuenfteCent: 0n,
sozialhilfeCent: 90_000n,
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(256_000n)
expect(ergebnis.deckungslueckeCent).toBe(20_201n)
expect(ergebnis.ueberdeckungCent).toBe(0n)
expect(ergebnis.gedeckt).toBe(false)
})
it('zählt die Pauschale der Pflegekasse nicht noch einmal als Deckung', () => {
// Der einrichtungseinheitliche Eigenanteil ist definitionsgemäß schon
// um sie gekürzt (§ 43 Abs. 2 SGB XI). Trägt man sie trotzdem ein,
// bleibt die Lücke gleich - und die Anwendung sagt, warum.
const mitDoppelzaehlung = heimkostenAuswerten({ ...grundfall, pflegekasseCent: 131_900n })
expect(mitDoppelzaehlung.deckungCent).toBe(256_000n)
expect(mitDoppelzaehlung.deckungslueckeCent).toBe(20_201n)
expect(mitDoppelzaehlung.pflegekasseNachrichtlichCent).toBe(131_900n)
expect(mitDoppelzaehlung.hinweise.join(' ')).toContain('§ 43 Abs. 2 SGB XI')
})
it('kommt bei der Brutto-Lesart zum selben Ergebnis', () => {
// Wer den vollen Pflegesatz erfasst, muss die Pauschale als Deckung
// gegenrechnen. Bei konsistenter Erfassung ist die Lücke dieselbe -
// nur die beiden Seiten sind um denselben Betrag größer.
const brutto = heimkostenAuswerten(
{
...grundfall,
pflegeEigenanteilCent: 120_000n + 131_900n,
pflegekasseCent: 131_900n,
},
'PFLEGESATZ_BRUTTO',
)
expect(brutto.heimkostenCent).toBe(392_900n)
expect(brutto.deckungCent).toBe(387_900n)
expect(brutto.deckungslueckeCent).toBe(20_201n)
expect(brutto.pflegekasseNachrichtlichCent).toBe(0n)
})
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(49_799n)
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(20_201n)
})
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(8_347n)
// Die erfasste Lücke bleibt davon unberührt - maßgeblich ist die
// Heimrechnung, die Schätzung steht nur daneben.
expect(ergebnis.deckungslueckeCent).toBe(20_201n)
})
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(20_201n + 100_201n + 20_201n)
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)
})
})