import { ConsentType, ConsentStatus } from '@prisma/client'; import prisma from '../lib/prisma.js'; import fs from 'fs'; import path from 'path'; export interface UpdateConsentData { status: ConsentStatus; source?: string; documentPath?: string; version?: string; ipAddress?: string; createdBy: string; } /** * Holt alle Einwilligungen eines Kunden */ export async function getCustomerConsents(customerId: number) { const consents = await prisma.customerConsent.findMany({ where: { customerId }, orderBy: { consentType: 'asc' }, }); // Alle verfügbaren Consent-Typen mit Status const allTypes = Object.values(ConsentType); const consentMap = new Map(consents.map((c) => [c.consentType, c])); return allTypes.map((type) => { const existing = consentMap.get(type); return existing || { id: null, customerId, consentType: type, status: 'PENDING' as ConsentStatus, grantedAt: null, withdrawnAt: null, source: null, documentPath: null, version: null, ipAddress: null, createdBy: null, createdAt: null, updatedAt: null, }; }); } /** * Aktualisiert oder erstellt eine Einwilligung */ export async function updateConsent( customerId: number, consentType: ConsentType, data: UpdateConsentData ) { // Prüfen ob Kunde existiert const customer = await prisma.customer.findUnique({ where: { id: customerId }, }); if (!customer) { throw new Error('Kunde nicht gefunden'); } const now = new Date(); const updateData = { status: data.status, source: data.source, documentPath: data.documentPath, version: data.version, ipAddress: data.ipAddress, grantedAt: data.status === 'GRANTED' ? now : undefined, withdrawnAt: data.status === 'WITHDRAWN' ? now : undefined, }; const result = await prisma.customerConsent.upsert({ where: { customerId_consentType: { customerId, consentType }, }, update: updateData, create: { customerId, consentType, ...updateData, createdBy: data.createdBy, }, }); // Bei Widerruf: Datenschutz-PDF löschen wenn keine Einwilligung mehr besteht if (data.status === 'WITHDRAWN') { await deletePrivacyPdfOnWithdraw(customerId); } return result; } /** * Holt die Historie einer Einwilligung (aus Audit-Logs) */ export async function getConsentHistory(customerId: number, consentType: ConsentType) { // Aus Audit-Logs die Änderungen dieser Einwilligung abrufen const logs = await prisma.auditLog.findMany({ where: { resourceType: 'CustomerConsent', dataSubjectId: customerId, changesAfter: { contains: consentType }, }, orderBy: { createdAt: 'desc' }, take: 50, }); return logs; } /** * Prüft ob eine bestimmte Einwilligung erteilt wurde */ export async function hasConsent(customerId: number, consentType: ConsentType): Promise { const consent = await prisma.customerConsent.findUnique({ where: { customerId_consentType: { customerId, consentType }, }, }); return consent?.status === 'GRANTED'; } /** * Prüft ob ein Kunde die DSGVO-Einwilligung erfüllt hat. * Erfüllt = entweder privacyPolicyPath vorhanden ODER alle Online-Consents GRANTED. */ export async function hasFullConsent(customerId: number): Promise<{ hasConsent: boolean; hasPaperConsent: boolean; hasOnlineConsent: boolean; consentDetails: { type: string; status: string }[]; consentHash: string | null; }> { // Prüfe ob Papier-Datenschutzerklärung vorhanden const customer = await prisma.customer.findUnique({ where: { id: customerId }, select: { privacyPolicyPath: true, consentHash: true }, }); const hasPaperConsent = !!customer?.privacyPolicyPath; // Online-Consents prüfen const allTypes = Object.values(ConsentType); const consents = await prisma.customerConsent.findMany({ where: { customerId }, }); const consentMap = new Map(consents.map((c) => [c.consentType, c.status])); const consentDetails = allTypes.map((type) => ({ type, status: (consentMap.get(type) || 'PENDING') as string, })); const hasOnlineConsent = allTypes.every( (type) => consentMap.get(type) === 'GRANTED' ); return { hasConsent: hasPaperConsent || hasOnlineConsent, hasPaperConsent, hasOnlineConsent, consentDetails, consentHash: customer?.consentHash || null, }; } /** * Widerruft alle Einwilligungen eines Kunden */ export async function withdrawAllConsents(customerId: number, withdrawnBy: string) { const result = await prisma.customerConsent.updateMany({ where: { customerId, status: 'GRANTED', }, data: { status: 'WITHDRAWN', withdrawnAt: new Date(), }, }); // Datenschutz-PDF löschen await deletePrivacyPdfOnWithdraw(customerId); return result; } /** * Löscht die Datenschutz-PDF bei Widerruf. * Sobald auch nur eine Einwilligung widerrufen wird, ist die Gesamteinwilligung ungültig. */ async function deletePrivacyPdfOnWithdraw(customerId: number) { const customer = await prisma.customer.findUnique({ where: { id: customerId }, select: { privacyPolicyPath: true }, }); if (customer?.privacyPolicyPath) { try { const filePath = path.join(process.cwd(), customer.privacyPolicyPath); if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); } } catch (err) { console.error('Fehler beim Löschen der Datenschutz-PDF:', err); } await prisma.customer.update({ where: { id: customerId }, data: { privacyPolicyPath: null }, }); console.log(`Datenschutz-PDF für Kunde ${customerId} gelöscht (Einwilligung widerrufen)`); } } /** * Consent-Übersicht für DSGVO-Dashboard */ export async function getConsentOverview() { const allConsents = await prisma.customerConsent.groupBy({ by: ['consentType', 'status'], _count: { id: true }, }); // Gruppieren nach Typ const overview: Record = {}; for (const type of Object.values(ConsentType)) { overview[type] = { granted: 0, withdrawn: 0, pending: 0 }; } for (const row of allConsents) { const type = row.consentType; const status = row.status.toLowerCase() as 'granted' | 'withdrawn' | 'pending'; overview[type][status] = row._count.id; } return overview; } /** * Consent-Typ Labels für UI */ export const CONSENT_TYPE_LABELS: Record = { DATA_PROCESSING: { label: 'Datenverarbeitung', description: 'Grundlegende Verarbeitung personenbezogener Daten zur Vertragserfüllung', }, MARKETING_EMAIL: { label: 'Elektronisches Marketing', description: 'Zusendung von Werbung und Angeboten über elektronische Kommunikationswege (E-Mail, Messenger etc.)', }, MARKETING_PHONE: { label: 'Telefonmarketing', description: 'Kontaktaufnahme zu Werbezwecken per Telefon', }, DATA_SHARING_PARTNER: { label: 'Datenweitergabe', description: 'Weitergabe von Daten an Partnerunternehmen', }, };