import { describe, expect, it } from 'vitest' import { BETRAGS_TOLERANZ_PROZENT, belegBegruendet, belegDurchVertrag, belegFehlt, belegquote, betragPasstZumVertrag, betragPruefen, type BuchungEingabe, } from '@/lib/finanzen/kassenbuch' /** * Dauerzahlungen zu einem Vertrag. * * Bei einem Dauerschuldverhältnis ist der Vertrag der Beleg — für zwölf * gleiche Lastschriften gibt es keine zwölf Quittungen, sondern eine Police. * * Der gefährliche Fehler wäre hier nicht ein zu strenges Urteil, sondern ein * zu mildes: Wenn eine verdreifachte Prämie als „belegt" durchgeht, ist die * Zuordnung eine Hintertür geworden. Die Hälfte dieser Prüfungen zielt * deshalb darauf, dass die Anerkennung auch wirklich **wegfällt**. */ function buchung(teile: Partial = {}): BuchungEingabe { return { id: 'b1', datum: new Date('2026-03-01T00:00:00Z'), betragCent: 4250n, richtung: 'AUSGABE', kategorie: 'VERSICHERUNG', gegenstelle: 'Allianz', verwendungszweck: null, kontoId: 'k1', belegId: null, belegFehltBegruendung: null, betrifftBetreute: true, vermoegenswirksam: false, ...teile, } } /** Monatlicher Vertrag über 42,50 EUR. */ const MONATLICH = { vertragId: 'v1', vertragBeitragCent: 4250n, vertragRhythmus: 'MONATLICH' as const } describe('Betrag gegen den hinterlegten Beitrag', () => { it('erkennt die passende monatliche Rate', () => { expect(betragPasstZumVertrag(4250n, 4250n, 'MONATLICH')).toBe(true) }) it('lässt eine Beitragsanpassung durchgehen', () => { // 46,00 statt 42,50 - knapp acht Prozent, das ist eine normale Anpassung. expect(betragPasstZumVertrag(4600n, 4250n, 'MONATLICH')).toBe(true) }) it('meldet die Prämie, die von 42 auf 420 springt', () => { expect(betragPasstZumVertrag(42_000n, 4250n, 'MONATLICH')).toBe(false) }) it('meldet auch die Zahlweise, die nicht zum Vertrag passt', () => { // Am Vertrag steht die Monatsrate, die Bank bucht jährlich ab // (12 x 42,50 = 510,00). Das gilt bewusst NICHT als in Ordnung: Dann // stimmen die Vertragsdaten nicht, und das soll auffallen. // // Ein früherer Entwurf hat solche Umrechnungen zugelassen — und damit // ein Toleranzband mit vier Löchern geschaffen. 420 EUR wären als // "nahe am Jahresbetrag von 510" durchgegangen, also ausgerechnet der // Fall, den diese Prüfung fangen soll. expect(betragPasstZumVertrag(51_000n, 4250n, 'MONATLICH')).toBe(false) }) it('akzeptiert die Rate, sobald die Zahlweise am Vertrag stimmt', () => { // Derselbe Fall, nachdem die Zahlweise berichtigt wurde. expect(betragPasstZumVertrag(51_000n, 51_000n, 'JAEHRLICH')).toBe(true) }) it('antwortet „unbekannt“, wenn kein Beitrag hinterlegt ist', () => { // Nicht "in Ordnung". Es soll nicht so aussehen, als wäre geprüft worden. expect(betragPasstZumVertrag(4250n, null, 'MONATLICH')).toBeNull() expect(betragPasstZumVertrag(4250n, 0n, 'MONATLICH')).toBeNull() }) it('antwortet „unbekannt“ ohne Zahlweise und bei EINMALIG', () => { expect(betragPasstZumVertrag(4250n, 4250n, null)).toBeNull() // Zu einer Einmalzahlung gibt es keine wiederkehrende Rate. expect(betragPasstZumVertrag(4250n, 4250n, 'EINMALIG')).toBeNull() }) it('rechnet mit dem Betrag, nicht mit dem Vorzeichen', () => { expect(betragPasstZumVertrag(-4250n, 4250n, 'MONATLICH')).toBe(true) }) it('hält die dokumentierte Toleranz ein', () => { expect(BETRAGS_TOLERANZ_PROZENT).toBe(20n) // Genau an der Grenze: 42,50 + 20 % = 51,00. expect(betragPasstZumVertrag(5100n, 4250n, 'MONATLICH')).toBe(true) // Knapp darüber nicht mehr. expect(betragPasstZumVertrag(5200n, 4250n, 'MONATLICH')).toBe(false) }) }) describe('Belegzustand', () => { it('zählt eine Dauerzahlung als belegt', () => { const b = buchung(MONATLICH) expect(belegDurchVertrag(b)).toBe(true) expect(belegFehlt(b)).toBe(false) expect(belegBegruendet(b)).toBe(false) }) it('lässt die Anerkennung bei abweichendem Betrag wegfallen', () => { // Der wichtigste Test: Sonst wäre die Zuordnung eine Hintertür. const b = buchung({ ...MONATLICH, betragCent: 42_000n }) expect(belegDurchVertrag(b)).toBe(false) expect(betragPruefen(b)).toBe(true) expect(belegFehlt(b)).toBe(true) }) it('erkennt eine Zuordnung an, wenn am Vertrag kein Beitrag steht', () => { // Zuordnung gilt, aber es wird nicht behauptet, sie sei geprüft. const b = buchung({ vertragId: 'v1', vertragBeitragCent: null, vertragRhythmus: null }) expect(belegDurchVertrag(b)).toBe(true) expect(betragPruefen(b)).toBe(false) }) it('lässt einen echten Beleg vorgehen', () => { const b = buchung({ ...MONATLICH, belegId: 'd1' }) expect(belegDurchVertrag(b)).toBe(false) expect(belegFehlt(b)).toBe(false) }) it('bleibt ohne Zuordnung beim alten Verhalten', () => { expect(belegFehlt(buchung())).toBe(true) expect(belegBegruendet(buchung({ belegFehltBegruendung: 'Barzahlung' }))).toBe(true) expect(belegFehlt(buchung({ belegFehltBegruendung: 'Barzahlung' }))).toBe(false) }) it('lässt eine Begründung nicht neben den Vertrag treten', () => { // Der Vertrag ist die stärkere Aussage; doppelt gezählt würde die // Summe der Kategorien die Gesamtzahl übersteigen. const b = buchung({ ...MONATLICH, belegFehltBegruendung: 'Dauerauftrag' }) expect(belegDurchVertrag(b)).toBe(true) expect(belegBegruendet(b)).toBe(false) }) }) describe('Belegquote', () => { it('weist die drei Fälle getrennt aus', () => { const quote = belegquote([ buchung({ id: '1', belegId: 'd1' }), buchung({ id: '2', ...MONATLICH }), buchung({ id: '3', belegFehltBegruendung: 'Barzahlung' }), buchung({ id: '4' }), ]) expect(quote.gesamt).toBe(4) expect(quote.mitBeleg).toBe(1) expect(quote.durchVertrag).toBe(1) expect(quote.begruendet).toBe(1) expect(quote.ohneNachweis).toBe(1) expect(quote.quoteProzent).toBe(75) }) it('rechnet die Kategorien überschneidungsfrei', () => { // Jede Buchung zählt in genau eine Kategorie - sonst ergäbe die Summe // mehr als das Ganze. const buchungen = [ buchung({ id: '1', belegId: 'd1' }), buchung({ id: '2', ...MONATLICH }), buchung({ id: '3', ...MONATLICH, belegFehltBegruendung: 'egal' }), buchung({ id: '4', belegFehltBegruendung: 'Barzahlung' }), buchung({ id: '5' }), buchung({ id: '6', ...MONATLICH, betragCent: 99_000n }), ] const q = belegquote(buchungen) expect(q.mitBeleg + q.durchVertrag + q.begruendet + q.ohneNachweis).toBe(q.gesamt) }) it('zählt eine abweichende Zahlung als zu prüfen und nicht als belegt', () => { const quote = belegquote([buchung({ ...MONATLICH, betragCent: 42_000n })]) expect(quote.durchVertrag).toBe(0) expect(quote.zuPruefen).toBe(1) expect(quote.ohneNachweis).toBe(1) expect(quote.quoteProzent).toBe(0) }) it('bleibt bei null Buchungen bei 100 Prozent', () => { expect(belegquote([]).quoteProzent).toBe(100) }) })