import { describe, expect, it } from 'vitest' import { tag } from '@/lib/fristen/datum' import { leerstandCheckliste, naechsteFaelligkeit, naechsterStichtagImJahr, } from '@/lib/immobilie/leerstand-checkliste' import { durchschnittlicheJahreskosten, kostenkontoAufbauen, } from '@/lib/immobilie/kostenkonto' import { inventarNotizLesen, inventarNotizSchreiben, inventarSumme } from '@/lib/immobilie/inventar' import { centAusText } from '@/lib/validierung/substanz' describe('Pflichten-Checkliste bei Leerstand', () => { const heute = tag(2026, 3, 15) it('nennt die Meldung an den Gebäudeversicherer als dringenden Punkt', () => { const punkte = leerstandCheckliste(tag(2026, 1, 10), heute) const versicherung = punkte.find((p) => p.schluessel === 'gebaeudeversicherung-leerstand-melden') expect(versicherung).toBeDefined() expect(versicherung!.dringend).toBe(true) expect(versicherung!.beschreibung).toMatch(/leistungsfrei/i) }) it('legt den Frostschutz auf den Oktober', () => { const punkte = leerstandCheckliste(tag(2026, 1, 10), heute) const frostschutz = punkte.find((p) => p.schluessel === 'frostschutz') expect(frostschutz).toBeDefined() expect(frostschutz!.faelligAm.getUTCMonth() + 1).toBe(10) expect(frostschutz!.faelligAm.getUTCFullYear()).toBe(2026) expect(frostschutz!.wiederholungRrule).toContain('BYMONTH=10') }) it('schiebt den Frostschutz ins Folgejahr, wenn der Oktober schon vorbei ist', () => { const punkte = leerstandCheckliste(tag(2026, 1, 10), tag(2026, 11, 2)) const frostschutz = punkte.find((p) => p.schluessel === 'frostschutz')! expect(frostschutz.faelligAm.getUTCFullYear()).toBe(2027) expect(frostschutz.faelligAm.getUTCMonth() + 1).toBe(10) }) it('lässt den Kontrollgang wöchentlich wiederkehren', () => { const kontrollgang = leerstandCheckliste(tag(2026, 1, 10), heute).find( (p) => p.schluessel === 'kontrollgang', )! expect(kontrollgang.wiederholungRrule).toBe('FREQ=WEEKLY;INTERVAL=1') }) it('setzt keine Fälligkeit in die Vergangenheit', () => { const punkte = leerstandCheckliste(tag(2020, 1, 1), heute) for (const punkt of punkte) { expect(punkt.faelligAm.getTime()).toBeGreaterThanOrEqual(heute.getTime()) } }) it('deckt alle Pflichten ab, die bei Leerstand teuer werden können', () => { const schluessel = leerstandCheckliste(null, heute).map((p) => p.schluessel) expect(schluessel).toEqual( expect.arrayContaining([ 'gebaeudeversicherung-leerstand-melden', 'kontrollgang', 'frostschutz', 'winterdienst-verkehrssicherung', 'zaehlerstaende', 'schluesselverzeichnis', 'fotodokumentation', 'briefkasten-nachsendeauftrag', 'rauchmelder', 'heizungswartung', ]), ) }) it('findet den nächsten Oktobertermin auch am Stichtag selbst', () => { expect(naechsterStichtagImJahr(tag(2026, 10, 1), 10, 1)).toEqual(tag(2026, 10, 1)) expect(naechsterStichtagImJahr(tag(2026, 10, 2), 10, 1)).toEqual(tag(2027, 10, 1)) }) it('rechnet den nächsten Kontrollgang eine Woche nach dem Erledigen', () => { expect(naechsteFaelligkeit('FREQ=WEEKLY;INTERVAL=1', tag(2026, 3, 15))).toEqual( tag(2026, 3, 22), ) }) it('rechnet den nächsten Frostschutz auf den Oktober des Folgejahres', () => { expect( naechsteFaelligkeit('FREQ=YEARLY;BYMONTH=10;BYMONTHDAY=1', tag(2026, 10, 1)), ).toEqual(tag(2027, 10, 1)) }) }) describe('Kostenkonto der Immobilie', () => { const buchungen = [ { datum: tag(2025, 2, 3), betragCent: 12_000n, richtung: 'AUSGABE' as const, kategorie: 'GRUNDSTEUER' as const }, { datum: tag(2025, 5, 3), betragCent: 12_000n, richtung: 'AUSGABE' as const, kategorie: 'GRUNDSTEUER' as const }, { datum: tag(2025, 7, 1), betragCent: 48_000n, richtung: 'AUSGABE' as const, kategorie: 'IMMOBILIE_BETRIEBSKOSTEN' as const }, { datum: tag(2025, 9, 9), betragCent: 150_000n, richtung: 'AUSGABE' as const, kategorie: 'IMMOBILIE_INSTANDHALTUNG' as const }, { datum: tag(2026, 2, 3), betragCent: 13_000n, richtung: 'AUSGABE' as const, kategorie: 'GRUNDSTEUER' as const }, // Fremde Kategorien und Umbuchungen bleiben draußen. { datum: tag(2025, 3, 1), betragCent: 90_000n, richtung: 'AUSGABE' as const, kategorie: 'HEIMKOSTEN' as const }, { datum: tag(2025, 3, 2), betragCent: 50_000n, richtung: 'UMBUCHUNG' as const, kategorie: 'GRUNDSTEUER' as const }, ] it('summiert je Jahr und Kategorie', () => { const jahre = kostenkontoAufbauen(buchungen) expect(jahre.map((j) => j.jahr)).toEqual([2026, 2025]) const zweitausendfuenfundzwanzig = jahre.find((j) => j.jahr === 2025)! expect(zweitausendfuenfundzwanzig.ausgabenCent).toBe(222_000n) expect( zweitausendfuenfundzwanzig.ausgaben.find((z) => z.kategorie === 'GRUNDSTEUER')?.summeCent, ).toBe(24_000n) expect( zweitausendfuenfundzwanzig.ausgaben.find((z) => z.kategorie === 'GRUNDSTEUER')?.anzahl, ).toBe(2) }) it('lässt Umbuchungen und fremde Kategorien außen vor', () => { const jahre = kostenkontoAufbauen(buchungen) const zweitausendfuenfundzwanzig = jahre.find((j) => j.jahr === 2025)! expect(zweitausendfuenfundzwanzig.anzahlBuchungen).toBe(4) }) it('zählt Mieteinnahmen als Einnahme gegen die Kosten', () => { const jahre = kostenkontoAufbauen([ ...buchungen, { datum: tag(2025, 6, 1), betragCent: 60_000n, richtung: 'EINNAHME' as const, kategorie: 'MIETEINNAHME' as const }, ]) const zweitausendfuenfundzwanzig = jahre.find((j) => j.jahr === 2025)! expect(zweitausendfuenfundzwanzig.einnahmenCent).toBe(60_000n) expect(zweitausendfuenfundzwanzig.saldoCent).toBe(60_000n - 222_000n) }) it('rechnet den Durchschnitt nur über abgeschlossene Jahre', () => { const jahre = kostenkontoAufbauen(buchungen) expect(durchschnittlicheJahreskosten(jahre, 2026)).toBe(222_000n) }) }) describe('Inventarliste', () => { it('schreibt und liest Raum und Verbleib verlustfrei', () => { const text = inventarNotizSchreiben({ raum: 'Wohnzimmer', verbleib: 'im Haus', bemerkung: 'Standuhr des Großvaters', }) expect(inventarNotizLesen(text)).toEqual({ raum: 'Wohnzimmer', verbleib: 'im Haus', bemerkung: 'Standuhr des Großvaters', }) }) it('rettet eine formlose Notiz aus einem anderen Modul in die Bemerkung', () => { expect(inventarNotizLesen('von Hand notiert')).toEqual({ raum: null, verbleib: null, bemerkung: 'von Hand notiert', }) }) it('summiert die geschätzten Werte', () => { expect( inventarSumme([{ wertCent: 25_000n }, { wertCent: null }, { wertCent: 5_000n }]), ).toBe(30_000n) }) }) describe('Betragseingabe', () => { it('liest deutsche und englische Schreibweise als Cent', () => { expect(centAusText('1.234,56')).toBe(123_456n) expect(centAusText('1234.56')).toBe(123_456n) expect(centAusText('8,15')).toBe(815n) expect(centAusText('1.000')).toBe(100_000n) expect(centAusText('12')).toBe(1_200n) expect(centAusText('-45,90')).toBe(-4_590n) }) it('rundet nicht über Fließkomma - 8,15 Euro bleiben 815 Cent', () => { expect(centAusText('8,15')).toBe(815n) expect(centAusText('0,07')).toBe(7n) expect(centAusText('1,1')).toBe(110n) }) it('weist Unsinn zurück', () => { expect(centAusText('viel')).toBeNull() expect(centAusText('')).toBeNull() expect(centAusText('1,234')).toBeNull() }) })