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 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 | 11x 11x 11x 11x 11x 11x 10x 10x 10x 4x 4x 4x 1x 4x 13x 10x 12x 12x 11x 1x 1x 10x 10x 11x 13x 12x 13x 13x 13x 13x 12x 12x 12x 12x 1x 12x | import { createDal } from "../../../dal";
import { getDb } from "../../../db";
import { NotificationService } from "../../../services/notification/notification.service";
import { CommunicationGateway } from "../gateway";
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();
if (dueReminders.length === 0) return;
// Batch 1: single query to find all reminder_due logs from the last hour.
// Replaces the per-reminder findRecentByEventAndMetadata call (was N queries).
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000);
const recentLogs = await dal.notificationDeliveryLog.findAllRecentByEvent(
"reminder_due",
oneHourAgo,
);
const alreadyNotified = new Set<number>(
recentLogs
.map((log) => {
try {
const meta = JSON.parse(log.metadataJson ?? "{}") as Record<
string,
unknown
>;
return typeof meta.reminderId === "number" ? meta.reminderId : null;
} catch {
return null;
}
})
.filter((id): id is number => id !== null),
);
// Batch 2: fetch team members once per unique proId (parallel, not per reminder).
// Replaces the per-reminder getProUsersWithContact call (was N queries; now U queries
// where U = unique pros in this cron batch, which is typically much smaller than N).
const uniqueProIds = [...new Set(dueReminders.map((r) => r.proId))];
const teamMemberEntries = await Promise.all(
uniqueProIds.map(async (proId) => {
try {
const members = await dal.userTenantRoles.getProUsersWithContact(proId);
return [proId, members] as const;
} catch (err) {
console.error(
"[REMINDER] Failed to fetch team members for pro",
proId,
":",
err,
);
return [
proId,
[] as Awaited<
ReturnType<typeof dal.userTenantRoles.getProUsersWithContact>
>,
] as const;
}
}),
);
const teamMembersByProId = new Map(teamMemberEntries);
const portalUrl = env.PORTAL_URL || "https://portal.interioring.com";
for (const { reminder, leadId, proId, customerName } of dueReminders) {
if (alreadyNotified.has(reminder.id)) continue;
const teamMembers = teamMembersByProId.get(proId) ?? [];
const ownersAndManagers = teamMembers.filter(
(m) => m.role === "owner" || m.role === "manager",
);
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" });
}
}
|