Skip to content

Commit cb8947a

Browse files
committed
feat(app): trigger notification in task-schedule
1 parent bdb757d commit cb8947a

File tree

4 files changed

+83
-262
lines changed

4 files changed

+83
-262
lines changed

apps/app/src/jobs/tasks/task/task-schedule.ts

Lines changed: 83 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { db } from '@db';
2-
import { sendTaskReviewNotificationEmail } from '@trycompai/email';
2+
import { Novu } from '@novu/api';
33
import { logger, schedules } from '@trigger.dev/sdk';
44

55
export const taskSchedule = schedules.task({
@@ -8,6 +8,9 @@ export const taskSchedule = schedules.task({
88
maxDuration: 1000 * 60 * 10, // 10 minutes
99
run: async () => {
1010
const now = new Date();
11+
const novu = new Novu({
12+
secretKey: process.env.NOVU_API_KEY
13+
});
1114

1215
// Find all Done tasks that have a review date and frequency set
1316
const candidateTasks = await db.task.findMany({
@@ -23,18 +26,39 @@ export const taskSchedule = schedules.task({
2326
include: {
2427
organization: {
2528
select: {
29+
id: true,
2630
name: true,
31+
members: {
32+
where: {
33+
role: { contains: 'owner' }
34+
},
35+
select: {
36+
user: {
37+
select: {
38+
id: true,
39+
name: true,
40+
email: true,
41+
},
42+
},
43+
},
44+
},
2745
},
2846
},
2947
assignee: {
30-
include: {
31-
user: true,
48+
select: {
49+
user: {
50+
select: {
51+
id: true,
52+
name: true,
53+
email: true,
54+
},
55+
},
3256
},
3357
},
3458
},
3559
});
3660

37-
// Helpers to compute next due date based on frequency
61+
// FIle all tasks past their review deadline.
3862
const addDaysToDate = (date: Date, days: number) => {
3963
const result = new Date(date.getTime());
4064
result.setDate(result.getDate() + days);
@@ -90,8 +114,8 @@ export const taskSchedule = schedules.task({
90114
};
91115
}
92116

93-
// Update all overdue tasks to "todo" status
94117
try {
118+
// Update all overdue tasks to "todo" status
95119
const taskIds = overdueTasks.map((task) => task.id);
96120

97121
const updateResult = await db.task.updateMany({
@@ -105,119 +129,74 @@ export const taskSchedule = schedules.task({
105129
},
106130
});
107131

108-
109-
110-
// Log details about updated tasks
111-
overdueTasks.forEach((task) => {
112-
logger.info(
113-
`Updated task "${task.title}" (${task.id}) from org "${task.organization.name}" - frequency ${task.frequency} - last reviewed ${task.reviewDate?.toISOString()}`,
114-
);
115-
});
116-
117-
logger.info(`Successfully updated ${updateResult.count} tasks to "todo" status`);
118-
119-
// Build a map of admins by organization for targeted notifications
120-
const uniqueOrgIds = Array.from(new Set(overdueTasks.map((t) => t.organizationId)));
121-
const admins = await db.member.findMany({
122-
where: {
123-
organizationId: { in: uniqueOrgIds },
124-
isActive: true,
125-
// role is a comma-separated string sometimes
126-
role: { contains: 'admin' },
127-
},
128-
include: {
129-
user: true,
130-
},
131-
});
132-
133-
const adminsByOrgId = new Map<string, { email: string; name: string }[]>();
134-
admins.forEach((admin) => {
135-
const email = admin.user?.email;
136-
if (!email) return;
137-
const list = adminsByOrgId.get(admin.organizationId) ?? [];
138-
list.push({ email, name: admin.user.name ?? email });
139-
adminsByOrgId.set(admin.organizationId, list);
140-
});
141-
142-
// Rate limit: 2 emails per second
143-
const EMAIL_BATCH_SIZE = 2;
144-
const EMAIL_BATCH_DELAY_MS = 1000;
145-
146-
// Build a flat list of email jobs
147-
type EmailJob = {
132+
const recipientsMap = new Map<string, {
148133
email: string;
134+
userId: string;
149135
name: string;
150136
task: typeof overdueTasks[number];
151-
};
152-
const emailJobs: EmailJob[] = [];
153-
154-
// Helper to compute next due date again for email content
155-
const computeNextDueDate = (reviewDate: Date, frequency: string): Date | null => {
156-
switch (frequency) {
157-
case 'daily':
158-
return addDaysToDate(reviewDate, 1);
159-
case 'weekly':
160-
return addDaysToDate(reviewDate, 7);
161-
case 'monthly':
162-
return addMonthsToDate(reviewDate, 1);
163-
case 'quarterly':
164-
return addMonthsToDate(reviewDate, 3);
165-
case 'yearly':
166-
return addMonthsToDate(reviewDate, 12);
167-
default:
168-
return null;
137+
}>();
138+
const addRecipients = (
139+
users: Array<{ user: { id: string; email: string; name?: string } }>,
140+
task: typeof overdueTasks[number],
141+
) => {
142+
for (const entry of users) {
143+
const user = entry.user;
144+
if (user && user.email && user.id) {
145+
const key = `${user.id}-${task.id}`;
146+
if (!recipientsMap.has(key)) {
147+
recipientsMap.set(key, {
148+
email: user.email,
149+
userId: user.id,
150+
name: user.name ?? '',
151+
task,
152+
});
153+
}
154+
}
169155
}
170156
};
171157

158+
// Find recipients (org owner and assignee) for each task and add to recipientsMap
172159
for (const task of overdueTasks) {
173-
const recipients = new Map<string, string>(); // email -> name
174-
175-
// Assignee (if any)
176-
const assigneeEmail = task.assignee?.user?.email;
177-
if (assigneeEmail) {
178-
recipients.set(assigneeEmail, task.assignee?.user?.name ?? assigneeEmail);
160+
// Org owners
161+
if (task.organization && Array.isArray(task.organization.members)) {
162+
addRecipients(task.organization.members, task);
179163
}
180-
181-
// Organization admins
182-
const orgAdmins = adminsByOrgId.get(task.organizationId) ?? [];
183-
orgAdmins.forEach((a) => recipients.set(a.email, a.name));
184-
185-
if (recipients.size === 0) {
186-
logger.info(`No recipients found for task ${task.id} (${task.title})`);
187-
continue;
164+
// Policy assignee
165+
if (task.assignee) {
166+
addRecipients([task.assignee], task);
188167
}
168+
}
189169

190-
for (const [email, name] of recipients.entries()) {
191-
emailJobs.push({ email, name, task });
192-
}
170+
// Final deduplicated recipients array.
171+
const recipients = Array.from(recipientsMap.values());
172+
// Trigger notification for each recipient.
173+
for (const recipient of recipients) {
174+
novu.trigger({
175+
workflowId: 'task-review-required',
176+
to: {
177+
subscriberId: `${recipient.userId}-${recipient.task.organizationId}`,
178+
email: recipient.email,
179+
},
180+
payload: {
181+
email: recipient.email,
182+
userName: recipient.name,
183+
taskName: recipient.task.title,
184+
organizationName: recipient.task.organization.name,
185+
organizationId: recipient.task.organizationId,
186+
taskId: recipient.task.id,
187+
taskUrl: `${process.env.NEXT_PUBLIC_APP_URL ?? 'https://app.trycomp.ai'}/${recipient.task.organizationId}/tasks/${recipient.task.id}`,
188+
}
189+
});
193190
}
194191

195-
for (let i = 0; i < emailJobs.length; i += EMAIL_BATCH_SIZE) {
196-
const batch = emailJobs.slice(i, i + EMAIL_BATCH_SIZE);
197-
198-
await Promise.all(
199-
batch.map(async ({ email, name, task }) => {
200-
try {
201-
await sendTaskReviewNotificationEmail({
202-
email,
203-
userName: name,
204-
taskName: task.title,
205-
organizationName: task.organization.name,
206-
organizationId: task.organizationId,
207-
taskId: task.id,
208-
});
209-
logger.info(`Sent task review notification to ${email} for task ${task.id}`);
210-
} catch (emailError) {
211-
logger.error(`Failed to send review email to ${email} for task ${task.id}: ${emailError}`);
212-
}
213-
}),
192+
// Log details about updated tasks
193+
overdueTasks.forEach((task) => {
194+
logger.info(
195+
`Updated task "${task.title}" (${task.id}) from org "${task.organization.name}" - frequency ${task.frequency} - last reviewed ${task.reviewDate?.toISOString()}`,
214196
);
197+
});
215198

216-
// Only delay if there are more emails to send
217-
if (i + EMAIL_BATCH_SIZE < emailJobs.length) {
218-
await new Promise((resolve) => setTimeout(resolve, EMAIL_BATCH_DELAY_MS));
219-
}
220-
}
199+
logger.info(`Successfully updated ${updateResult.count} tasks to "todo" status`);
221200

222201
return {
223202
success: true,

packages/email/emails/task-review-notification.tsx

Lines changed: 0 additions & 115 deletions
This file was deleted.

packages/email/index.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,11 @@ export * from './emails/magic-link';
55
export * from './emails/marketing/welcome';
66
export * from './emails/otp';
77
export * from './emails/policy-notification';
8-
export * from './emails/task-review-notification';
98
export * from './emails/waitlist';
109

1110
// Email sending functions
1211
export * from './lib/invite-member';
1312
export * from './lib/magic-link';
1413
export * from './lib/policy-notification';
15-
export * from './lib/task-review-notification';
1614
export * from './lib/resend';
1715
export * from './lib/waitlist';

0 commit comments

Comments
 (0)