Auto-Vertragsstatus: nightly EXPIRED + Kündigungsbestätigung → CANCELLED
- Neuer Scheduler (02:00 + Catch-up 60s nach Start): alle ACTIVE-Verträge mit endDate < heute werden auf EXPIRED umgestellt. Audit-Log pro Vertrag. - Upload cancellationConfirmationPath: Vertrag wechselt von ACTIVE → CANCELLED (mit Audit-Log). "Options"-Upload triggert bewusst nicht, da für Vertragsänderungen gedacht, nicht für echte Kündigungen. - Keine neuen Statuswerte. "Kündigung gesendet vs. bestätigt" bleibt über die getrennten Felder cancellationSentDate / cancellationConfirmationDate lesbar, Status bleibt bis zur Bestätigung auf ACTIVE. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* 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 } 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 };
|
||||
Reference in New Issue
Block a user