import { Response } from 'express'; import { ApiResponse, AuthRequest } from '../types/index.js'; import { logChange } from '../services/audit.service.js'; import { ApiError } from '../utils/apiError.js'; import * as creditNoteService from '../services/creditNote.service.js'; import * as numberRangeService from '../services/creditNoteNumberRange.service.js'; import { generateCreditNotePdf } from '../services/creditNotePdf.service.js'; // Gutschriften sind ein reiner Mitarbeiter-/Admin-Bereich (interne // Subventions-Belege). Portal-Kunden dürfen sie weder sehen noch ändern – // Defense-in-Depth: nicht nur die UI blendet aus, der Endpoint blockt // Portal-Token explizit. function blockPortal(req: AuthRequest, res: Response): boolean { if (req.user?.isCustomerPortal) { res.status(403).json({ success: false, error: 'Kein Zugriff' } as ApiResponse); return true; } return false; } function idParam(req: AuthRequest, res: Response, name: string): number | null { const raw = req.params[name]; if (typeof raw !== 'string' || !/^\d+$/.test(raw)) { res.status(400).json({ success: false, error: `Ungültige ID: ${raw}` } as ApiResponse); return null; } const parsed = Number.parseInt(raw, 10); if (!Number.isInteger(parsed) || parsed < 1) { res.status(400).json({ success: false, error: `Ungültige ID: ${raw}` } as ApiResponse); return null; } return parsed; } function handleError(res: Response, error: unknown, fallback: string) { const status = error instanceof ApiError ? error.statusCode : 500; res.status(status).json({ success: false, error: error instanceof Error ? error.message : fallback, } as ApiResponse); } // ---- Gutschriften pro Vertrag ---- export async function listByContract(req: AuthRequest, res: Response): Promise { try { if (blockPortal(req, res)) return; const contractId = idParam(req, res, 'contractId'); if (contractId === null) return; const data = await creditNoteService.getCreditNotesByContract(contractId); res.json({ success: true, data } as ApiResponse); } catch (error) { handleError(res, error, 'Fehler beim Laden der Gutschriften'); } } // Vorbelegung fürs Formular: Default-Kundentyp (Firma/Privat aus Kunde) + // Vorschau der nächsten Gutschrift-Nummer. export async function formDefaults(req: AuthRequest, res: Response): Promise { try { if (blockPortal(req, res)) return; const contractId = idParam(req, res, 'contractId'); if (contractId === null) return; const [defaults, nextNumber] = await Promise.all([ creditNoteService.getCreditNoteDefaults(contractId), numberRangeService.previewNextNumber(), ]); res.json({ success: true, data: { ...defaults, nextNumber } } as ApiResponse); } catch (error) { handleError(res, error, 'Fehler beim Laden der Vorbelegung'); } } export async function create(req: AuthRequest, res: Response): Promise { try { if (blockPortal(req, res)) return; const contractId = idParam(req, res, 'contractId'); if (contractId === null) return; const created = await creditNoteService.createCreditNote(contractId, req.body ?? {}, req.user?.email); await logChange({ req, action: 'CREATE', resourceType: 'CreditNote', resourceId: created.id.toString(), label: `Gutschrift ${created.number} angelegt (${created.type}, ${created.amountGross} ${created.currency}) an Vertrag #${contractId}`, }); res.status(201).json({ success: true, data: created } as ApiResponse); } catch (error) { handleError(res, error, 'Fehler beim Anlegen der Gutschrift'); } } export async function getOne(req: AuthRequest, res: Response): Promise { try { if (blockPortal(req, res)) return; const id = idParam(req, res, 'id'); if (id === null) return; const data = await creditNoteService.getCreditNoteById(id); if (!data) { res.status(404).json({ success: false, error: 'Gutschrift nicht gefunden' } as ApiResponse); return; } res.json({ success: true, data } as ApiResponse); } catch (error) { handleError(res, error, 'Fehler beim Laden der Gutschrift'); } } export async function update(req: AuthRequest, res: Response): Promise { try { if (blockPortal(req, res)) return; const id = idParam(req, res, 'id'); if (id === null) return; const updated = await creditNoteService.updateCreditNote(id, req.body ?? {}); await logChange({ req, action: 'UPDATE', resourceType: 'CreditNote', resourceId: updated.id.toString(), label: `Gutschrift ${updated.number} geändert (${updated.type}, ${updated.amountGross} ${updated.currency})`, }); res.json({ success: true, data: updated } as ApiResponse); } catch (error) { handleError(res, error, 'Fehler beim Ändern der Gutschrift'); } } export async function remove(req: AuthRequest, res: Response): Promise { try { if (blockPortal(req, res)) return; const id = idParam(req, res, 'id'); if (id === null) return; const deleted = await creditNoteService.deleteCreditNote(id); await logChange({ req, action: 'DELETE', resourceType: 'CreditNote', resourceId: id.toString(), label: `Gutschrift ${deleted.number} gelöscht`, }); res.json({ success: true } as ApiResponse); } catch (error) { handleError(res, error, 'Fehler beim Löschen der Gutschrift'); } } // PDF (Gutschrift-Dokument) erzeugen. ZUGFeRD-Embedding folgt (Teil 2). export async function generatePdf(req: AuthRequest, res: Response): Promise { try { if (blockPortal(req, res)) return; const id = idParam(req, res, 'id'); if (id === null) return; const { pdfPath } = await generateCreditNotePdf(id); await logChange({ req, action: 'UPDATE', resourceType: 'CreditNote', resourceId: id.toString(), label: 'Gutschrift-PDF erzeugt', }); res.json({ success: true, data: { pdfPath } } as ApiResponse); } catch (error) { handleError(res, error, 'Fehler beim Erzeugen des PDF'); } } // ---- Nummernkreis (Einstellungen) ---- export async function getNumberRange(req: AuthRequest, res: Response): Promise { try { if (blockPortal(req, res)) return; const [range, nextNumber] = await Promise.all([ numberRangeService.getOrCreateRange(), numberRangeService.previewNextNumber(), ]); res.json({ success: true, data: { ...range, preview: nextNumber } } as ApiResponse); } catch (error) { handleError(res, error, 'Fehler beim Laden des Nummernkreises'); } } export async function updateNumberRange(req: AuthRequest, res: Response): Promise { try { if (blockPortal(req, res)) return; const range = await numberRangeService.updateRange(req.body ?? {}); const preview = await numberRangeService.previewNextNumber(); await logChange({ req, action: 'UPDATE', resourceType: 'CreditNoteNumberRange', resourceId: range.id.toString(), label: `Gutschrift-Nummernkreis geändert (Präfix "${range.prefix}", nächste Nr. ${range.nextNumber})`, }); res.json({ success: true, data: { ...range, preview } } as ApiResponse); } catch (error) { handleError(res, error, 'Fehler beim Speichern des Nummernkreises'); } }