From 6333bc70b21c8e4401d3002e033a5ca2c95e12ca Mon Sep 17 00:00:00 2001 From: bernd Date: Thu, 13 Aug 2026 07:56:18 +0000 Subject: [PATCH] Add push reminder job for expiring notice deadlines MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "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. --- backend/package.json | 1 + backend/src/jobs/noticeDeadlineReminderJob.ts | 85 +++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 backend/src/jobs/noticeDeadlineReminderJob.ts diff --git a/backend/package.json b/backend/package.json index 535e176..77a3efa 100644 --- a/backend/package.json +++ b/backend/package.json @@ -12,6 +12,7 @@ "dunning:run": "ts-node -r dotenv/config src/jobs/dailyDunningJob.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", + "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", "prisma:generate": "prisma generate --schema=../prisma/schema.prisma", "prisma:migrate": "prisma migrate dev --schema=../prisma/schema.prisma", diff --git a/backend/src/jobs/noticeDeadlineReminderJob.ts b/backend/src/jobs/noticeDeadlineReminderJob.ts new file mode 100644 index 0000000..9532341 --- /dev/null +++ b/backend/src/jobs/noticeDeadlineReminderJob.ts @@ -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); + }); +}