@@ -14,6 +14,7 @@ import APIError from '@api/core/exceptions/api'
1414import { BaseService } from '@api/core/services/base.service'
1515import { NotificationTaskActions } from '@api/core/types/tasks'
1616import { getEmailDetails , getInProductNotificationDetails , mergeEmailOverride } from '@api/notification/notification.helpers'
17+ // import { resolveIuNotificationSettingId } from '@api/notification/resolveNotificationSettingId'
1718import { AssigneeType , ClientNotification , GroupedEmailEventType , Prisma , Task } from '@prisma/client'
1819import { randomUUID } from 'crypto'
1920import { enqueueGroupedEmailFlush } from '@/jobs/notifications/flush-grouped-email'
@@ -28,12 +29,12 @@ export class NotificationService extends BaseService {
2829 action : NotificationTaskActions ,
2930 task : Task ,
3031 opts : {
31- disableEmail : boolean
32+ disableEmail ? : boolean
3233 disableInProduct ?: boolean
3334 commentId ?: string
3435 senderCompanyId ?: string
3536 emailOverride ?: EmailNotificationDetails
36- } = { disableEmail : false } ,
37+ } = { } ,
3738 ) {
3839 try {
3940 const isAssignedToIu =
@@ -72,7 +73,12 @@ export class NotificationService extends BaseService {
7273 : getEmailDetails ( workspace , actionUser , task , { commentId : opts ?. commentId } ) [ action ]
7374 const email = baseEmail ? mergeEmailOverride ( { base : baseEmail , override : opts . emailOverride } ) : baseEmail
7475
75- const groupedType = email && recipientId ? this . groupedEventTypeFor ( action ) : null
76+ const category = this . groupedEventTypeFor ( action )
77+ // TODO(OUT-3929): re-enable per-IU gating once Copilot exposes a preference-read endpoint — ship IUs ungated for now.
78+ const notificationSettingId = undefined
79+ // const notificationSettingId = isRecipientIu && category ? await resolveIuNotificationSettingId({ copilot: this.copilot, workspaceId: task.workspaceId, category }) : undefined
80+
81+ const groupedType = email && recipientId ? category : null
7682 if ( groupedType ) {
7783 const association = AssociationsSchema . parse ( task . associations ) ?. [ 0 ]
7884 await this . bufferGroupedEmailEvent ( {
@@ -89,6 +95,7 @@ export class NotificationService extends BaseService {
8995 { email } ,
9096 senderCompanyId ,
9197 isRecipientIu ,
98+ notificationSettingId ,
9299 ) ,
93100 } )
94101 }
@@ -102,19 +109,17 @@ export class NotificationService extends BaseService {
102109 { inProduct, email } ,
103110 senderCompanyId ,
104111 isRecipientIu ,
112+ notificationSettingId ,
105113 )
106114 if ( groupedType ) notificationDetails . deliveryTargets = { inProduct }
107115 if ( ! inProduct && ! notificationDetails . deliveryTargets ?. email ) return
108116 console . info ( 'NotificationService#create | Creating single notification:' , notificationDetails )
109117
110- let notification : NotificationCreatedResponse
111- try {
112- notification = await this . copilot . createNotification ( notificationDetails )
113- } catch ( e : unknown ) {
114- notification = await this . handleIfSenderCompanyIdError ( e , notificationDetails )
115- }
118+ const notification = await this . dispatchNotification ( notificationDetails )
116119
117120 console . info ( 'NotificationService#create | Created single notification:' , notification )
121+ // Suppressed by the recipient IU's preference — nothing was created, so there's nothing to save.
122+ if ( ! notification ) return
118123
119124 // 3. Save notification to ClientNotification or InternalUserNotification table. Check for notification.recipientClientId too
120125 if ( task . assigneeType === AssigneeType . client && ! ! notification . recipientClientId && ! opts . disableInProduct ) {
@@ -194,9 +199,13 @@ export class NotificationService extends BaseService {
194199 const iuNotifications = [ ]
195200
196201 const association = AssociationsSchema . parse ( task . associations ) ?. [ 0 ]
197- // Non-null only when these emails should be diverted into the grouped buffer.
198- const groupedType = email ? this . groupedEventTypeFor ( action ) : null
202+ const category = this . groupedEventTypeFor ( action )
199203 const isRecipientIu = opts . isRecipientIu
204+ // TODO(OUT-3929): re-enable per-IU gating once Copilot exposes a preference-read endpoint — ship IUs ungated for now.
205+ const notificationSettingId = undefined
206+ // const notificationSettingId = isRecipientIu && category ? await resolveIuNotificationSettingId({ copilot: this.copilot, workspaceId: task.workspaceId, category }) : undefined
207+ // Non-null only when these emails should be diverted into the grouped buffer.
208+ const groupedType = email ? category : null
200209
201210 // NOTE: The reason we are skipping using NotificationService#create and implementing notification dispatch + save manually is because
202211 // we can just do one `createMany` DB call instead of one per notification, saving a ton of DB calls
@@ -227,6 +236,7 @@ export class NotificationService extends BaseService {
227236 { email } ,
228237 opts ?. senderCompanyId ,
229238 isRecipientIu ,
239+ notificationSettingId ,
230240 ) ,
231241 } )
232242 if ( ! inProduct ) continue
@@ -241,16 +251,12 @@ export class NotificationService extends BaseService {
241251 { inProduct, email } ,
242252 opts ?. senderCompanyId ,
243253 isRecipientIu ,
254+ notificationSettingId ,
244255 )
245256 if ( groupedType ) notificationDetails . deliveryTargets = { inProduct }
246257
247258 console . info ( 'NotificationService#bulkCreate | Creating single notification:' , notificationDetails )
248- let notification : NotificationCreatedResponse
249- try {
250- notification = await this . copilot . createNotification ( notificationDetails )
251- } catch ( e : unknown ) {
252- notification = await this . handleIfSenderCompanyIdError ( e , notificationDetails )
253- }
259+ const notification = await this . dispatchNotification ( notificationDetails )
254260
255261 console . info ( 'NotificationService#bulkCreate | Created single notification:' , notification )
256262 if ( ! notification ) {
@@ -580,6 +586,7 @@ export class NotificationService extends BaseService {
580586 )
581587 . map ( ( iu ) => iu . id )
582588 }
589+ break
583590 default :
584591 const userInfo = await this . copilot . me ( )
585592 senderId = z . string ( ) . parse ( userInfo ?. id )
@@ -637,7 +644,7 @@ export class NotificationService extends BaseService {
637644 }
638645 }
639646
640- private async bufferGroupedEmailEvent ( args : {
647+ async bufferGroupedEmailEvent ( args : {
641648 task : Task
642649 recipientId : string
643650 companyId ?: string
@@ -690,6 +697,16 @@ export class NotificationService extends BaseService {
690697 }
691698 }
692699
700+ private async dispatchNotification (
701+ notificationDetails : NotificationRequestBody ,
702+ ) : Promise < NotificationCreatedResponse | null > {
703+ try {
704+ return await this . copilot . createNotification ( notificationDetails )
705+ } catch ( e : unknown ) {
706+ return await this . handleIfSenderCompanyIdError ( e , notificationDetails )
707+ }
708+ }
709+
693710 private async handleIfSenderCompanyIdError ( e : unknown , notificationDetails : NotificationRequestBody ) {
694711 // Account for workspaces that don't have multi-companies enabled, thus don't support the senderCompanyId key
695712 // Yes, this is hacky. No, I don't have a choice (I can't find out if workspace has single/multi company at all from the Copilot API)
@@ -715,6 +732,9 @@ export class NotificationService extends BaseService {
715732 deliveryTargets : NotificationRequestBody [ 'deliveryTargets' ] ,
716733 senderCompanyId ?: string ,
717734 isRecipientIu ?: boolean ,
735+ // Set for IU payloads so the platform gates each requested surface (in-product and email)
736+ // against the IU's per-category preference. Suppression is enforced platform-side.
737+ notificationSettingId ?: string ,
718738 ) : NotificationRequestBody {
719739 const associations = AssociationsSchema . parse ( task . associations )
720740 const association = associations ?. [ 0 ]
@@ -730,6 +750,7 @@ export class NotificationService extends BaseService {
730750 delete notificationDetails . recipientCompanyId
731751 delete notificationDetails . recipientClientId
732752 notificationDetails . recipientInternalUserId = recipientId
753+ if ( notificationSettingId ) notificationDetails . notificationSettingId = notificationSettingId
733754 }
734755 return notificationDetails
735756 }
0 commit comments