/** * Scheduler für automatische Vertrags-Status-Übergänge. * * Einmal täglich um 02:00: alle Verträge mit status=ACTIVE und * endDate < heute werden auf EXPIRED umgestellt (+ Audit-Log). * * Läuft zusätzlich 60 Sekunden nach Server-Start als Catch-up falls * der Prozess zum 02:00-Slot neu gestartet wurde. */ import cron from 'node-cron'; import prisma from '../lib/prisma.js'; import { createAuditLog, logChange } from './audit.service.js'; async function runExpireCheck(): Promise { const today = new Date(); today.setHours(0, 0, 0, 0); const expiring = await prisma.contract.findMany({ where: { status: 'ACTIVE', endDate: { not: null, lt: today }, }, select: { id: true, contractNumber: true, customerId: true, endDate: true, }, }); if (expiring.length === 0) { console.log('[ContractStatusScheduler] Keine abgelaufenen Verträge.'); return; } console.log(`[ContractStatusScheduler] ${expiring.length} Vertrag/Verträge auf EXPIRED setzen.`); for (const c of expiring) { try { await prisma.contract.update({ where: { id: c.id }, data: { status: 'EXPIRED' }, }); await createAuditLog({ userEmail: 'system', userRole: 'System', action: 'UPDATE', resourceType: 'Contract', resourceId: c.id.toString(), resourceLabel: `Vertrag ${c.contractNumber} automatisch auf EXPIRED gesetzt (Laufzeit überschritten)`, endpoint: 'scheduler:contract-status', httpMethod: 'SYSTEM', ipAddress: 'localhost', dataSubjectId: c.customerId, changesBefore: { status: 'ACTIVE' }, changesAfter: { status: 'EXPIRED', endDate: c.endDate?.toISOString() }, }); } catch (err) { console.error(`[ContractStatusScheduler] Fehler bei Vertrag #${c.id}:`, err); } } console.log('[ContractStatusScheduler] Fertig.'); } export function startContractStatusScheduler(): void { // Täglich um 02:00 Uhr (Server-Zeit) cron.schedule('0 2 * * *', () => { runExpireCheck().catch((err) => console.error('[ContractStatusScheduler] Daily run failed:', err), ); }); // Catch-up 60 Sekunden nach Start setTimeout(() => { runExpireCheck().catch((err) => console.error('[ContractStatusScheduler] Catch-up run failed:', err), ); }, 60_000); console.log('[ContractStatusScheduler] Gestartet – täglich um 02:00 + Catch-up nach 60s'); } export { runExpireCheck }; /** * Wird nach einem ContractDocument-Upload aufgerufen. Wenn der Typ eine * Lieferbestätigung ist: * - Contract.status von DRAFT auf ACTIVE setzen (falls DRAFT) * - Contract.startDate auf deliveryDate (oder heute) setzen, falls noch leer * * Schreibweise "Lieferbestätigung" stammt aus dem Frontend-Dropdown * (SaveAttachmentModal / ContractDetail). Vergleich case-insensitive + * getrimmt zur Robustheit. */ export async function maybeActivateOnDeliveryConfirmation( contractId: number, documentType: string, req: unknown, deliveryDate?: Date | string | null, ): Promise { if (!documentType || typeof documentType !== 'string') return; if (documentType.trim().toLowerCase() !== 'lieferbestätigung') return; const contract = await prisma.contract.findUnique({ where: { id: contractId }, select: { status: true, contractNumber: true, customerId: true, startDate: true }, }); if (!contract) return; // deliveryDate parsen, Fallback auf heute let parsedDate: Date | null = null; if (deliveryDate) { const parsed = new Date(deliveryDate); if (!isNaN(parsed.getTime())) parsedDate = parsed; } const effectiveDate = parsedDate || new Date(); const updateData: Record = {}; const changes: Record = {}; if (contract.status === 'DRAFT') { updateData.status = 'ACTIVE'; changes.status = { vorher: 'DRAFT', nachher: 'ACTIVE' }; } if (!contract.startDate) { updateData.startDate = effectiveDate; changes.startDate = { vorher: null, nachher: effectiveDate.toISOString().split('T')[0] }; } if (Object.keys(updateData).length === 0) return; await prisma.contract.update({ where: { id: contractId }, data: updateData, }); await logChange({ req, action: 'UPDATE', resourceType: 'Contract', resourceId: contractId.toString(), label: `Vertrag ${contract.contractNumber} automatisch aktualisiert (Lieferbestätigung hochgeladen)`, details: { ...changes, trigger: 'Lieferbestätigung-Upload' }, customerId: contract.customerId, }); }