@@ -421,6 +421,87 @@ describe('policyAcknowledgmentDigest', () => {
421421 } ) ;
422422 } ) ;
423423
424+ it ( 'rolls up across orgs by email when the same person has multiple User records (schema allows duplicate emails)' , async ( ) => {
425+ // Regression: User.email is not @unique in the Prisma schema, so one
426+ // person can end up with multiple user rows — typically when they get
427+ // invited to separate orgs through different flows. Keying the rollup
428+ // on user.id split those duplicates into one email each. Rollup must
429+ // collapse by normalized email instead.
430+ mockFindMany . mockResolvedValueOnce ( [
431+ {
432+ id : 'org_1' ,
433+ name : 'Acme' ,
434+ policy : [
435+ {
436+ id : 'pol_a' ,
437+ name : 'Access Control' ,
438+ signedBy : [ ] ,
439+ visibility : 'ALL' ,
440+ visibleToDepartments : [ ] ,
441+ } ,
442+ ] ,
443+ members : [
444+ {
445+ id : 'mem_1' ,
446+ department : 'it' ,
447+ user : {
448+ id : 'usr_alice_first' ,
449+ name : 'Alice' ,
450+ email : 'alice@example.com' ,
451+ role : null ,
452+ } ,
453+ } ,
454+ ] ,
455+ } ,
456+ {
457+ id : 'org_2' ,
458+ name : 'Beta' ,
459+ policy : [
460+ {
461+ id : 'pol_b' ,
462+ name : 'Backup' ,
463+ signedBy : [ ] ,
464+ visibility : 'ALL' ,
465+ visibleToDepartments : [ ] ,
466+ } ,
467+ ] ,
468+ members : [
469+ {
470+ id : 'mem_2' ,
471+ department : 'hr' ,
472+ user : {
473+ // Different user row, same email — Alice was re-invited under
474+ // a separate user record.
475+ id : 'usr_alice_second' ,
476+ name : 'Alice' ,
477+ email : 'ALICE@example.com' ,
478+ role : null ,
479+ } ,
480+ } ,
481+ ] ,
482+ } ,
483+ ] ) ;
484+
485+ const result = await taskUnderTest . run ( { timestamp : new Date ( ) } as never ) ;
486+
487+ expect ( mockSendEmailViaApi ) . toHaveBeenCalledTimes ( 1 ) ;
488+ const call = mockSendEmailViaApi . mock . calls [ 0 ] [ 0 ] as {
489+ to : string ;
490+ subject : string ;
491+ organizationId : string ;
492+ } ;
493+ expect ( call . subject ) . toBe (
494+ 'You have 2 policies to review across 2 organizations' ,
495+ ) ;
496+ expect ( call . organizationId ) . toBe ( 'org_1' ) ;
497+ expect ( result ) . toMatchObject ( {
498+ success : true ,
499+ orgsProcessed : 2 ,
500+ recipients : 1 ,
501+ emailsSent : 1 ,
502+ } ) ;
503+ } ) ;
504+
424505 it ( 'drops a single org from the rollup when the user is unsubscribed there, but still emails about other orgs' , async ( ) => {
425506 mockFindMany . mockResolvedValueOnce ( [
426507 {
0 commit comments