All files / lib/communication/cron reminder-notifications.ts

100% Statements 22/22
100% Branches 12/12
100% Functions 3/3
100% Lines 22/22

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82            8x 8x 8x 8x   8x   8x   8x 8x           8x 1x       7x 7x 9x     7x 8x   8x 8x                                                       7x 7x           8x     1x       7x      
import { getDb } from "../../../db";
import { createDal } from "../../../dal";
import { CommunicationGateway } from "../gateway";
import { NotificationService } from "../../../services/notification/notification.service";
 
export async function checkDueReminders(env: CloudflareBindings): Promise<void> {
	const db = getDb(env.DB);
	const dal = createDal(db);
	const gateway = new CommunicationGateway(dal, env);
	const notificationService = new NotificationService(dal, env);
 
	const dueReminders = await dal.leadReminders.findDueReminders();
 
	for (const { reminder, leadId, proId, customerName } of dueReminders) {
		// Deduplicate: skip if already notified in the last hour
		const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000);
		const recentNotifications = await dal.communicationLog.findRecentByEventAndMetadata(
			"reminder_due",
			oneHourAgo,
			`"reminderId":${reminder.id}`,
		);
 
		if (recentNotifications.length > 0) {
			continue;
		}
 
		// Find pro team owners/managers to notify
		const teamMembers = await dal.userTenantRoles.getProUsersWithContact(proId);
		const ownersAndManagers = teamMembers.filter(
			(m) => m.role === "owner" || m.role === "manager",
		);
 
		const portalUrl = env.PORTAL_URL || "https://portal.interioring.com";
		const leadUrl = `${portalUrl}/crm/leads/${leadId}`;
 
		for (const member of ownersAndManagers) {
			await gateway.send({
				channel: "email",
				recipient: member.email,
				eventType: "reminder_due",
				proId,
				userId: member.userId,
				content: {
					template: "reminder-due",
					subject: `Reminder due: ${reminder.title}`,
					props: {
						proName: member.name,
						reminderTitle: reminder.title,
						customerName,
						dueAt: reminder.dueAt.toLocaleString("en-IN", {
							timeZone: "Asia/Kolkata",
							dateStyle: "medium",
							timeStyle: "short",
						}),
						notes: reminder.notes ?? undefined,
						leadUrl,
					},
					recipientName: member.name,
				},
				metadata: { reminderId: reminder.id, leadId },
			});
		}
 
		// Send push notifications to owners/managers
		try {
			await notificationService.notify({
				proId,
				eventType: "reminder_due",
				title: `Reminder: ${reminder.title || "Follow up"}`,
				body: reminder.notes?.substring(0, 200) || "You have a reminder due",
				data: { url: `/crm/leads/${leadId}`, leadId },
				targetUserIds: ownersAndManagers.map((m) => m.userId),
			});
		} catch (err) {
			console.error("[REMINDER] Failed to send push notification:", err);
		}
 
		// Update status from "upcoming" to "overdue"
		await dal.leadReminders.update(reminder.id, { status: "overdue" });
	}
}