/** * Startdaten. * * npm run db:seed * * Der Seed legt bewusst KEINE echten Personendaten an. Er richtet nur den * ersten Zugang ein; alles Weitere fragt der Ersteinrichtungs-Assistent * beim ersten Start ab. So landen keine Gesundheitsdaten im Repository, * und der Seed bleibt auch dann brauchbar, wenn eine zweite Betreuung * dazukommt. * * Zwei Schalter fuer die Entwicklung: * * SEED_DEMO=true legt einen vollstaendigen Beispielfall an * (Erika Musterfrau). Nur ausserhalb der * Produktion, und in der Oberflaeche als * Demo gekennzeichnet. * SEED_DEV_TOTP_SECRET=... aktiviert den zweiten Faktor mit einem * bekannten Geheimnis, damit automatisierte * Tests sich anmelden koennen. Wird in der * Produktion ignoriert. * * Der Seed ist idempotent: mehrfaches Ausfuehren aendert nichts. */ import { PrismaClient, type BuchungKategorie, type Prisma } from '@prisma/client' import { hash } from '@node-rs/argon2' import { createCipheriv, randomBytes } from 'node:crypto' const db = new PrismaClient() // Muss zu ARGON2_OPTIONEN in src/lib/crypto/hash.ts passen. // Ohne Angabe benutzt @node-rs/argon2 von sich aus Argon2id. const ARGON2 = { memoryCost: 19_456, timeCost: 2, parallelism: 1, } as const const istProduktion = process.env.NODE_ENV === 'production' /** * Verschluesselt wie src/lib/crypto/feld.ts. Bewusst hier noch einmal * ausgeschrieben: der Seed laeuft ausserhalb von Next.js und kann das * Modul mit seinem "server-only"-Import nicht laden. */ function verschluesseln(klartext: string | null, kontext: string): string | null { if (!klartext) return null const version = Number.parseInt(process.env.ENC_KEY_AKTIV ?? '1', 10) const roh = process.env[`ENC_KEY_V${version}`] if (!roh) throw new Error(`ENC_KEY_V${version} fehlt. Erzeuge ihn mit "npm run schluessel".`) const schluessel = Buffer.from(roh, 'base64') if (schluessel.length !== 32) throw new Error(`ENC_KEY_V${version} muss 32 Bytes lang sein.`) const iv = randomBytes(12) const cipher = createCipheriv('aes-256-gcm', schluessel, iv, { authTagLength: 16 }) cipher.setAAD(Buffer.from(`bc1:${version}:${kontext}`, 'utf8')) const geheim = Buffer.concat([cipher.update(klartext, 'utf8'), cipher.final()]) return [ 'bc1', version, iv.toString('base64url'), cipher.getAuthTag().toString('base64url'), geheim.toString('base64url'), ].join('.') } function datum(text: string): Date { return new Date(`${text}T00:00:00.000Z`) } function cent(euro: number): bigint { return BigInt(Math.round(euro * 100)) } async function ersterZugang() { const email = (process.env.SEED_ADMIN_EMAIL ?? '').trim().toLowerCase() const passwort = process.env.SEED_ADMIN_PASSWORT if (!email || !passwort) { console.log( 'Kein SEED_ADMIN_EMAIL / SEED_ADMIN_PASSWORT gesetzt - der erste Zugang wird beim ' + 'Aufruf von /ersteinrichtung im Browser angelegt.', ) return null } const vorhanden = await db.user.findUnique({ where: { email } }) if (vorhanden) { console.log(`Zugang ${email} besteht bereits.`) return vorhanden } // Der zweite Faktor bleibt normalerweise offen und wird beim ersten // Anmelden eingerichtet - nur die Entwicklung darf ihn vorbelegen. const devSecret = !istProduktion ? process.env.SEED_DEV_TOTP_SECRET : undefined const benutzer = await db.user.create({ data: { email, name: process.env.SEED_ADMIN_NAME ?? 'Betreuer', passwortHash: await hash(passwort, ARGON2), rolle: 'ADMIN', ...(devSecret ? { totpSecretVerschluesselt: verschluesseln(devSecret, 'totpSecretVerschluesselt'), totpAktiviertAm: new Date(), } : {}), }, }) console.log(`Zugang ${email} angelegt.`) if (devSecret) { console.log(' Zweiter Faktor mit dem Entwicklungs-Geheimnis vorbelegt.') } else { console.log(' Der zweite Faktor wird beim ersten Anmelden eingerichtet.') } return benutzer } /** * Vollstaendiger Beispielfall. Nur mit SEED_DEMO=true und nie in der * Produktion - er soll zeigen, wie die Anwendung mit Inhalt aussieht. */ async function demofall(userId: string) { const vorhanden = await db.betreute.findFirst({ where: { nachname: 'Musterfrau', vorname: 'Erika' }, }) if (vorhanden) { console.log('Demofall besteht bereits.') return } const betreute = await db.betreute.create({ data: { anrede: 'Frau', vorname: 'Erika', nachname: 'Musterfrau', geburtsname: 'Beispiel', geburtsdatum: datum('1943-04-17'), geburtsort: 'Bayreuth', heimName: 'Seniorenzentrum St. Katharina', heimStrasse: 'Kirchweg 12', heimPlz: '95444', heimOrt: 'Bayreuth', heimZimmer: '218', heimTelefon: '0921 123456-218', heimEinzugAm: datum('2026-01-19'), fruehereStrasse: 'Am Sonnenhang 4', frueherePlz: '95463', fruehererOrt: 'Bindlach', krankenkasse: 'AOK Bayern', krankenversichertennummerVerschluesselt: verschluesseln( 'A123456789', 'krankenversichertennummerVerschluesselt', ), pflegekasse: 'AOK Pflegekasse Bayern', pflegegrad: 3, pflegegradSeit: datum('2025-11-01'), pflegegradBescheidVom: datum('2025-10-22'), schwerbehinderungGdB: 80, schwerbehinderungMerkzeichen: 'G, B', diagnosenVerschluesselt: verschluesseln( [ 'COPD GOLD III mit chronischer respiratorischer Insuffizienz (J44.09)', 'Langzeitsauerstofftherapie seit 03/2025, 2 l/min, 16 h/Tag', 'Arterielle Hypertonie (I10)', 'Osteoporose mit Wirbelkörperfraktur BWK 12 (M80.08)', 'Beginnende vaskuläre Demenz (F01.9)', ].join('\n'), 'diagnosenVerschluesselt', ), ermittelterWilleVerschluesselt: verschluesseln( 'Möchte im Seniorenzentrum bleiben, dort ist eine frühere Nachbarin. ' + 'Will das Haus in Bindlach "nicht einfach hergeben", versteht aber, dass die ' + 'Pflegekosten gedeckt werden müssen. Wünscht keine künstliche Beatmung und keine ' + 'Verlegung in eine Klinik, wenn es zum Ende geht — so auch in der Patientenverfügung ' + 'vom 08.03.2019 festgehalten. Besuche bitte nachmittags, vormittags ist sie erschöpft.', 'ermittelterWilleVerschluesselt', ), patientenverfuegungVorhanden: true, vorsorgevollmachtVorhanden: false, betreuungsgericht: 'Amtsgericht Bayreuth', aktenzeichenGericht: 'XVII 412/25', bestellungAm: datum('2026-02-14'), aufgabenkreise: [ 'VERMOEGENSSORGE', 'GESUNDHEITSSORGE', 'AUFENTHALTSBESTIMMUNG', 'WOHNUNGSANGELEGENHEITEN', 'BEHOERDENANGELEGENHEITEN', ], befreiterBetreuer: true, ehrenamtlich: true, aktenzeichenBezirk: 'HzP 2026/04711', aktenzeichenSozialamt: 'SA-4711-26', rentenversicherungsnummerVerschluesselt: verschluesseln( '17 170443 M 512', 'rentenversicherungsnummerVerschluesselt', ), notizenVerschluesselt: verschluesseln( 'Dies ist ein Demofall zum Ausprobieren. Er lässt sich unter ' + 'Einstellungen jederzeit vollständig löschen.', 'notizenVerschluesselt', ), zugaenge: { create: { userId, rolle: 'BETREUER' } }, }, }) // --- Kontakte ----------------------------------------------------------- await db.kontakt.createMany({ data: [ { betreuteId: betreute.id, name: 'Frau Hartmann', organisation: 'Amtsgericht Bayreuth', rolle: 'RECHTSPFLEGER', telefon: '0921 504-0', aktenzeichen: 'XVII 412/25', }, { betreuteId: betreute.id, name: 'Herr Doblinger', organisation: 'Bezirk Oberfranken, Sozialverwaltung', rolle: 'SACHBEARBEITER_BEZIRK', telefon: '0921 604-1234', email: 'sozialverwaltung@bezirk-oberfranken.de', aktenzeichen: 'HzP 2026/04711', }, { betreuteId: betreute.id, name: 'Frau Kellner', organisation: 'Seniorenzentrum St. Katharina', rolle: 'SOZIALDIENST_HEIM', telefon: '0921 123456-30', }, { betreuteId: betreute.id, name: 'Dr. med. Reuther', organisation: 'Hausarztpraxis Bindlach', rolle: 'HAUSARZT', telefon: '09208 4477', }, ], }) // --- Konten und Vermoegen ---------------------------------------------- const giro = await db.konto.create({ data: { betreuteId: betreute.id, bezeichnung: 'Girokonto', bank: 'Sparkasse Bayreuth', art: 'GIRO', ibanVerschluesselt: verschluesseln('DE02773501100123456789', 'ibanVerschluesselt'), saldostaende: { // Nur der Anfangsstand. Der spaetere wird weiter unten aus den // Buchungen gerechnet - ein Demofall, der sich selbst // widerspricht, waere schlimmer als einer ohne Buchungen. create: [ { stichtag: datum('2026-02-14'), betragCent: cent(4218.36), quelle: 'Kontoauszug 02/2026' }, ], }, }, }) await db.konto.create({ data: { betreuteId: betreute.id, bezeichnung: 'Verwahrgeld im Heim', bank: 'Seniorenzentrum St. Katharina', art: 'VERWAHRGELD_HEIM', saldostaende: { create: { stichtag: datum('2026-07-31'), betragCent: cent(184.2), quelle: 'Abrechnung Heim' }, }, }, }) const immobilie = await db.immobilie.create({ data: { betreuteId: betreute.id, bezeichnung: 'Einfamilienhaus Bindlach', strasse: 'Am Sonnenhang 4', plz: '95463', ort: 'Bindlach', grundbuchamt: 'Amtsgericht Bayreuth', grundbuchBlatt: '3241', gemarkung: 'Bindlach', flurstueck: '812/3', eigentumsanteil: '1/1', wohnflaecheQm: 128, grundstueckQm: 640, baujahr: 1972, bewohntStatus: 'leerstehend', leerstandSeit: datum('2026-01-19'), statusSozialhilfe: 'UNGEKLAERT', statusBegruendungVerschluesselt: verschluesseln( 'Nicht mehr selbst bewohnt, seit dem Heimeinzug leerstehend. Schutz nach ' + '§ 90 Abs. 2 Nr. 8 SGB XII greift daher voraussichtlich nicht. Zu prüfen: ' + 'Härtefall nach § 90 Abs. 3 SGB XII und Darlehen nach § 91 SGB XII statt ' + 'sofortiger Verwertung. Mit dem Bezirk noch nicht abschließend geklärt.', 'statusBegruendungVerschluesselt', ), belastungen: { create: { art: 'Grundschuld', abteilung: 'III', laufendeNr: '2', glaeubiger: 'Sparkasse Bayreuth', betragCent: cent(30000), bemerkung: 'Valutiert nach Auskunft der Bank nicht mehr, Löschungsbewilligung angefordert.', }, }, }, }) const lebensversicherung = await db.vertrag.create({ data: { betreuteId: betreute.id, bezeichnung: 'Kapitallebensversicherung', gesellschaft: 'Musterland Leben AG', art: 'LEBENSVERSICHERUNG', status: 'BEITRAGSFREI', vertragsnummerVerschluesselt: verschluesseln('LV-8842119', 'vertragsnummerVerschluesselt'), beginnAm: datum('1998-07-01'), endeAm: datum('2028-07-01'), bewertungSozialhilfe: 'STRITTIG', bewertungBegruendungVerschluesselt: verschluesseln( 'Rückkaufswert liegt über dem Schonvermögen. Verwertung wäre grundsätzlich zumutbar, ' + 'aber der Vertrag läuft 2028 aus — Kündigung jetzt bedeutet spürbaren Verlust. ' + 'Härtefall nach § 90 Abs. 3 SGB XII geltend gemacht, Antwort des Bezirks steht aus.', 'bewertungBegruendungVerschluesselt', ), rueckkaufswertAngefragtAm: datum('2026-03-02'), }, }) await db.vertrag.create({ data: { betreuteId: betreute.id, bezeichnung: 'Sterbegeldversicherung', gesellschaft: 'Nordheim Sterbekasse VVaG', art: 'STERBEGELDVERSICHERUNG', status: 'AKTIV', vertragsnummerVerschluesselt: verschluesseln('SG-220417', 'vertragsnummerVerschluesselt'), beitragCent: cent(18.4), rhythmus: 'MONATLICH', bewertungSozialhilfe: 'GESCHUETZT', bewertungBegruendungVerschluesselt: verschluesseln( 'Angemessene Bestattungsvorsorge. Nach § 90 Abs. 3 SGB XII und ständiger ' + 'Rechtsprechung regelmäßig geschützt, weil sie der Würde der Bestattung dient.', 'bewertungBegruendungVerschluesselt', ), }, }) await db.vermoegensposten.createMany({ data: [ { betreuteId: betreute.id, bezeichnung: 'Girokonto Sparkasse', art: 'KONTO', kontoId: giro.id, bewertungSozialhilfe: 'VERWERTBAR' }, { betreuteId: betreute.id, bezeichnung: 'Einfamilienhaus Bindlach', art: 'IMMOBILIE', immobilieId: immobilie.id, bewertungSozialhilfe: 'STRITTIG' }, { betreuteId: betreute.id, bezeichnung: 'Kapitallebensversicherung', art: 'VERSICHERUNG', vertragId: lebensversicherung.id, bewertungSozialhilfe: 'STRITTIG' }, ], }) const posten = await db.vermoegensposten.findMany({ where: { betreuteId: betreute.id } }) for (const p of posten) { const werte: Record = { IMMOBILIE: { wert: 268000, quelle: 'Bodenrichtwert und Vergleichspreise, eigene Schätzung' }, VERSICHERUNG: { wert: 14820.55, quelle: 'Rückkaufswertmitteilung vom 26.03.2026' }, KONTO: { wert: 2967.11, quelle: 'Kontoauszug 07/2026' }, } const w = werte[p.art] if (w) { await db.wertstand.create({ data: { vermoegenspostenId: p.id, stichtag: datum('2026-07-31'), wertCent: cent(w.wert), wertquelle: w.quelle, }, }) } } // --- Sozialhilfeantrag mit Nachforderungen ------------------------------ const antrag = await db.sozialhilfeAntrag.create({ data: { betreuteId: betreute.id, traeger: 'Bezirk Oberfranken', leistungsart: 'Hilfe zur Pflege nach dem 7. Kapitel SGB XII', aktenzeichen: 'HzP 2026/04711', sachbearbeiter: 'Herr Doblinger', telefon: '0921 604-1234', antragAm: datum('2026-02-26'), eingangBeimTraegerAm: datum('2026-03-02'), }, }) await db.nachforderung.createMany({ data: [ { antragId: antrag.id, bezeichnung: 'Rentenbescheide aller Träger', status: 'ANERKANNT', angefordertAm: datum('2026-03-18'), uebersandtAm: datum('2026-03-26'), ausCheckliste: true, reihenfolge: 1 }, { antragId: antrag.id, bezeichnung: 'Kontoauszüge der letzten 3 Monate, lückenlos', status: 'ANERKANNT', angefordertAm: datum('2026-03-18'), uebersandtAm: datum('2026-03-26'), ausCheckliste: true, reihenfolge: 2 }, { antragId: antrag.id, bezeichnung: 'Pflegegradbescheid', status: 'ANERKANNT', angefordertAm: datum('2026-03-18'), uebersandtAm: datum('2026-03-26'), ausCheckliste: true, reihenfolge: 3 }, { antragId: antrag.id, bezeichnung: 'Heimvertrag', status: 'UEBERSANDT', angefordertAm: datum('2026-03-18'), uebersandtAm: datum('2026-04-08'), ausCheckliste: true, reihenfolge: 4 }, { antragId: antrag.id, bezeichnung: 'Aktueller Grundbuchauszug', status: 'UEBERSANDT', angefordertAm: datum('2026-04-21'), uebersandtAm: datum('2026-05-06'), ausCheckliste: true, reihenfolge: 5 }, { antragId: antrag.id, bezeichnung: 'Rückkaufswert Kapitallebensversicherung zum Antragsdatum', status: 'IN_BESCHAFFUNG', angefordertAm: datum('2026-04-21'), fristBis: datum('2026-09-05'), erlaeuterung: 'Zweite Anfrage an die Gesellschaft am 12.08.2026 raus.', ausCheckliste: true, reihenfolge: 6 }, { antragId: antrag.id, bezeichnung: 'Notarielle Verträge früherer Grundstücksübertragungen', status: 'OFFEN', angefordertAm: datum('2026-04-21'), fristBis: datum('2026-09-05'), erlaeuterung: 'Der Träger prüft eine Rückforderung wegen Verarmung (§ 528 BGB). Notariat Bayreuth angefragt.', ausCheckliste: true, reihenfolge: 7 }, { antragId: antrag.id, bezeichnung: 'Erklärung zu Unterhaltspflichtigen', status: 'OFFEN', angefordertAm: datum('2026-07-30'), fristBis: datum('2026-08-31'), ausCheckliste: true, reihenfolge: 8 }, ], }) // --- Vorgaenge ---------------------------------------------------------- await db.vorgang.create({ data: { betreuteId: betreute.id, titel: 'Antrag Hilfe zur Pflege beim Bezirk Oberfranken', kategorie: 'SOZIALHILFE', status: 'WARTET_AUF_DRITTE', gegenstelle: 'Bezirk Oberfranken', aktenzeichen: 'HzP 2026/04711', naechsterSchritt: 'Rückkaufswert und Übertragungsverträge nachreichen, dann Sachstand erfragen', letzterKontaktAm: datum('2026-07-30'), angelegtVonId: userId, beschreibungVerschluesselt: verschluesseln( 'Antrag vom 26.02.2026. Der Bezirk hat in zwei Wellen nachgefordert. ' + 'Offen sind noch der Rückkaufswert der Lebensversicherung und die notariellen ' + 'Übertragungsverträge aus den 1990er-Jahren.', 'beschreibungVerschluesselt', ), }, }) await db.vorgang.create({ data: { betreuteId: betreute.id, titel: 'Haus Bindlach: Verwertung oder Darlehen klären', kategorie: 'IMMOBILIE', status: 'IN_ARBEIT', gegenstelle: 'Bezirk Oberfranken', genehmigungspflichtig: true, naechsterSchritt: 'Verkehrswert belastbar ermitteln lassen, dann Gespräch mit dem Bezirk', angelegtVonId: userId, beschreibungVerschluesselt: verschluesseln( 'Das Haus ist seit dem Heimeinzug leerstehend. Ein Verkauf wäre nach § 1850 BGB ' + 'genehmigungspflichtig. Vorrangig prüfen: Darlehen nach § 91 SGB XII gegen ' + 'Grundschuld, damit nicht unter Zeitdruck verkauft werden muss.', 'beschreibungVerschluesselt', ), }, }) await db.vorgang.create({ data: { betreuteId: betreute.id, titel: 'Aufwandspauschale für das erste Betreuungsjahr beantragen', kategorie: 'GERICHT', status: 'OFFEN', gegenstelle: 'Amtsgericht Bayreuth', aktenzeichen: 'XVII 412/25', naechsterSchritt: 'Antrag nach Ablauf des Betreuungsjahres am 13.02.2027 stellen', angelegtVonId: userId, }, }) // --- Kontaktprotokolle -------------------------------------------------- const besuche: { tag: string; dauer: number; inhalt: string; wunsch?: string }[] = [ { tag: '2026-06-14', dauer: 75, inhalt: 'Besuch im Heim. Gemeinsam im Garten gesessen. Atmung an diesem Tag ruhig, Sauerstoff 2 l/min. Über das Haus gesprochen.', wunsch: 'Möchte das Haus nicht verkaufen, solange es nicht sein muss.' }, { tag: '2026-07-05', dauer: 60, inhalt: 'Besuch. Erschöpft, blieb im Zimmer. Pflegedienstleitung berichtet von zwei kurzen Luftnotepisoden in der Vorwoche.' }, { tag: '2026-07-22', dauer: 30, inhalt: 'Telefonat mit dem Sozialdienst wegen der Heimabrechnung Juli.' }, { tag: '2026-08-09', dauer: 90, inhalt: 'Besuch mit Enkelin. Deutlich wacher als beim letzten Mal. Wünscht sich häufiger Besuch am Nachmittag.', wunsch: 'Besuche bitte nachmittags, vormittags ist sie zu müde.' }, ] for (const b of besuche) { await db.kontaktprotokoll.create({ data: { betreuteId: betreute.id, datum: new Date(`${b.tag}T15:00:00.000Z`), art: b.dauer === 30 ? 'TELEFONAT' : 'BESUCH', dauerMinuten: b.dauer, mitBetreuter: b.dauer !== 30, inhaltVerschluesselt: verschluesseln(b.inhalt, 'inhaltVerschluesselt'), wuenscheVerschluesselt: b.wunsch ? verschluesseln(b.wunsch, 'wuenscheVerschluesselt') : null, }, }) } // --- Gesundheitsverlauf ------------------------------------------------- await db.gesundheitsereignis.createMany({ data: [ { betreuteId: betreute.id, datum: datum('2026-03-11'), art: 'EXAZERBATION', titel: 'Infektexazerbation, ambulant behandelt', behandler: 'Dr. Reuther', beschreibungVerschluesselt: verschluesseln('Antibiotikum und kurzzeitig Prednisolon. Keine Einweisung nötig.', 'beschreibungVerschluesselt') }, { betreuteId: betreute.id, datum: datum('2026-05-02'), bisDatum: datum('2026-05-09'), art: 'KRANKENHAUS', titel: 'Stationär wegen Luftnot', einrichtung: 'Klinikum Bayreuth', abwesenheitstage: 7, beschreibungVerschluesselt: verschluesseln('Aufnahme über die Notaufnahme. Sauerstoffbedarf danach von 1,5 auf 2 l/min erhöht.', 'beschreibungVerschluesselt') }, { betreuteId: betreute.id, datum: datum('2026-06-20'), art: 'HILFSMITTEL', titel: 'Mobiler Sauerstoffkonzentrator bewilligt', beschreibungVerschluesselt: verschluesseln('Ermöglicht Ausflüge aus dem Zimmer.', 'beschreibungVerschluesselt') }, { betreuteId: betreute.id, datum: datum('2026-07-15'), art: 'IMPFUNG', titel: 'Pneumokokken-Impfung', behandler: 'Dr. Reuther' }, ], }) // --- Heimkosten --------------------------------------------------------- for (const monat of [5, 6, 7]) { await db.heimkostenmonat.create({ data: { betreuteId: betreute.id, jahr: 2026, monat, pflegeEigenanteilCent: cent(1489.0), unterkunftVerpflegungCent: cent(878.4), investitionskostenCent: cent(486.3), ausbildungsumlageCent: cent(112.5), pflegekasseCent: cent(1363.0), leistungszuschlagProzent: 15, leistungszuschlagCent: cent(223.35), renteCent: cent(1284.62), barbetragCent: cent(135.66), bekleidungspauschaleCent: cent(27.13), abwesenheitstage: monat === 5 ? 7 : 0, }, }) } // --- Kontobewegungen ---------------------------------------------------- // Zwoelf Monate auf dem Girokonto. Erzeugt aus einem Muster und nicht // einzeln abgetippt: So bleibt der Fall aenderbar, und die Betraege // widersprechen sich nicht. // // Die Gegenstellen sind absichtlich uneinheitlich geschrieben. Genau // damit muss die Zusammenfassung wiederkehrender Zahlungen umgehen - auf // einem Kontoauszug steht dieselbe Stelle auch mal in Grossbuchstaben. type Buchungsvorlage = { tag: number betrag: number richtung: 'EINNAHME' | 'AUSGABE' kategorie: BuchungKategorie gegenstellen: string[] zweck: string } const MONATLICH: Buchungsvorlage[] = [ { tag: 1, betrag: 1284.17, richtung: 'EINNAHME', kategorie: 'RENTE', gegenstellen: [ 'Deutsche Rentenversicherung Bund', 'DRV BUND', 'Deutsche Rentenversich. Bund', ], zweck: 'Altersrente', }, { tag: 1, betrag: 96.4, richtung: 'EINNAHME', kategorie: 'RENTE', gegenstellen: ['Zusatzversorgungskasse Bayern', 'ZVK Bayern'], zweck: 'Betriebsrente', }, { tag: 3, betrag: 1262, richtung: 'EINNAHME', kategorie: 'PFLEGEKASSE', gegenstellen: ['Pflegekasse bei der AOK Bayern', 'AOK Bayern - Pflegekasse'], zweck: 'Leistung vollstationaere Pflege, Pflegegrad 3', }, { tag: 5, betrag: 2489, richtung: 'AUSGABE', kategorie: 'HEIMKOSTEN', gegenstellen: [ 'Seniorenzentrum St. Katharina', 'SENIORENZENTRUM ST KATHARINA', 'Seniorenzentrum St. Katharina gGmbH', ], zweck: 'Heimentgelt', }, { tag: 5, betrag: 135, richtung: 'AUSGABE', kategorie: 'BARBETRAG', // Eigene Gegenstelle, nicht das Heim: Sonst faellt der Barbetrag // mit dem Heimentgelt in eine Gruppe, und aus zwei sauberen // monatlichen Zahlungen wird eine "schwankende". gegenstellen: ['Barauszahlung Verwahrgeldkonto'], zweck: 'Barbetrag zur persoenlichen Verfuegung', }, { tag: 15, betrag: 18.4, richtung: 'AUSGABE', kategorie: 'VERSICHERUNG', gegenstellen: ['Nordheim Sterbekasse VVaG', 'NORDHEIM STERBEKASSE', 'Nordheim Sterbekasse'], zweck: 'Beitrag Sterbegeldversicherung SG-220417', }, { tag: 20, betrag: 24.9, richtung: 'AUSGABE', kategorie: 'TELEFON_INTERNET', gegenstellen: ['TeleCare Kommunikation GmbH', 'TELECARE KOMMUNIKATION'], zweck: 'Telefon und Fernsehen im Zimmer', }, { tag: 28, betrag: 4.9, richtung: 'AUSGABE', kategorie: 'BANKGEBUEHREN', gegenstellen: ['Sparkasse Bayreuth'], zweck: 'Kontofuehrung', }, ] /** Zwoelf Monate bis einschliesslich Juli 2026. */ const MONATE = [ '2025-08', '2025-09', '2025-10', '2025-11', '2025-12', '2026-01', '2026-02', '2026-03', '2026-04', '2026-05', '2026-06', '2026-07', ] const buchungen: Prisma.BuchungCreateManyInput[] = [] MONATE.forEach((monat, lauf) => { MONATLICH.forEach((vorlage, stelle) => { // Die Sterbegeld-Lastschrift steigt im Mai 2026. Daran wird die // Pruefung beim Zuordnen einer Dauerzahlung sichtbar: Der Betrag // weicht vom hinterlegten Beitrag ab. const betrag = vorlage.kategorie === 'VERSICHERUNG' && monat >= '2026-05' ? 19.8 : vorlage.betrag buchungen.push({ betreuteId: betreute.id, kontoId: giro.id, datum: datum(`${monat}-${String(vorlage.tag).padStart(2, '0')}`), betragCent: cent(betrag), richtung: vorlage.richtung, kategorie: vorlage.kategorie, gegenstelle: vorlage.gegenstellen[lauf % vorlage.gegenstellen.length], verwendungszweckVerschluesselt: verschluesseln( vorlage.zweck, 'verwendungszweckVerschluesselt', ), // Wiederkehrende Zahlungen brauchen keinen Einzelbeleg - der // Bescheid bzw. der Vertrag dahinter ist der Nachweis. belegFehltBegruendung: vorlage.richtung === 'EINNAHME' ? 'Laufender Bezug, Nachweis ist der Bescheid.' : 'Wiederkehrende Zahlung aus laufendem Vertrag.', // Die Stelle in der Vorlagenliste gehoert in die Referenz: Zwei // Renten am Monatsersten haetten sonst dieselbe, und der // Unique-Index gegen Doppelbuchungen schlaegt zu Recht zu. importReferenz: `demo-${monat}-${stelle}-${vorlage.kategorie}`, }) }) }) // Vierteljaehrliches und Einmaliges - damit nicht alles im Monatsraster // liegt und die Belegpflicht etwas zu zeigen hat. const EINZELN: { datum: string betrag: number richtung: 'EINNAHME' | 'AUSGABE' kategorie: BuchungKategorie gegenstelle: string zweck: string begruendung?: string }[] = [ { datum: '2025-08-15', betrag: 55.08, richtung: 'AUSGABE', kategorie: 'RUNDFUNKBEITRAG', gegenstelle: 'ARD ZDF Deutschlandradio Beitragsservice', zweck: 'Rundfunkbeitrag 3. Quartal 2025', begruendung: 'Wiederkehrende Zahlung, Beitragsbescheid liegt vor.', }, { datum: '2025-11-15', betrag: 55.08, richtung: 'AUSGABE', kategorie: 'RUNDFUNKBEITRAG', gegenstelle: 'ARD ZDF Deutschlandradio Beitragsservice', zweck: 'Rundfunkbeitrag 4. Quartal 2025', begruendung: 'Wiederkehrende Zahlung, Beitragsbescheid liegt vor.', }, { datum: '2026-02-15', betrag: 55.08, richtung: 'AUSGABE', kategorie: 'RUNDFUNKBEITRAG', gegenstelle: 'ARD ZDF Deutschlandradio Beitragsservice', zweck: 'Rundfunkbeitrag 1. Quartal 2026', begruendung: 'Wiederkehrende Zahlung, Beitragsbescheid liegt vor.', }, { datum: '2026-05-15', betrag: 55.08, richtung: 'AUSGABE', kategorie: 'RUNDFUNKBEITRAG', gegenstelle: 'ARD ZDF Deutschlandradio Beitragsservice', zweck: 'Rundfunkbeitrag 2. Quartal 2026', begruendung: 'Wiederkehrende Zahlung, Beitragsbescheid liegt vor.', }, { datum: '2026-02-16', betrag: 187.5, richtung: 'AUSGABE', kategorie: 'GRUNDSTEUER', gegenstelle: 'Gemeinde Bindlach', zweck: 'Grundsteuer B, 1. Rate 2026', begruendung: 'Bescheid der Gemeinde liegt in der Akte.', }, { datum: '2026-05-16', betrag: 187.5, richtung: 'AUSGABE', kategorie: 'GRUNDSTEUER', gegenstelle: 'Gemeinde Bindlach', zweck: 'Grundsteuer B, 2. Rate 2026', begruendung: 'Bescheid der Gemeinde liegt in der Akte.', }, { datum: '2026-01-19', betrag: 240, richtung: 'AUSGABE', kategorie: 'IMMOBILIE_BETRIEBSKOSTEN', gegenstelle: 'Stadtwerke Bayreuth', zweck: 'Abschlag Strom und Wasser, Haus Bindlach', begruendung: 'Jahresabrechnung steht noch aus.', }, { datum: '2026-03-11', betrag: 10, richtung: 'AUSGABE', kategorie: 'ZUZAHLUNG', gegenstelle: 'Apotheke am Markt', zweck: 'Zuzahlung Rezept', begruendung: 'Quittung liegt in der Akte.', }, // Der Fall fuer die Belegpflicht: keine Quittung und keine Begruendung. // Genau so sieht eine Buchung aus, die noch Arbeit macht. { datum: '2026-04-08', betrag: 89.95, richtung: 'AUSGABE', kategorie: 'BEKLEIDUNG', gegenstelle: 'Modehaus Lindner', zweck: 'Winterjacke und zwei Nachthemden', }, { datum: '2026-06-17', betrag: 23, richtung: 'AUSGABE', kategorie: 'SONSTIGE_AUSGABE', gegenstelle: 'Friseur im Haus', zweck: 'Friseur', begruendung: 'Bar aus dem Barbetrag bezahlt, keine Quittung ausgestellt.', }, { datum: '2026-07-02', betrag: 62.4, richtung: 'EINNAHME', kategorie: 'ZINSEN', gegenstelle: 'Sparkasse Bayreuth', zweck: 'Zinsgutschrift', begruendung: 'Der Kontoauszug ist der Nachweis.', }, ] for (const eintrag of EINZELN) { buchungen.push({ betreuteId: betreute.id, kontoId: giro.id, datum: datum(eintrag.datum), betragCent: cent(eintrag.betrag), richtung: eintrag.richtung, kategorie: eintrag.kategorie, gegenstelle: eintrag.gegenstelle, verwendungszweckVerschluesselt: verschluesseln( eintrag.zweck, 'verwendungszweckVerschluesselt', ), belegFehltBegruendung: eintrag.begruendung ?? null, importReferenz: `demo-${eintrag.datum}-${eintrag.kategorie}`, }) } await db.buchung.createMany({ data: buchungen }) // Der Kontostand zum 31.07.2026 wird gerechnet, nicht behauptet: Anfangs- // stand plus alles, was seither ueber das Konto gelaufen ist. Sonst // widerspraeche der Demofall sich selbst, sobald jemand nachrechnet. const bewegungCent = buchungen .filter((eintrag) => { const tag = eintrag.datum as Date return tag > datum('2026-02-14') && tag <= datum('2026-07-31') }) .reduce((summe, eintrag) => { const betrag = eintrag.betragCent as bigint return eintrag.richtung === 'EINNAHME' ? summe + betrag : summe - betrag }, 0n) await db.saldostand.create({ data: { kontoId: giro.id, stichtag: datum('2026-07-31'), betragCent: cent(4218.36) + bewegungCent, quelle: 'Kontoauszug 07/2026', }, }) console.log('Demofall "Erika Musterfrau" angelegt.') } async function main() { const benutzer = await ersterZugang() if (process.env.SEED_DEMO === 'true' && !istProduktion && benutzer) { await demofall(benutzer.id) } else if (process.env.SEED_DEMO === 'true' && istProduktion) { console.log('SEED_DEMO wird in der Produktion nicht ausgefuehrt.') } console.log('\nFertig. Weiter geht es im Browser unter /ersteinrichtung.') } main() .catch((fehler) => { console.error('Seed fehlgeschlagen:', fehler) process.exit(1) }) .finally(() => db.$disconnect())