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:
parent
762e17a864
commit
6333bc70b2
@ -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",
|
||||
|
||||
85
backend/src/jobs/noticeDeadlineReminderJob.ts
Normal file
85
backend/src/jobs/noticeDeadlineReminderJob.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user