import { Router, Response } from 'express'; import { PrismaClient, SettlementStatus } from '@prisma/client'; import { AuthedRequest, requireAuth, requireRole } from '../middleware/auth'; const prisma = new PrismaClient(); /** * Router für die Nebenkostenabrechnung. * * Der Vermieter trägt eine Abrechnung (Zeitraum + Gesamtbetrag) ein; die App * verteilt den Betrag automatisch proportional zur Zimmergröße (sizeSqm) auf * alle aktuell aktiven Mieter — dieselbe faire Grundlage wie bei der * Nettokaltmiete. Zugriffsregel: ein Mieter sieht nur seinen EIGENEN Anteil, * niemals die Anteile anderer Mieter (private Abrechnungsdaten). Vermieter/ * Admin sehen die komplette Abrechnung inkl. aller Anteile. */ export const utilityStatementsRouter = Router(); const SHARE_SELECT = { id: true, shareAmount: true, settlementStatus: true, settledAt: true, user: { select: { id: true, fullName: true } }, room: { select: { id: true, roomNumber: true } }, }; const STATEMENT_SELECT_LANDLORD = { id: true, periodStart: true, periodEnd: true, totalAmount: true, description: true, createdAt: true, createdBy: { select: { id: true, fullName: true } }, shares: { select: SHARE_SELECT }, }; // POST /utility-statements — Abrechnung anlegen, automatischer Split nach // Zimmergröße unter allen aktuell aktiven Mietern (Vermieter/Admin only). utilityStatementsRouter.post( '/utility-statements', requireAuth, requireRole('LANDLORD', 'ADMIN'), async (req: AuthedRequest, res: Response) => { const { periodStart, periodEnd, totalAmount, description } = req.body || {}; const parsedAmount = Number(totalAmount); if (!Number.isFinite(parsedAmount) || parsedAmount <= 0) { return res.status(400).json({ error: 'totalAmount muss eine positive Zahl sein' }); } const start = new Date(periodStart); const end = new Date(periodEnd); if (isNaN(start.getTime()) || isNaN(end.getTime()) || start >= end) { return res.status(400).json({ error: 'periodStart/periodEnd sind ungültig' }); } // Aktive Verträge mit Zimmer laden, um proportional nach sizeSqm zu splitten. const activeContracts = await prisma.contract.findMany({ where: { isActive: true }, select: { userId: true, roomId: true, room: { select: { sizeSqm: true } } }, }); if (activeContracts.length === 0) { return res.status(409).json({ error: 'Keine aktiven Mietverträge vorhanden, Split nicht möglich' }); } const totalSqm = activeContracts.reduce((sum, c) => sum + Number(c.room.sizeSqm), 0); let allocated = 0; const shares = activeContracts.map((c, idx) => { const isLast = idx === activeContracts.length - 1; let shareAmount: number; if (isLast) { // letzter Anteil bekommt den Rest, um Rundungsdifferenzen zu vermeiden shareAmount = Math.round((parsedAmount - allocated) * 100) / 100; } else { shareAmount = Math.round(((parsedAmount * Number(c.room.sizeSqm)) / totalSqm) * 100) / 100; allocated += shareAmount; } return { userId: c.userId, roomId: c.roomId, shareAmount, settlementStatus: SettlementStatus.OPEN, }; }); const statement = await prisma.utilityStatement.create({ data: { periodStart: start, periodEnd: end, totalAmount: parsedAmount, description: typeof description === 'string' ? description.trim().slice(0, 300) : null, createdById: req.user!.id, shares: { create: shares }, }, select: STATEMENT_SELECT_LANDLORD, }); res.status(201).json({ statement }); }, ); // GET /utility-statements — Vermieter/Admin sehen alle Abrechnungen mit allen // Anteilen; Mieter sehen alle Abrechnungen, aber NUR ihren eigenen Anteil // (kein Einblick in die Anteile anderer Mieter). utilityStatementsRouter.get('/utility-statements', requireAuth, async (req: AuthedRequest, res: Response) => { const isLandlord = req.user!.role === 'LANDLORD' || req.user!.role === 'ADMIN'; const statements = await prisma.utilityStatement.findMany({ orderBy: { periodStart: 'desc' }, select: { id: true, periodStart: true, periodEnd: true, totalAmount: true, description: true, createdAt: true, createdBy: { select: { id: true, fullName: true } }, shares: { select: SHARE_SELECT, where: isLandlord ? undefined : { userId: req.user!.id }, }, }, }); res.status(200).json({ statements }); }); // PATCH /utility-statement-shares/:id — eigenen Anteil als beglichen markieren // (Mieter selbst) oder durch Vermieter/Admin. utilityStatementsRouter.patch( '/utility-statement-shares/:id', requireAuth, async (req: AuthedRequest, res: Response) => { const share = await prisma.utilityStatementShare.findUnique({ where: { id: req.params.id } }); if (!share) return res.status(404).json({ error: 'Anteil nicht gefunden' }); const isLandlord = req.user!.role === 'LANDLORD' || req.user!.role === 'ADMIN'; if (!isLandlord && share.userId !== req.user!.id) { return res.status(403).json({ error: 'Keine Berechtigung, diesen Anteil zu ändern' }); } const updated = await prisma.utilityStatementShare.update({ where: { id: req.params.id }, data: { settlementStatus: SettlementStatus.SETTLED, settledAt: new Date() }, select: SHARE_SELECT, }); res.status(200).json({ share: updated }); }, ); // DELETE /utility-statements/:id — nur Vermieter/Admin. utilityStatementsRouter.delete( '/utility-statements/:id', requireAuth, requireRole('LANDLORD', 'ADMIN'), async (req: AuthedRequest, res: Response) => { try { await prisma.utilityStatementShare.deleteMany({ where: { statementId: req.params.id } }); await prisma.utilityStatement.delete({ where: { id: req.params.id } }); res.status(204).send(); } catch { res.status(404).json({ error: 'Abrechnung nicht gefunden' }); } }, );