Skip to content

Commit 8403c94

Browse files
arpandhakalclaude
andcommitted
refactor(notifications): make create() route all completion actions to IU
Greptile flagged that groupedEventTypeFor maps all four Completed* actions to COMPLETED, but create()'s isRecipientIu only covered CompletedByIU and CompletedForCompanyByIU. Latent today (Completed/CompletedByCompanyMember only reach createBulkNotification), but a future create(Completed) call would buffer the IU recipient as a CU row and mis-deliver. Extract a single isCompletionAction predicate used by both create() and groupedEventTypeFor so the two can't drift, and extend the completion test to cover all four actions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent f1d6966 commit 8403c94

2 files changed

Lines changed: 37 additions & 31 deletions

File tree

src/app/api/notification/notification.service.test.ts

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -372,29 +372,33 @@ describe('guard: IU wiring boundaries', () => {
372372
})
373373

374374
describe('guard: IU completion emails', () => {
375-
it.each([NotificationTaskActions.CompletedByIU, NotificationTaskActions.CompletedForCompanyByIU])(
376-
'buffers a %s email as a COMPLETED IU event and keeps the in-product notification immediate',
377-
async (action) => {
378-
await buildService().create(action, makeTask({ assigneeType: AssigneeType.internalUser, clientId: null }))
375+
// Every completion action routes to an IU; create() must buffer them as IU rows regardless of
376+
// which one is passed, so the guard stays consistent with groupedEventTypeFor.
377+
it.each([
378+
NotificationTaskActions.CompletedByIU,
379+
NotificationTaskActions.CompletedForCompanyByIU,
380+
NotificationTaskActions.Completed,
381+
NotificationTaskActions.CompletedByCompanyMember,
382+
])('buffers a %s email as a COMPLETED IU event and keeps the in-product notification immediate', async (action) => {
383+
await buildService().create(action, makeTask({ assigneeType: AssigneeType.internalUser, clientId: null }))
379384

380-
expect(mockGroupedCreateMany).toHaveBeenCalledTimes(1)
381-
const row = mockGroupedCreateMany.mock.calls[0][0].data[0]
382-
expect(row).toMatchObject({
383-
recipientIuId: '33333333-3333-3333-3333-333333333333',
384-
recipientClientId: null,
385-
recipientCompanyId: null,
386-
eventType: GroupedEmailEventType.COMPLETED,
387-
})
388-
expect(row.individualEmail.recipientInternalUserId).toBe('33333333-3333-3333-3333-333333333333')
385+
expect(mockGroupedCreateMany).toHaveBeenCalledTimes(1)
386+
const row = mockGroupedCreateMany.mock.calls[0][0].data[0]
387+
expect(row).toMatchObject({
388+
recipientIuId: '33333333-3333-3333-3333-333333333333',
389+
recipientClientId: null,
390+
recipientCompanyId: null,
391+
eventType: GroupedEmailEventType.COMPLETED,
392+
})
393+
expect(row.individualEmail.recipientInternalUserId).toBe('33333333-3333-3333-3333-333333333333')
389394

390-
expect(mockCreateNotification).toHaveBeenCalledTimes(1)
391-
const sent = mockCreateNotification.mock.calls[0][0]
392-
expect(sent.recipientInternalUserId).toBe('33333333-3333-3333-3333-333333333333')
393-
expect(sent.recipientClientId).toBeUndefined()
394-
expect(deliveryTargetsOf(0).inProduct).toBeDefined()
395-
expect(deliveryTargetsOf(0).email).toBeUndefined()
396-
},
397-
)
395+
expect(mockCreateNotification).toHaveBeenCalledTimes(1)
396+
const sent = mockCreateNotification.mock.calls[0][0]
397+
expect(sent.recipientInternalUserId).toBe('33333333-3333-3333-3333-333333333333')
398+
expect(sent.recipientClientId).toBeUndefined()
399+
expect(deliveryTargetsOf(0).inProduct).toBeDefined()
400+
expect(deliveryTargetsOf(0).email).toBeUndefined()
401+
})
398402

399403
it('does not buffer and still routes the in-product CompletedByIU notification to the IU when email is disabled', async () => {
400404
const task = makeTask({ assigneeType: AssigneeType.internalUser, clientId: null })

src/app/api/notification/notification.service.ts

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,8 @@ export class NotificationService extends BaseService {
3939
const isAssignedToIu =
4040
task.assigneeType === AssigneeType.internalUser &&
4141
(action === NotificationTaskActions.Assigned || action === NotificationTaskActions.ReassignedToIU)
42-
// Completion notifications go to the task creator, an IU
43-
const isRecipientIu =
44-
isAssignedToIu ||
45-
action === NotificationTaskActions.CompletedByIU ||
46-
action === NotificationTaskActions.CompletedForCompanyByIU
42+
// Completion notifications always go to IUs (task creator, or IUs with access)
43+
const isRecipientIu = isAssignedToIu || this.isCompletionAction(action)
4744

4845
// 1. Check for existing notification. Skip if duplicate. This dedup is keyed on the client
4946
// assignee, so it must not gate IU-recipient notifications (e.g. CompletedByIU on a
@@ -616,7 +613,17 @@ export class NotificationService extends BaseService {
616613
})
617614
}
618615

616+
private isCompletionAction(action: NotificationTaskActions): boolean {
617+
return (
618+
action === NotificationTaskActions.Completed ||
619+
action === NotificationTaskActions.CompletedByIU ||
620+
action === NotificationTaskActions.CompletedByCompanyMember ||
621+
action === NotificationTaskActions.CompletedForCompanyByIU
622+
)
623+
}
624+
619625
private groupedEventTypeFor(action: NotificationTaskActions): GroupedEmailEventType | null {
626+
if (this.isCompletionAction(action)) return GroupedEmailEventType.COMPLETED
620627
switch (action) {
621628
case NotificationTaskActions.Assigned:
622629
case NotificationTaskActions.AssignedToCompany:
@@ -627,11 +634,6 @@ export class NotificationService extends BaseService {
627634
return GroupedEmailEventType.SHARED
628635
case NotificationTaskActions.Commented:
629636
return GroupedEmailEventType.COMMENT
630-
case NotificationTaskActions.Completed:
631-
case NotificationTaskActions.CompletedByIU:
632-
case NotificationTaskActions.CompletedByCompanyMember:
633-
case NotificationTaskActions.CompletedForCompanyByIU:
634-
return GroupedEmailEventType.COMPLETED
635637
default:
636638
return null
637639
}

0 commit comments

Comments
 (0)