Wire up OneSignal push delivery: register endpoint + reminder jobs
sendPushNotification() (services/notificationService.ts) already called the real OneSignal API correctly, but two things made it unreachable: no endpoint ever set User.pushToken, and no job existed for the two push-based requirements beyond rent dunning — cleaning check-in reminders and the trash-calendar evening push. Both CleaningTask and TrashSchedule already had an unused `reminderSentAt` column, so the schema anticipated this and was just never wired up. - PUT /v1/auth/push-token: registers the current device's OneSignal Player ID. - jobs/cleaningReminderJob.ts, jobs/trashReminderJob.ts: same find-due/send-once/mark-reminderSentAt pattern as the existing dailyDunningJob.ts. Safe to run without OneSignal credentials configured — sendPushNotification degrades to an in-app Notification log when ONESIGNAL_APP_ID/API_KEY or a user's pushToken are absent. Not done here (needs real third-party setup from the user, same as the Cloudflare tunnel token pattern): ONESIGNAL_APP_ID/API_KEY in .env, and a frontend OneSignal Web SDK integration (service worker, VAPID keys) to actually populate pushToken from a real browser.
This commit is contained in:
parent
3629f5f66f
commit
762e17a864
@ -10,6 +10,8 @@
|
|||||||
"build": "tsc -p tsconfig.json",
|
"build": "tsc -p tsconfig.json",
|
||||||
"start": "node dist/app.js",
|
"start": "node dist/app.js",
|
||||||
"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",
|
||||||
|
"trash-reminder:run": "ts-node -r dotenv/config src/jobs/trashReminderJob.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",
|
||||||
|
|||||||
62
backend/src/jobs/cleaningReminderJob.ts
Normal file
62
backend/src/jobs/cleaningReminderJob.ts
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
|
import { PrismaClient, CleaningArea } from '@prisma/client';
|
||||||
|
import { sendPushNotification } from '../services/notificationService';
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
const AREA_LABEL: Record<CleaningArea, string> = {
|
||||||
|
BATHROOM_1: 'Bad 1',
|
||||||
|
BATHROOM_2: 'Bad 2',
|
||||||
|
KITCHEN: 'Wohnküche',
|
||||||
|
HALLWAY_LAUNDRY: 'Waschraum/Flur',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Täglicher Cron-Job (analog dailyDunningJob.ts): erinnert Mieter per Push an
|
||||||
|
* eine offene Putzaufgabe, die morgen fällig ist. Verschickt (max. 1x je
|
||||||
|
* Aufgabe) und markiert per `reminderSentAt`.
|
||||||
|
*/
|
||||||
|
export async function runCleaningReminderJob(referenceDate: Date = new Date()) {
|
||||||
|
const tomorrowStart = new Date(referenceDate);
|
||||||
|
tomorrowStart.setDate(tomorrowStart.getDate() + 1);
|
||||||
|
tomorrowStart.setHours(0, 0, 0, 0);
|
||||||
|
const tomorrowEnd = new Date(tomorrowStart);
|
||||||
|
tomorrowEnd.setHours(23, 59, 59, 999);
|
||||||
|
|
||||||
|
const dueTasks = await prisma.cleaningTask.findMany({
|
||||||
|
where: {
|
||||||
|
status: 'PENDING',
|
||||||
|
dueDate: { gte: tomorrowStart, lte: tomorrowEnd },
|
||||||
|
reminderSentAt: null,
|
||||||
|
},
|
||||||
|
include: { assignedUser: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const task of dueTasks) {
|
||||||
|
await prisma.cleaningTask.update({
|
||||||
|
where: { id: task.id },
|
||||||
|
data: { reminderSentAt: new Date() },
|
||||||
|
});
|
||||||
|
|
||||||
|
await sendPushNotification({
|
||||||
|
userId: task.assignedUserId,
|
||||||
|
type: 'CLEANING_REMINDER',
|
||||||
|
title: 'Putzplan-Erinnerung',
|
||||||
|
body: `Morgen ist "${AREA_LABEL[task.area]}" an der Reihe. Bitte nicht vergessen!`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { remindersSent: dueTasks.length };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
runCleaningReminderJob()
|
||||||
|
.then((result) => {
|
||||||
|
console.log(`[cleaningReminderJob] ${result.remindersSent} Erinnerungen versendet.`);
|
||||||
|
return prisma.$disconnect();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('[cleaningReminderJob] Fehlgeschlagen:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
70
backend/src/jobs/trashReminderJob.ts
Normal file
70
backend/src/jobs/trashReminderJob.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
|
import { PrismaClient, TrashType } from '@prisma/client';
|
||||||
|
import { sendPushNotification } from '../services/notificationService';
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
const TRASH_LABEL: Record<TrashType, string> = {
|
||||||
|
RESTMUELL: 'Restmüll',
|
||||||
|
BIOMUELL: 'Biomüll',
|
||||||
|
GELBER_SACK: 'Gelber Sack',
|
||||||
|
PAPIER: 'Papiertonne',
|
||||||
|
GLAS: 'Glas',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Täglicher Cron-Job (analog dailyDunningJob.ts): "Vorabend-Push" für den
|
||||||
|
* Müll-Kalender — erinnert alle Mieter am Vorabend an eine Abholung am
|
||||||
|
* nächsten Tag. Verschickt (max. 1x je Termin) und markiert per
|
||||||
|
* `reminderSentAt`.
|
||||||
|
*/
|
||||||
|
export async function runTrashReminderJob(referenceDate: Date = new Date()) {
|
||||||
|
const tomorrowStart = new Date(referenceDate);
|
||||||
|
tomorrowStart.setDate(tomorrowStart.getDate() + 1);
|
||||||
|
tomorrowStart.setHours(0, 0, 0, 0);
|
||||||
|
const tomorrowEnd = new Date(tomorrowStart);
|
||||||
|
tomorrowEnd.setHours(23, 59, 59, 999);
|
||||||
|
|
||||||
|
const dueEntries = await prisma.trashSchedule.findMany({
|
||||||
|
where: {
|
||||||
|
date: { gte: tomorrowStart, lte: tomorrowEnd },
|
||||||
|
reminderSentAt: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!dueEntries.length) return { remindersSent: 0 };
|
||||||
|
|
||||||
|
const tenants = await prisma.user.findMany({ where: { role: 'TENANT' } });
|
||||||
|
|
||||||
|
for (const entry of dueEntries) {
|
||||||
|
await prisma.trashSchedule.update({
|
||||||
|
where: { id: entry.id },
|
||||||
|
data: { reminderSentAt: new Date() },
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
tenants.map((tenant) =>
|
||||||
|
sendPushNotification({
|
||||||
|
userId: tenant.id,
|
||||||
|
type: 'TRASH_REMINDER',
|
||||||
|
title: 'Müll-Erinnerung',
|
||||||
|
body: `Morgen wird "${TRASH_LABEL[entry.type]}" abgeholt. Bitte heute Abend rausstellen.`,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { remindersSent: dueEntries.length };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
runTrashReminderJob()
|
||||||
|
.then((result) => {
|
||||||
|
console.log(`[trashReminderJob] ${result.remindersSent} Erinnerungen versendet.`);
|
||||||
|
return prisma.$disconnect();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('[trashReminderJob] Fehlgeschlagen:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -38,3 +38,18 @@ authRouter.get('/auth/me', requireAuth, async (req: AuthedRequest, res: Response
|
|||||||
if (!user) return res.status(404).json({ error: 'Nutzer nicht gefunden' });
|
if (!user) return res.status(404).json({ error: 'Nutzer nicht gefunden' });
|
||||||
res.status(200).json({ user: publicUser(user) });
|
res.status(200).json({ user: publicUser(user) });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// PUT /v1/auth/push-token { pushToken } -> registriert die OneSignal Player ID
|
||||||
|
// des aktuellen Geräts. Ohne registrierten Token bleibt sendPushNotification()
|
||||||
|
// ein reines In-App-Log (siehe services/notificationService.ts).
|
||||||
|
authRouter.put('/auth/push-token', requireAuth, async (req: AuthedRequest, res: Response) => {
|
||||||
|
const { pushToken } = req.body || {};
|
||||||
|
if (pushToken !== null && typeof pushToken !== 'string') {
|
||||||
|
return res.status(400).json({ error: 'pushToken muss ein String oder null sein' });
|
||||||
|
}
|
||||||
|
await prisma.user.update({
|
||||||
|
where: { id: req.user!.id },
|
||||||
|
data: { pushToken: pushToken || null },
|
||||||
|
});
|
||||||
|
res.status(200).json({ ok: true });
|
||||||
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user