@@ -292,9 +292,13 @@ describe('guard: CU wiring boundaries', () => {
292292 expect ( svc . groupedEventTypeFor ( NotificationTaskActions . Shared ) ) . toBe ( GroupedEmailEventType . SHARED )
293293 expect ( svc . groupedEventTypeFor ( NotificationTaskActions . SharedToCompany ) ) . toBe ( GroupedEmailEventType . SHARED )
294294 expect ( svc . groupedEventTypeFor ( NotificationTaskActions . Commented ) ) . toBe ( GroupedEmailEventType . COMMENT )
295+ expect ( svc . groupedEventTypeFor ( NotificationTaskActions . Completed ) ) . toBe ( GroupedEmailEventType . COMPLETED )
296+ expect ( svc . groupedEventTypeFor ( NotificationTaskActions . CompletedByIU ) ) . toBe ( GroupedEmailEventType . COMPLETED )
297+ expect ( svc . groupedEventTypeFor ( NotificationTaskActions . CompletedByCompanyMember ) ) . toBe ( GroupedEmailEventType . COMPLETED )
298+ expect ( svc . groupedEventTypeFor ( NotificationTaskActions . CompletedForCompanyByIU ) ) . toBe ( GroupedEmailEventType . COMPLETED )
295299 } )
296300
297- it ( 'returns null for every action that must not be buffered' , ( ) => {
301+ it ( 'returns null for every action that must not be buffered (incl. shared-CU completion emails) ' , ( ) => {
298302 const svc = buildService ( ) as unknown as {
299303 groupedEventTypeFor : ( a : NotificationTaskActions ) => GroupedEmailEventType | null
300304 }
@@ -305,23 +309,17 @@ describe('guard: CU wiring boundaries', () => {
305309 NotificationTaskActions . Shared ,
306310 NotificationTaskActions . SharedToCompany ,
307311 NotificationTaskActions . Commented ,
312+ NotificationTaskActions . Completed ,
313+ NotificationTaskActions . CompletedByIU ,
314+ NotificationTaskActions . CompletedByCompanyMember ,
315+ NotificationTaskActions . CompletedForCompanyByIU ,
308316 ]
309317 const unmapped = Object . values ( NotificationTaskActions ) . filter ( ( a ) => ! mapped . includes ( a ) )
310318 for ( const action of unmapped ) {
311319 expect ( svc . groupedEventTypeFor ( action ) ) . toBeNull ( )
312320 }
313321 } )
314322
315- it ( 'never returns COMPLETED — that type is reserved for the deferred IU milestone' , ( ) => {
316- const svc = buildService ( ) as unknown as {
317- groupedEventTypeFor : ( a : NotificationTaskActions ) => GroupedEmailEventType | null
318- }
319- const allActions = Object . values ( NotificationTaskActions )
320- for ( const action of allActions ) {
321- expect ( svc . groupedEventTypeFor ( action ) ) . not . toBe ( GroupedEmailEventType . COMPLETED )
322- }
323- } )
324-
325323 it ( 'never writes recipientIuId in a CU grouped event row' , async ( ) => {
326324 await buildService ( ) . create ( NotificationTaskActions . Assigned , makeTask ( ) )
327325 const row = mockGroupedCreateMany . mock . calls [ 0 ] [ 0 ] . data [ 0 ]
@@ -372,3 +370,94 @@ describe('guard: IU wiring boundaries', () => {
372370 expect ( mockEnqueueFlush ) . not . toHaveBeenCalled ( )
373371 } )
374372} )
373+
374+ describe ( 'guard: IU completion emails' , ( ) => {
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 } ) )
384+
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' )
394+
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+ } )
402+
403+ it ( 'does not buffer and still routes the in-product CompletedByIU notification to the IU when email is disabled' , async ( ) => {
404+ const task = makeTask ( { assigneeType : AssigneeType . internalUser , clientId : null } )
405+ await buildService ( ) . create ( NotificationTaskActions . CompletedByIU , task , { disableEmail : true } )
406+
407+ expect ( mockGroupedCreateMany ) . not . toHaveBeenCalled ( )
408+ const sent = mockCreateNotification . mock . calls [ 0 ] [ 0 ]
409+ expect ( sent . recipientInternalUserId ) . toBe ( '33333333-3333-3333-3333-333333333333' )
410+ expect ( sent . recipientClientId ) . toBeUndefined ( )
411+ expect ( deliveryTargetsOf ( 0 ) . email ) . toBeUndefined ( )
412+ } )
413+
414+ it ( 'is not blocked by the client-notification dedup guard on a client-assigned task' , async ( ) => {
415+ // A client-assigned task still has an unread ClientNotification from its assignment when the
416+ // IU completes it; the CompletedByIU recipient is the creator IU, so the guard must not fire.
417+ mockFindFirst . mockResolvedValue ( { id : 'existing-client-notif' } )
418+ const task = makeTask ( { assigneeType : AssigneeType . client , clientId : '33333333-3333-3333-3333-333333333333' } )
419+
420+ await buildService ( ) . create ( NotificationTaskActions . CompletedByIU , task , { disableEmail : false } )
421+
422+ // guard skipped: the completion email buffers and the IU notification dispatches
423+ expect ( mockFindFirst ) . not . toHaveBeenCalled ( )
424+ expect ( mockGroupedCreateMany ) . toHaveBeenCalledTimes ( 1 )
425+ expect ( mockGroupedCreateMany . mock . calls [ 0 ] [ 0 ] . data [ 0 ] . recipientIuId ) . toBe ( '33333333-3333-3333-3333-333333333333' )
426+ expect ( mockCreateNotification ) . toHaveBeenCalledTimes ( 1 )
427+ expect ( mockCreateNotification . mock . calls [ 0 ] [ 0 ] . recipientInternalUserId ) . toBe ( '33333333-3333-3333-3333-333333333333' )
428+ } )
429+
430+ it ( 'bulk Completed buffers one COMPLETED IU row per recipient and strips the email from dispatch' , async ( ) => {
431+ await buildService ( ) . createBulkNotification ( NotificationTaskActions . Completed , makeTask ( ) , [ 'iu_a' , 'iu_b' ] , {
432+ email : true ,
433+ } )
434+
435+ expect ( mockGroupedCreateMany ) . toHaveBeenCalledTimes ( 2 )
436+ const rows = mockGroupedCreateMany . mock . calls . map ( ( c ) => c [ 0 ] . data [ 0 ] )
437+ expect ( rows . map ( ( r ) => r . recipientIuId ) ) . toEqual ( [ 'iu_a' , 'iu_b' ] )
438+ for ( const row of rows ) {
439+ expect ( row . eventType ) . toBe ( GroupedEmailEventType . COMPLETED )
440+ expect ( row . recipientClientId ) . toBeNull ( )
441+ expect ( row . individualEmail . recipientInternalUserId ) . toBeDefined ( )
442+ }
443+
444+ expect ( mockCreateNotification ) . toHaveBeenCalledTimes ( 2 )
445+ const sent = mockCreateNotification . mock . calls . map ( ( c ) => c [ 0 ] )
446+ expect ( sent . map ( ( s ) => s . recipientInternalUserId ) ) . toEqual ( [ 'iu_a' , 'iu_b' ] )
447+ for ( const s of sent ) {
448+ expect ( s . recipientClientId ) . toBeUndefined ( )
449+ expect ( s . deliveryTargets . email ) . toBeUndefined ( )
450+ }
451+ } )
452+
453+ it ( 'bulk CompletedByCompanyMember neither buffers nor emails when the flag is off (email opt falsy)' , async ( ) => {
454+ await buildService ( ) . createBulkNotification ( NotificationTaskActions . CompletedByCompanyMember , makeTask ( ) , [ 'iu_a' ] , {
455+ email : false ,
456+ } )
457+
458+ expect ( mockGroupedCreateMany ) . not . toHaveBeenCalled ( )
459+ expect ( mockCreateNotification ) . toHaveBeenCalledTimes ( 1 )
460+ expect ( deliveryTargetsOf ( 0 ) . email ) . toBeUndefined ( )
461+ expect ( mockCreateNotification . mock . calls [ 0 ] [ 0 ] . recipientInternalUserId ) . toBe ( 'iu_a' )
462+ } )
463+ } )
0 commit comments