import prisma from '../lib/prisma.js'; import { stripHtml } from '../utils/sanitize.js'; export interface CreateHistoryEntryData { title: string; description?: string; isAutomatic?: boolean; createdBy: string; } // Read-Time-Defensive: title + description durch stripHtml schicken, damit // Alt-Einträge (vor Pentest 43.6) mit rohen HTML-Payloads nicht roh // rausgehen. Schützt zusätzlich gegen einen umgangenen Write-Filter. function sanitizeEntry(entry: T): T { return { ...entry, title: stripHtml(entry.title) as string, description: entry.description != null ? (stripHtml(entry.description) as string) : entry.description, }; } /** * Alle Historie-Einträge für einen Vertrag abrufen */ export async function getHistoryEntries(contractId: number) { const entries = await prisma.contractHistoryEntry.findMany({ where: { contractId }, orderBy: { createdAt: 'desc' }, }); return entries.map(sanitizeEntry); } /** * Einzelnen Historie-Eintrag abrufen */ export async function getHistoryEntry(contractId: number, entryId: number) { const entry = await prisma.contractHistoryEntry.findFirst({ where: { id: entryId, contractId }, }); return entry ? sanitizeEntry(entry) : null; } /** * Neuen Historie-Eintrag erstellen */ export async function createHistoryEntry(contractId: number, data: CreateHistoryEntryData) { // Prüfen ob Vertrag existiert const contract = await prisma.contract.findUnique({ where: { id: contractId }, }); if (!contract) { throw new Error('Vertrag nicht gefunden'); } // Pentest 2026-05-30 (MEDIUM, 43.6): Admin konnte HTML-Tags in title + // description schreiben, Portal-User las sie roh zurück. stripHtml räumt // Tags + gefährliche URI-Schemata vor dem Persistieren weg. return prisma.contractHistoryEntry.create({ data: { contractId, title: stripHtml(data.title) as string, description: data.description != null ? (stripHtml(data.description) as string) : data.description, isAutomatic: data.isAutomatic ?? false, createdBy: data.createdBy, }, }); } /** * Historie-Eintrag aktualisieren (nur manuelle Einträge) */ export async function updateHistoryEntry( contractId: number, entryId: number, data: { title?: string; description?: string } ) { const entry = await prisma.contractHistoryEntry.findFirst({ where: { id: entryId, contractId }, }); if (!entry) { throw new Error('Historie-Eintrag nicht gefunden'); } if (entry.isAutomatic) { throw new Error('Automatische Einträge können nicht bearbeitet werden'); } return prisma.contractHistoryEntry.update({ where: { id: entryId }, data: { title: data.title != null ? (stripHtml(data.title) as string) : undefined, description: data.description != null ? (stripHtml(data.description) as string) : undefined, }, }); } /** * Historie-Eintrag löschen (nur manuelle Einträge) */ export async function deleteHistoryEntry(contractId: number, entryId: number) { const entry = await prisma.contractHistoryEntry.findFirst({ where: { id: entryId, contractId }, }); if (!entry) { throw new Error('Historie-Eintrag nicht gefunden'); } if (entry.isAutomatic) { throw new Error('Automatische Einträge können nicht gelöscht werden'); } return prisma.contractHistoryEntry.delete({ where: { id: entryId } }); } /** * Automatischen Historie-Eintrag für Folgevertrag erstellen (im Vorgängervertrag) */ export async function createFollowUpHistoryEntry( previousContractId: number, newContractNumber: string, createdBy: string ) { return createHistoryEntry(previousContractId, { title: `Folgevertrag erstellt: ${newContractNumber}`, description: `Ein neuer Folgevertrag (${newContractNumber}) wurde aus diesem Vertrag erstellt.`, isAutomatic: true, createdBy, }); } /** * Automatischen Historie-Eintrag für neuen Folgevertrag erstellen (im neuen Vertrag selbst) */ export async function createNewContractFromPredecessorEntry( newContractId: number, previousContractNumber: string, createdBy: string ) { return createHistoryEntry(newContractId, { title: `Folgevertrag zu ${previousContractNumber}`, description: `Dieser Vertrag wurde als Folgevertrag zu ${previousContractNumber} erstellt.`, isAutomatic: true, createdBy, }); } /** * Automatischen Historie-Eintrag für VVL (Vertragsverlängerung) im Vorgängervertrag. */ export async function createRenewalHistoryEntry( previousContractId: number, newContractNumber: string, createdBy: string ) { return createHistoryEntry(previousContractId, { title: `Vertragsverlängerung erstellt: ${newContractNumber}`, description: `Eine Vertragsverlängerung (VVL) als ${newContractNumber} wurde aus diesem Vertrag erstellt – alle Daten wurden 1:1 übernommen, das Auftragsdokument muss neu hochgeladen werden.`, isAutomatic: true, createdBy, }); } /** * Automatischen Historie-Eintrag im neuen VVL-Vertrag. */ export async function createNewRenewalFromPredecessorEntry( newContractId: number, previousContractNumber: string, createdBy: string ) { return createHistoryEntry(newContractId, { title: `VVL zu ${previousContractNumber}`, description: `Dieser Vertrag wurde als Vertragsverlängerung (VVL) zu ${previousContractNumber} erstellt.`, isAutomatic: true, createdBy, }); }