Der Demofall brachte Konten und Kontostaende mit, aber keine einzige Buchung. Kassenbuch, Belegpflicht, Dauerzahlungen und die Erkennung wiederkehrender Zahlungen liefen darin also leer - wer die Anwendung zum ersten Mal ansah, musste erst eine CSV importieren. Neu sind 107 Buchungen ueber zwoelf Monate, erzeugt aus einem Muster statt einzeln abgetippt: Rente, Betriebsrente und Pflegekasse als Einnahmen, Heimentgelt, Barbetrag, Sterbegeldbeitrag, Telefon und Kontofuehrung monatlich, dazu Rundfunkbeitrag und Grundsteuer vierteljaehrlich sowie eine Handvoll Einzelposten. Was der Fall damit zeigt: - Das Heimentgelt steht in drei Schreibweisen auf dem Auszug und wird trotzdem als eine monatliche Zahlung gefuehrt. - Genau eine Buchung hat weder Quittung noch Begruendung. Eine, nicht zwanzig: Die Liste soll etwas zu zeigen haben und nicht rot leuchten. - Die Sterbegeld-Lastschriften bleiben unzugeordnet, damit sich der Handgriff "zwoelf Zahlungen als belegt zuordnen" ausprobieren laesst. Der Beitrag steigt im Mai von 18,40 auf 19,80 - damit auch die Abweichungspruefung sichtbar wird. - Der Kontostand zum 31.07. wird aus Anfangsstand und Buchungen gerechnet, nicht behauptet. Ein Beispielfall, der sich selbst widerspricht, waere schlimmer als einer ohne Buchungen. Zwei Funde beim Bauen: Der Unique-Index auf (kontoId, importReferenz) hat einen echten Fehler in den Daten gefangen: Zwei Renten am Monatsersten ergaben dieselbe Referenz. Genau dafuer ist er da. "gGmbH" fehlte in der Rechtsformliste. Dadurch galten "Seniorenzentrum St. Katharina" und "... gGmbH" als zwei Empfaenger - und die Gruppe zerfiel ausgerechnet in dem Fachgebiet, in dem fast jeder Traeger eine gGmbH ist. Ergaenzt, mit Test. Geprueft: ein Einheitstest fuer die Rechtsform, vier End-to-End-Tests am Beispielfall - Umfang und Zeitraum, Kontostand gegen Buchungssumme, die erkannten Gruppen und das Kassenbuch. Refs #22
852 lines
31 KiB
TypeScript
852 lines
31 KiB
TypeScript
/**
|
|
* 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<string, { wert: number; quelle: string }> = {
|
|
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())
|