import { Router, Response } from 'express'; import multer from 'multer'; import path from 'path'; import fs from 'fs'; import prisma from '../lib/prisma.js'; import { authenticate, requirePermission } from '../middleware/auth.js'; import { AuthRequest } from '../types/index.js'; import { logChange } from '../services/audit.service.js'; import { canAccessContract } from '../utils/accessControl.js'; const router = Router(); // Uploads-Verzeichnis erstellen falls nicht vorhanden const uploadsDir = path.join(process.cwd(), 'uploads'); if (!fs.existsSync(uploadsDir)) { fs.mkdirSync(uploadsDir, { recursive: true }); } // Multer-Konfiguration const storage = multer.diskStorage({ destination: (req, file, cb) => { const subDir = (req as any).uploadSubDir || 'misc'; const targetDir = path.join(uploadsDir, subDir); if (!fs.existsSync(targetDir)) { fs.mkdirSync(targetDir, { recursive: true }); } cb(null, targetDir); }, filename: (req, file, cb) => { const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9); const ext = path.extname(file.originalname); cb(null, `${uniqueSuffix}${ext}`); }, }); const fileFilter = ( req: Express.Request, file: Express.Multer.File, cb: multer.FileFilterCallback ) => { // Nur PDFs und Bilder erlauben const allowedTypes = ['application/pdf', 'image/jpeg', 'image/png', 'image/jpg']; if (allowedTypes.includes(file.mimetype)) { cb(null, true); } else { cb(new Error('Nur PDF, JPG und PNG Dateien sind erlaubt')); } }; const upload = multer({ storage, fileFilter, limits: { fileSize: 10 * 1024 * 1024, // 10MB max }, }); // Middleware um Subdirectory zu setzen function setUploadDir(subDir: string) { return (req: AuthRequest, res: Response, next: Function) => { (req as any).uploadSubDir = subDir; next(); }; } // Upload für Bankkarten-Dokumente router.post( '/bank-cards/:id', authenticate, requirePermission('customers:update'), setUploadDir('bank-cards'), upload.single('document'), async (req: AuthRequest, res: Response) => { try { if (!req.file) { res.status(400).json({ success: false, error: 'Keine Datei hochgeladen' }); return; } const bankCardId = parseInt(req.params.id); const relativePath = `/uploads/bank-cards/${req.file.filename}`; // Bankkarte in der DB aktualisieren await prisma.bankCard.update({ where: { id: bankCardId }, data: { documentPath: relativePath }, }); res.json({ success: true, data: { path: relativePath, filename: req.file.filename, originalName: req.file.originalname, size: req.file.size, }, }); } catch (error) { console.error('Upload error:', error); res.status(500).json({ success: false, error: 'Upload fehlgeschlagen' }); } } ); // Upload für Ausweis-Dokumente router.post( '/documents/:id', authenticate, requirePermission('customers:update'), setUploadDir('documents'), upload.single('document'), async (req: AuthRequest, res: Response) => { try { if (!req.file) { res.status(400).json({ success: false, error: 'Keine Datei hochgeladen' }); return; } const documentId = parseInt(req.params.id); const relativePath = `/uploads/documents/${req.file.filename}`; // Ausweis in der DB aktualisieren await prisma.identityDocument.update({ where: { id: documentId }, data: { documentPath: relativePath }, }); res.json({ success: true, data: { path: relativePath, filename: req.file.filename, originalName: req.file.originalname, size: req.file.size, }, }); } catch (error) { console.error('Upload error:', error); res.status(500).json({ success: false, error: 'Upload fehlgeschlagen' }); } } ); // Löschen von Bankkarten-Dokumenten router.delete( '/bank-cards/:id', authenticate, requirePermission('customers:update'), async (req: AuthRequest, res: Response) => { try { const bankCardId = parseInt(req.params.id); // Bankkarte aus DB holen um Dateipfad zu bekommen const bankCard = await prisma.bankCard.findUnique({ where: { id: bankCardId }, }); if (!bankCard) { res.status(404).json({ success: false, error: 'Bankkarte nicht gefunden' }); return; } if (!bankCard.documentPath) { res.status(400).json({ success: false, error: 'Kein Dokument vorhanden' }); return; } // Datei von Festplatte löschen const filePath = path.join(process.cwd(), bankCard.documentPath); if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); } // documentPath in DB auf null setzen await prisma.bankCard.update({ where: { id: bankCardId }, data: { documentPath: null }, }); res.json({ success: true }); } catch (error) { console.error('Delete error:', error); res.status(500).json({ success: false, error: 'Löschen fehlgeschlagen' }); } } ); // Löschen von Ausweis-Dokumenten router.delete( '/documents/:id', authenticate, requirePermission('customers:update'), async (req: AuthRequest, res: Response) => { try { const documentId = parseInt(req.params.id); // Ausweis aus DB holen um Dateipfad zu bekommen const document = await prisma.identityDocument.findUnique({ where: { id: documentId }, }); if (!document) { res.status(404).json({ success: false, error: 'Ausweis nicht gefunden' }); return; } if (!document.documentPath) { res.status(400).json({ success: false, error: 'Kein Dokument vorhanden' }); return; } // Datei von Festplatte löschen const filePath = path.join(process.cwd(), document.documentPath); if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); } // documentPath in DB auf null setzen await prisma.identityDocument.update({ where: { id: documentId }, data: { documentPath: null }, }); res.json({ success: true }); } catch (error) { console.error('Delete error:', error); res.status(500).json({ success: false, error: 'Löschen fehlgeschlagen' }); } } ); // ==================== FIRMEN-DOKUMENTE ==================== // Upload für Gewerbeanmeldung router.post( '/customers/:id/business-registration', authenticate, requirePermission('customers:update'), setUploadDir('business-registrations'), upload.single('document'), async (req: AuthRequest, res: Response) => { try { if (!req.file) { res.status(400).json({ success: false, error: 'Keine Datei hochgeladen' }); return; } const customerId = parseInt(req.params.id); const relativePath = `/uploads/business-registrations/${req.file.filename}`; // Alte Datei löschen falls vorhanden const customer = await prisma.customer.findUnique({ where: { id: customerId } }); if (customer?.businessRegistrationPath) { const oldPath = path.join(process.cwd(), customer.businessRegistrationPath); if (fs.existsSync(oldPath)) { fs.unlinkSync(oldPath); } } // Kunde in der DB aktualisieren await prisma.customer.update({ where: { id: customerId }, data: { businessRegistrationPath: relativePath }, }); res.json({ success: true, data: { path: relativePath, filename: req.file.filename, originalName: req.file.originalname, size: req.file.size, }, }); } catch (error) { console.error('Upload error:', error); res.status(500).json({ success: false, error: 'Upload fehlgeschlagen' }); } } ); // Upload für Handelsregisterauszug router.post( '/customers/:id/commercial-register', authenticate, requirePermission('customers:update'), setUploadDir('commercial-registers'), upload.single('document'), async (req: AuthRequest, res: Response) => { try { if (!req.file) { res.status(400).json({ success: false, error: 'Keine Datei hochgeladen' }); return; } const customerId = parseInt(req.params.id); const relativePath = `/uploads/commercial-registers/${req.file.filename}`; // Alte Datei löschen falls vorhanden const customer = await prisma.customer.findUnique({ where: { id: customerId } }); if (customer?.commercialRegisterPath) { const oldPath = path.join(process.cwd(), customer.commercialRegisterPath); if (fs.existsSync(oldPath)) { fs.unlinkSync(oldPath); } } // Kunde in der DB aktualisieren await prisma.customer.update({ where: { id: customerId }, data: { commercialRegisterPath: relativePath }, }); res.json({ success: true, data: { path: relativePath, filename: req.file.filename, originalName: req.file.originalname, size: req.file.size, }, }); } catch (error) { console.error('Upload error:', error); res.status(500).json({ success: false, error: 'Upload fehlgeschlagen' }); } } ); // Löschen der Gewerbeanmeldung router.delete( '/customers/:id/business-registration', authenticate, requirePermission('customers:update'), async (req: AuthRequest, res: Response) => { try { const customerId = parseInt(req.params.id); const customer = await prisma.customer.findUnique({ where: { id: customerId } }); if (!customer) { res.status(404).json({ success: false, error: 'Kunde nicht gefunden' }); return; } if (!customer.businessRegistrationPath) { res.status(400).json({ success: false, error: 'Kein Dokument vorhanden' }); return; } // Datei löschen const filePath = path.join(process.cwd(), customer.businessRegistrationPath); if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); } // Pfad in DB auf null setzen await prisma.customer.update({ where: { id: customerId }, data: { businessRegistrationPath: null }, }); res.json({ success: true }); } catch (error) { console.error('Delete error:', error); res.status(500).json({ success: false, error: 'Löschen fehlgeschlagen' }); } } ); // Löschen des Handelsregisterauszugs router.delete( '/customers/:id/commercial-register', authenticate, requirePermission('customers:update'), async (req: AuthRequest, res: Response) => { try { const customerId = parseInt(req.params.id); const customer = await prisma.customer.findUnique({ where: { id: customerId } }); if (!customer) { res.status(404).json({ success: false, error: 'Kunde nicht gefunden' }); return; } if (!customer.commercialRegisterPath) { res.status(400).json({ success: false, error: 'Kein Dokument vorhanden' }); return; } // Datei löschen const filePath = path.join(process.cwd(), customer.commercialRegisterPath); if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); } // Pfad in DB auf null setzen await prisma.customer.update({ where: { id: customerId }, data: { commercialRegisterPath: null }, }); res.json({ success: true }); } catch (error) { console.error('Delete error:', error); res.status(500).json({ success: false, error: 'Löschen fehlgeschlagen' }); } } ); // ==================== DATENSCHUTZERKLÄRUNG (für alle Kunden) ==================== // Upload für Datenschutzerklärung router.post( '/customers/:id/privacy-policy', authenticate, requirePermission('customers:update'), setUploadDir('privacy-policies'), upload.single('document'), async (req: AuthRequest, res: Response) => { try { if (!req.file) { res.status(400).json({ success: false, error: 'Keine Datei hochgeladen' }); return; } const customerId = parseInt(req.params.id); const relativePath = `/uploads/privacy-policies/${req.file.filename}`; // Alte Datei löschen falls vorhanden const customer = await prisma.customer.findUnique({ where: { id: customerId } }); if (customer?.privacyPolicyPath) { const oldPath = path.join(process.cwd(), customer.privacyPolicyPath); if (fs.existsSync(oldPath)) { fs.unlinkSync(oldPath); } } // Kunde in der DB aktualisieren await prisma.customer.update({ where: { id: customerId }, data: { privacyPolicyPath: relativePath }, }); // Alle Consents auf GRANTED setzen (PDF = vollständige Einwilligung) const consentTypes = ['DATA_PROCESSING', 'MARKETING_EMAIL', 'MARKETING_PHONE', 'DATA_SHARING_PARTNER'] as const; for (const consentType of consentTypes) { await prisma.customerConsent.upsert({ where: { customerId_consentType: { customerId, consentType } }, update: { status: 'GRANTED', grantedAt: new Date(), source: 'papier' }, create: { customerId, consentType, status: 'GRANTED', grantedAt: new Date(), source: 'papier', createdBy: (req as any).user?.email || 'admin' }, }); } // Audit const cust = await prisma.customer.findUnique({ where: { id: customerId }, select: { firstName: true, lastName: true } }); await logChange({ req, action: 'CREATE', resourceType: 'CustomerConsent', label: `Datenschutzerklärung-PDF hochgeladen für ${cust?.firstName} ${cust?.lastName} – alle Einwilligungen erteilt`, details: { aktion: 'PDF hochgeladen', einwilligungen: 'alle erteilt', quelle: 'papier' }, customerId, }); res.json({ success: true, data: { path: relativePath, filename: req.file.filename, originalName: req.file.originalname, size: req.file.size, }, }); } catch (error) { console.error('Upload error:', error); res.status(500).json({ success: false, error: 'Upload fehlgeschlagen' }); } } ); // Löschen der Datenschutzerklärung router.delete( '/customers/:id/privacy-policy', authenticate, requirePermission('customers:update'), async (req: AuthRequest, res: Response) => { try { const customerId = parseInt(req.params.id); const customer = await prisma.customer.findUnique({ where: { id: customerId } }); if (!customer) { res.status(404).json({ success: false, error: 'Kunde nicht gefunden' }); return; } if (!customer.privacyPolicyPath) { res.status(400).json({ success: false, error: 'Kein Dokument vorhanden' }); return; } // Datei löschen const filePath = path.join(process.cwd(), customer.privacyPolicyPath); if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); } // Pfad in DB auf null setzen await prisma.customer.update({ where: { id: customerId }, data: { privacyPolicyPath: null }, }); // Nur Consents widerrufen die per Papier erteilt wurden await prisma.customerConsent.updateMany({ where: { customerId, status: 'GRANTED', source: 'papier' }, data: { status: 'WITHDRAWN', withdrawnAt: new Date() }, }); // Audit const cust = await prisma.customer.findUnique({ where: { id: customerId }, select: { firstName: true, lastName: true } }); await logChange({ req, action: 'DELETE', resourceType: 'CustomerConsent', label: `Datenschutzerklärung-PDF gelöscht für ${cust?.firstName} ${cust?.lastName} – Papier-Einwilligungen widerrufen`, details: { aktion: 'PDF gelöscht', einwilligungen: 'papier-basierte widerrufen' }, customerId, }); res.json({ success: true }); } catch (error) { console.error('Delete error:', error); res.status(500).json({ success: false, error: 'Löschen fehlgeschlagen' }); } } ); // ==================== VERTRAGS-DOKUMENTE ==================== // Generische Funktion für Vertrags-Dokument Upload async function handleContractDocumentUpload( req: AuthRequest, res: Response, fieldName: 'cancellationLetterPath' | 'cancellationConfirmationPath' | 'cancellationLetterOptionsPath' | 'cancellationConfirmationOptionsPath', subDir: string ) { try { if (!req.file) { res.status(400).json({ success: false, error: 'Keine Datei hochgeladen' }); return; } const contractId = parseInt(req.params.id); if (!(await canAccessContract(req, res, contractId))) return; const relativePath = `/uploads/${subDir}/${req.file.filename}`; // Alte Datei löschen falls vorhanden const contract = await prisma.contract.findUnique({ where: { id: contractId } }); if (!contract) { res.status(404).json({ success: false, error: 'Vertrag nicht gefunden' }); return; } const oldPath = contract[fieldName]; if (oldPath) { const fullPath = path.join(process.cwd(), oldPath); if (fs.existsSync(fullPath)) { fs.unlinkSync(fullPath); } } // Bei Kündigungsbestätigung(s-Optionen): optionales Datum aus multipart // übernehmen. Ohne Angabe: falls Feld noch leer → heute, sonst nicht anfassen. const updateData: Record = { [fieldName]: relativePath }; if (fieldName === 'cancellationConfirmationPath' || fieldName === 'cancellationConfirmationOptionsPath') { const dateField = fieldName === 'cancellationConfirmationPath' ? 'cancellationConfirmationDate' : 'cancellationConfirmationOptionsDate'; const provided = typeof req.body?.confirmationDate === 'string' ? req.body.confirmationDate : null; let target: Date | null = null; if (provided) { const parsed = new Date(provided); if (!isNaN(parsed.getTime())) target = parsed; } if (target) { updateData[dateField] = target; } else if (!contract[dateField]) { updateData[dateField] = new Date(); } } // Vertrag in der DB aktualisieren await prisma.contract.update({ where: { id: contractId }, data: updateData, }); // Wenn eine Kündigungsbestätigung (nicht "Optionen") hochgeladen wurde und // der Vertrag noch ACTIVE ist → auf CANCELLED umstellen + Audit-Log. // "Optionen" ist für Vertrags-Änderungen gedacht, nicht für echte Kündigungen. if (fieldName === 'cancellationConfirmationPath' && contract.status === 'ACTIVE') { await prisma.contract.update({ where: { id: contractId }, data: { status: 'CANCELLED' }, }); await logChange({ req, action: 'UPDATE', resourceType: 'Contract', resourceId: contractId.toString(), label: `Vertrag ${contract.contractNumber} automatisch auf CANCELLED gesetzt (Kündigungsbestätigung hochgeladen)`, details: { vorher: 'ACTIVE', nachher: 'CANCELLED', trigger: 'cancellationConfirmation-Upload' }, customerId: contract.customerId, }); } res.json({ success: true, data: { path: relativePath, filename: req.file.filename, originalName: req.file.originalname, size: req.file.size, }, }); } catch (error) { console.error('Upload error:', error); res.status(500).json({ success: false, error: 'Upload fehlgeschlagen' }); } } // Generische Funktion für Vertrags-Dokument Löschen async function handleContractDocumentDelete( req: AuthRequest, res: Response, fieldName: 'cancellationLetterPath' | 'cancellationConfirmationPath' | 'cancellationLetterOptionsPath' | 'cancellationConfirmationOptionsPath' ) { try { const contractId = parseInt(req.params.id); if (!(await canAccessContract(req, res, contractId))) return; const contract = await prisma.contract.findUnique({ where: { id: contractId } }); if (!contract) { res.status(404).json({ success: false, error: 'Vertrag nicht gefunden' }); return; } const documentPath = contract[fieldName]; if (!documentPath) { res.status(400).json({ success: false, error: 'Kein Dokument vorhanden' }); return; } // Datei löschen const filePath = path.join(process.cwd(), documentPath); if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); } // Pfad in DB auf null setzen await prisma.contract.update({ where: { id: contractId }, data: { [fieldName]: null }, }); res.json({ success: true }); } catch (error) { console.error('Delete error:', error); res.status(500).json({ success: false, error: 'Löschen fehlgeschlagen' }); } } // Kündigungsschreiben router.post( '/contracts/:id/cancellation-letter', authenticate, requirePermission('contracts:update'), setUploadDir('cancellation-letters'), upload.single('document'), (req: AuthRequest, res: Response) => handleContractDocumentUpload(req, res, 'cancellationLetterPath', 'cancellation-letters') ); router.delete( '/contracts/:id/cancellation-letter', authenticate, requirePermission('contracts:update'), (req: AuthRequest, res: Response) => handleContractDocumentDelete(req, res, 'cancellationLetterPath') ); // Kündigungsbestätigung router.post( '/contracts/:id/cancellation-confirmation', authenticate, requirePermission('contracts:update'), setUploadDir('cancellation-confirmations'), upload.single('document'), (req: AuthRequest, res: Response) => handleContractDocumentUpload(req, res, 'cancellationConfirmationPath', 'cancellation-confirmations') ); router.delete( '/contracts/:id/cancellation-confirmation', authenticate, requirePermission('contracts:update'), (req: AuthRequest, res: Response) => handleContractDocumentDelete(req, res, 'cancellationConfirmationPath') ); // Kündigungsschreiben Optionen router.post( '/contracts/:id/cancellation-letter-options', authenticate, requirePermission('contracts:update'), setUploadDir('cancellation-letters-options'), upload.single('document'), (req: AuthRequest, res: Response) => handleContractDocumentUpload(req, res, 'cancellationLetterOptionsPath', 'cancellation-letters-options') ); router.delete( '/contracts/:id/cancellation-letter-options', authenticate, requirePermission('contracts:update'), (req: AuthRequest, res: Response) => handleContractDocumentDelete(req, res, 'cancellationLetterOptionsPath') ); // Kündigungsbestätigung Optionen router.post( '/contracts/:id/cancellation-confirmation-options', authenticate, requirePermission('contracts:update'), setUploadDir('cancellation-confirmations-options'), upload.single('document'), (req: AuthRequest, res: Response) => handleContractDocumentUpload(req, res, 'cancellationConfirmationOptionsPath', 'cancellation-confirmations-options') ); router.delete( '/contracts/:id/cancellation-confirmation-options', authenticate, requirePermission('contracts:update'), (req: AuthRequest, res: Response) => handleContractDocumentDelete(req, res, 'cancellationConfirmationOptionsPath') ); // ==================== RECHNUNGS-DOKUMENTE ==================== // Upload für Rechnungs-Dokument router.post( '/invoices/:id', authenticate, requirePermission('contracts:update'), setUploadDir('invoices'), upload.single('document'), async (req: AuthRequest, res: Response) => { try { if (!req.file) { res.status(400).json({ success: false, error: 'Keine Datei hochgeladen' }); return; } const invoiceId = parseInt(req.params.id); const relativePath = `/uploads/invoices/${req.file.filename}`; // Alte Datei löschen falls vorhanden const invoice = await prisma.invoice.findUnique({ where: { id: invoiceId } }); if (!invoice) { res.status(404).json({ success: false, error: 'Rechnung nicht gefunden' }); return; } if (invoice.documentPath) { const oldPath = path.join(process.cwd(), invoice.documentPath); if (fs.existsSync(oldPath)) { fs.unlinkSync(oldPath); } } // Invoice in der DB aktualisieren await prisma.invoice.update({ where: { id: invoiceId }, data: { documentPath: relativePath }, }); res.json({ success: true, data: { path: relativePath, filename: req.file.filename, originalName: req.file.originalname, size: req.file.size, }, }); } catch (error) { console.error('Invoice upload error:', error); res.status(500).json({ success: false, error: 'Upload fehlgeschlagen' }); } } ); // Löschen von Rechnungs-Dokument router.delete( '/invoices/:id', authenticate, requirePermission('contracts:update'), async (req: AuthRequest, res: Response) => { try { const invoiceId = parseInt(req.params.id); const invoice = await prisma.invoice.findUnique({ where: { id: invoiceId } }); if (!invoice) { res.status(404).json({ success: false, error: 'Rechnung nicht gefunden' }); return; } if (!invoice.documentPath) { res.status(400).json({ success: false, error: 'Kein Dokument vorhanden' }); return; } // Datei löschen const filePath = path.join(process.cwd(), invoice.documentPath); if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); } // documentPath in DB auf null setzen await prisma.invoice.update({ where: { id: invoiceId }, data: { documentPath: null }, }); res.json({ success: true }); } catch (error) { console.error('Invoice delete error:', error); res.status(500).json({ success: false, error: 'Löschen fehlgeschlagen' }); } } ); export default router;