11import { db } from '@db' ;
2- import { sendTaskReviewNotificationEmail } from '@trycompai/email ' ;
2+ import { Novu } from '@novu/api ' ;
33import { logger , schedules } from '@trigger.dev/sdk' ;
44
55export 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 ,
0 commit comments