Add push reminder job for expiring notice deadlines

"Automatische Warnungen bei Auslauf/Kündigungsfristen" was only a
passive dashboard view (GET /contracts/notice-deadlines, isUrgent
flag) — nobody gets notified unless the landlord happens to open the
cockpit. Adds a daily job (same pattern as dailyDunningJob.ts) that
pushes landlords/admins when a contract's notice deadline falls
within 30 days. Dedup is via the Notification history (payload.
contractId within a 14-day cooldown) rather than a new Contract
column, consistent with contracts.ts's existing "no extra model
needed" approach.
This commit is contained in:
Giuseppe Lombardo 2026-08-13 07:56:18 +00:00
parent 762e17a864
commit 6333bc70b2
2 changed files with 86 additions and 0 deletions

View File

@ -12,6 +12,7 @@
"dunning:run": "ts-node -r dotenv/config src/jobs/dailyDunningJob.ts", "dunning:run": "ts-node -r dotenv/config src/jobs/dailyDunningJob.ts",
"cleaning-reminder:run": "ts-node -r dotenv/config src/jobs/cleaningReminderJob.ts", "cleaning-reminder:run": "ts-node -r dotenv/config src/jobs/cleaningReminderJob.ts",
"trash-reminder:run": "ts-node -r dotenv/config src/jobs/trashReminderJob.ts", "trash-reminder:run": "ts-node -r dotenv/config src/jobs/trashReminderJob.ts",
"notice-deadline-reminder:run": "ts-node -r dotenv/config src/jobs/noticeDeadlineReminderJob.ts",
"test:webhook": "ts-node -r dotenv/config scripts/send-test-webhook.ts", "test:webhook": "ts-node -r dotenv/config scripts/send-test-webhook.ts",
"prisma:generate": "prisma generate --schema=../prisma/schema.prisma", "prisma:generate": "prisma generate --schema=../prisma/schema.prisma",
"prisma:migrate": "prisma migrate dev --schema=../prisma/schema.prisma", "prisma:migrate": "prisma migrate dev --schema=../prisma/schema.prisma",

View File

@ -0,0 +1,85 @@
import 'dotenv/config';
import { PrismaClient } from '@prisma/client';
import { sendPushNotification } from '../services/notificationService';
const prisma = new PrismaClient();
const NOTIFICATION_TYPE = 'NOTICE_DEADLINE';
// Keine Wiederholung innerhalb dieses Zeitraums je Vertrag, um den Vermieter
// nicht täglich 30 Tage lang zu spammen (bewusst kein Extra-Feld auf Contract
// nötig — analog contracts.ts prüfen wir stattdessen den Notification-Verlauf).
const RESEND_COOLDOWN_DAYS = 14;
function addMonths(date: Date, months: number): Date {
const d = new Date(date);
d.setMonth(d.getMonth() - months);
return d;
}
/**
* Täglicher Cron-Job (analog dailyDunningJob.ts): warnt Vermieter/Admin per
* Push, wenn bei einem aktiven, befristeten Vertrag die Kündigungsfrist
* innerhalb der nächsten 30 Tage abläuft (isUrgent-Logik aus
* routes/contracts.ts). Max. 1x je Vertrag pro RESEND_COOLDOWN_DAYS.
*/
export async function runNoticeDeadlineReminderJob(referenceDate: Date = new Date()) {
const contracts = await prisma.contract.findMany({
where: { isActive: true, endDate: { not: null } },
select: {
id: true,
endDate: true,
noticePeriodMonths: true,
user: { select: { fullName: true } },
room: { select: { roomNumber: true } },
},
});
const urgent = contracts.filter((c) => {
const noticeDeadline = addMonths(c.endDate!, c.noticePeriodMonths);
const daysUntil = Math.ceil((noticeDeadline.getTime() - referenceDate.getTime()) / (1000 * 60 * 60 * 24));
return daysUntil <= 30 && daysUntil >= 0;
});
const landlords = await prisma.user.findMany({ where: { role: { in: ['LANDLORD', 'ADMIN'] } } });
const cooldownStart = new Date(referenceDate);
cooldownStart.setDate(cooldownStart.getDate() - RESEND_COOLDOWN_DAYS);
let remindersSent = 0;
for (const contract of urgent) {
const recentlySent = await prisma.notification.findFirst({
where: {
type: NOTIFICATION_TYPE,
sentAt: { gte: cooldownStart },
payload: { path: ['contractId'], equals: contract.id },
},
});
if (recentlySent) continue;
await Promise.all(
landlords.map((landlord) =>
sendPushNotification({
userId: landlord.id,
type: NOTIFICATION_TYPE,
title: 'Kündigungsfrist läuft bald ab',
body: `Für ${contract.room.roomNumber} (${contract.user.fullName}) endet die Kündigungsfrist in Kürze.`,
payload: { contractId: contract.id },
}),
),
);
remindersSent++;
}
return { remindersSent };
}
if (require.main === module) {
runNoticeDeadlineReminderJob()
.then((result) => {
console.log(`[noticeDeadlineReminderJob] ${result.remindersSent} Erinnerungen versendet.`);
return prisma.$disconnect();
})
.catch((err) => {
console.error('[noticeDeadlineReminderJob] Fehlgeschlagen:', err);
process.exit(1);
});
}