150 lines
4.5 KiB
TypeScript
150 lines
4.5 KiB
TypeScript
/**
|
||
* 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<void> {
|
||
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<void> {
|
||
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<string, unknown> = {};
|
||
const changes: Record<string, { vorher: unknown; nachher: unknown }> = {};
|
||
|
||
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,
|
||
});
|
||
}
|