Files
betreuungs-cockpit/src/app/(app)/vertraege/neu/page.tsx
T
Florian Wilde aa0cc66292 Wiederkehrende Zahlungen aufspueren
Am Anfang einer Betreuung weiss niemand, welche Vertraege es gibt. Die
Antwort steht aber im Kontoauszug: Wer regelmaessig abbucht, hat einen
Vertrag. Neu unter Vermoegen & Kasse wird aus hunderten Buchungszeilen
eine Arbeitsliste.

Aufbau:
- Die Erkennung liegt als reine Funktionen in
  src/lib/finanzen/wiederkehrend.ts, ohne Datenbank. Nur so laesst sich
  jede Einordnung einzeln nachrechnen.
- Der Datenzugriff steht getrennt in wiederkehrend-laden.ts.
- Ein kleines Modell haelt fest, was bewusst KEIN Vertrag ist. Es haengt
  am normalisierten Empfaengernamen, nicht an Buchungen - nur so
  ueberlebt die Entscheidung den naechsten Import.

Zwei Haltungen liegen der Erkennung zugrunde:

Im Zweifel getrennt lassen. Zusammengefasst wird nur das Unstrittige:
Gross- und Kleinschreibung, Satzzeichen, Umlaute, Rechtsform am Ende.
Keine Aehnlichkeitssuche - "Stadtwerke Bamberg" und "Stadtwerke
Bayreuth" trennen drei Zeichen und zwei Rechnungssteller. Zwei Zeilen zu
viel sind harmlos; eine falsche Gruppe verbirgt einen Vertrag.

Nichts behaupten, was nicht belegt ist. Statt "monatlich" steht dort
"6 Zahlungen, etwa monatlich". Schwankt der Betrag, wird die Spanne
genannt. Ohne erkennbaren Abstand wird nicht hochgerechnet, sondern
gezeigt, was geflossen ist.

Dazu eine zweite Liste fuer einzelne groessere Zahlungen: Die
Gruppierung braucht zwei Zahlungen, ein Jahresbeitrag erscheint bei
einem importierten Jahr aber nur einmal - und ausgerechnet
Versicherungen zahlt man oft jaehrlich.

Geprueft: 37 Einheitstests, die Haelfte davon in der Gegenrichtung, plus
vier End-to-End-Tests auf beiden Geraeteprofilen. Der Browsertest hat
zwei Fehler gefunden, die Typpruefung und Einheitstests durchgelassen
haben - darunter ein verlorener Backslash, der die Vorbelegung des
Beitrags stillschweigend verschluckte.

Nebenbei: .claude/ ins .gitignore. Dort legen parallel arbeitende
Agenten ihre Worktrees ab; ein "git add -A" haette einen kompletten
zweiten Checkout eingecheckt.

Refs #3
2026-08-23 02:42:26 +02:00

96 lines
3.3 KiB
TypeScript

import { redirect } from 'next/navigation'
import { Info } from 'lucide-react'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { SeitenKopf } from '@/components/seiten-kopf'
import { LEERER_VERTRAG, VertragFormular } from '@/components/vertraege/vertrag-formular'
import { aktiveBetreute, erfordereBenutzer, erfordereZugang } from '@/lib/auth/sitzung'
import { alsZahlRhythmus, type Rhythmus } from '@/lib/finanzen/wiederkehrend'
import { betragFormatieren } from '@/lib/finanzen/betrag'
export const dynamic = 'force-dynamic'
/**
* Uebernimmt eine Vorbelegung aus der Adresszeile.
*
* Sie kommt von "Wiederkehrende Zahlungen": Dort steht schon, wer da
* abbucht und wie viel. Diese Angaben noch einmal abzutippen waere genau
* die Arbeit, die die Auswertung ersparen soll.
*
* Uebernommen wird nur, was aus dem Kontoauszug ablesbar ist - Gesellschaft,
* Beitrag, Rhythmus. Die Bezeichnung und die Vertragsart bleiben leer: Was
* fuer ein Vertrag das ist, steht nicht im Auszug, und eine geratene
* Vorbelegung wird uebersehen statt korrigiert.
*/
function vorbelegung(parameter: Record<string, string | string[] | undefined>) {
const text = (name: string): string => {
const wert = parameter[name]
return typeof wert === 'string' ? wert.trim() : ''
}
const werte = { ...LEERER_VERTRAG }
let etwasUebernommen = false
const gesellschaft = text('gesellschaft')
if (gesellschaft) {
werte.gesellschaft = gesellschaft.slice(0, 200)
etwasUebernommen = true
}
const cent = text('beitragCent')
if (/^[0-9]{1,15}$/.test(cent)) {
werte.beitrag = betragFormatieren(BigInt(cent))
etwasUebernommen = true
}
const rhythmus = alsZahlRhythmus(text('rhythmus') as Rhythmus)
if (rhythmus) {
werte.rhythmus = rhythmus as typeof werte.rhythmus
etwasUebernommen = true
}
return { werte, etwasUebernommen }
}
export default async function VertragAnlegen({
searchParams,
}: {
searchParams: Promise<Record<string, string | string[] | undefined>>
}) {
const parameter = await searchParams
const benutzer = await erfordereBenutzer('/vertraege/neu')
const aktive = await aktiveBetreute(benutzer)
if (!aktive) return null
const { darfSchreiben } = await erfordereZugang(aktive.id, 'VERTRAEGE', 'lesen')
if (!darfSchreiben('VERTRAEGE')) redirect('/vertraege')
const { werte, etwasUebernommen } = vorbelegung(parameter)
return (
<div className="space-y-6">
<SeitenKopf
titel="Vertrag anlegen"
beschreibung="Beginn und Kündigungsfrist sind die beiden Angaben, aus denen alles Weitere folgt. Stehen sie nicht in der Police, frag bei der Gesellschaft nach."
pfad={[
{ titel: 'Übersicht', href: '/heute' },
{ titel: 'Verträge', href: '/vertraege' },
{ titel: 'Anlegen' },
]}
/>
{etwasUebernommen && (
<Alert variant="hinweis">
<Info aria-hidden="true" />
<AlertDescription>
Gesellschaft, Beitrag und Rhythmus sind aus den wiederkehrenden Zahlungen
übernommen. Bezeichnung und Art musst du selbst wählen — was für ein Vertrag das
ist, steht nicht im Kontoauszug.
</AlertDescription>
</Alert>
)}
<VertragFormular betreuteId={aktive.id} werte={werte} />
</div>
)
}