Etappe 5. Bildet das laufende Verfahren beim Bezirk Oberfranken ab. Rechtsbehelfsfrist: § 187 Abs. 1, § 188 Abs. 2 und 3 BGB, danach § 64 Abs. 3 SGG - hier verschieben Wochenende und Feiertag das Fristende auf den naechsten Werktag. Das ist genau umgekehrt zu den Betreuerfristen, wo bewusst nicht verschoben wird, und deshalb liegt die Feiertagsliste getrennt. Zwei bayerische Sonderfaelle, die haeufig falsch gemacht werden: Mariae Himmelfahrt verschiebt NICHT (Art. 1 Abs. 1 S. 2 BayFTG - der Feiertag gilt nur in ueberwiegend katholischen Gemeinden, Bayreuth ist keine), und der Buss- und Bettag ist in Bayern seit 1995 kein Feiertag mehr. Beide erzeugen stattdessen einen ausdruecklichen Hinweis, damit man die Frist im Zweifel selbst nachrechnet. Der Rechenweg wird als Schrittliste ausgegeben. Heimkosten: Der Dashboard-Agent hatte einen fachlichen Widerspruch gefunden - der einrichtungseinheitliche Eigenanteil auf der Heimrechnung ist bereits um die Leistung der Pflegekasse gekuerzt. Wird sie zusaetzlich als Deckung erfasst, entsteht eine Doppelzaehlung von ueber tausend Euro im Monat. Das Modul rechnet jetzt ausdruecklich nach der Lesart EIGENANTEIL_NETTO, fuehrt die Pflegekassenleistung nur nachrichtlich mit und warnt, wenn sie doch belegt ist. Die Oberflaeche sagt auf einer eigenen Karte, was hier gegen was gerechnet wird. Die Schonvermoegens-Bewertung liefert Argumentationshilfen zum Uebernehmen, keine Automatik - die Einordnung obliegt dem Traeger und gegebenenfalls dem Sozialgericht. Das steht an jeder dieser Karten. Alle jaehrlich veraenderlichen Betraege stehen an einer Stelle mit Stand-Datum und Prueftermin. Die Bekleidungspauschale bewusst ohne Zahl: sie ist regional und steht im Bescheid. 46 Tests. Etappe: 5 Phase: Behoerde
638 lines
26 KiB
TypeScript
638 lines
26 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)
|
|
})
|
|
})
|