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",
|
"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",
|
||||||
|
|||||||
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