From 9c1ab114062fea27b5bfa7c008853d603ab31bf5 Mon Sep 17 00:00:00 2001 From: Shivam K <292110273+shivoomiess@users.noreply.github.com> Date: Mon, 6 Jul 2026 11:09:55 +0200 Subject: [PATCH 1/3] add: emit event and trigger email handler for data access invite --- .../eventHandlers/email/emailTemplateId.ts | 1 + .../eventHandlers/email/essEmailHandler.ts | 49 +++++++++++++++++++ apps/backend/src/events/applicationEvents.ts | 7 +++ apps/backend/src/events/event.enum.ts | 8 +++ .../src/mutations/DataAccessUsersMutations.ts | 22 +++++++++ 5 files changed, 87 insertions(+) diff --git a/apps/backend/src/eventHandlers/email/emailTemplateId.ts b/apps/backend/src/eventHandlers/email/emailTemplateId.ts index e83c8e0853..9a64fe2ca2 100644 --- a/apps/backend/src/eventHandlers/email/emailTemplateId.ts +++ b/apps/backend/src/eventHandlers/email/emailTemplateId.ts @@ -18,4 +18,5 @@ export enum EmailTemplateId { CALL_CREATED_EMAIL = 'call-created-email', FEEDBACK_REQUEST = 'feedback-request', CO_PROPOSER_INVITE = 'co-proposer-invite', + DATA_ACCESS_USER_ADDED = 'data-access-user-added', } diff --git a/apps/backend/src/eventHandlers/email/essEmailHandler.ts b/apps/backend/src/eventHandlers/email/essEmailHandler.ts index b75e1a0662..e89210c984 100644 --- a/apps/backend/src/eventHandlers/email/essEmailHandler.ts +++ b/apps/backend/src/eventHandlers/email/essEmailHandler.ts @@ -350,6 +350,55 @@ export async function essEmailHandler(event: ApplicationEvent) { break; } + case Event.DATA_ACCESS_USER_ADDED: { + const proposal = await proposalDataSource.get(event.proposalPKey); + if (!proposal) { + logger.logError('No proposal found when trying to send email', { + proposalPKey: event.proposalPKey, + event, + }); + + return; + } + + const invitedUsers = await userDataSource.getBasicUsersInfo( + event.invitedUserIds + ); + + for (const user of invitedUsers) { + mailService + .sendMail({ + content: { + template: EmailTemplateId.DATA_ACCESS_USER_ADDED, + }, + substitution_data: { + preferredname: user.preferredname, + firstname: user.firstname, + lastname: user.lastname, + proposalTitle: proposal.title, + proposalId: proposal.proposalId, + }, + recipients: [{ address: user.email }], + }) + .then((res) => { + logger.logInfo('Email sent on data access user added', { + result: res, + userId: user.id, + proposalPKey: event.proposalPKey, + }); + }) + .catch((err: string) => { + logger.logError('Could not send email on data access user added', { + error: err, + userId: user.id, + event, + }); + }); + } + + return; + } + case Event.FAP_REVIEWER_NOTIFIED: { const { id: reviewId, userID, proposalPk } = event.fapReview; const fapReviewer = await userDataSource.getUser(userID); diff --git a/apps/backend/src/events/applicationEvents.ts b/apps/backend/src/events/applicationEvents.ts index 78af21d208..dc58146726 100644 --- a/apps/backend/src/events/applicationEvents.ts +++ b/apps/backend/src/events/applicationEvents.ts @@ -417,6 +417,12 @@ interface UserDataAccessUpdatedEvent extends GeneralEvent { proposalPKey: number; } +interface DataAccessUserAddedEvent extends GeneralEvent { + type: Event.DATA_ACCESS_USER_ADDED; + proposalPKey: number; + invitedUserIds: number[]; +} + interface ExperimentSafetyManagementDecisionSubmittedByISEvent extends GeneralEvent { type: Event.EXPERIMENT_SAFETY_MANAGEMENT_DECISION_SUBMITTED_BY_IS; @@ -555,6 +561,7 @@ export type ApplicationEvent = | VisitRegistrationApprovedEvent | VisitRegistrationCancelledEvent | UserDataAccessUpdatedEvent + | DataAccessUserAddedEvent | ExperimentESFApprovedByIsEvent | ExperimentESFApprovedByESREvent | ExperimentESFRejectedByIsEvent diff --git a/apps/backend/src/events/event.enum.ts b/apps/backend/src/events/event.enum.ts index 04013d83e4..fa9bf0f1b6 100644 --- a/apps/backend/src/events/event.enum.ts +++ b/apps/backend/src/events/event.enum.ts @@ -125,6 +125,7 @@ export enum Event { EXPERIMENT_ESF_APPROVED_BY_ESR = 'EXPERIMENT_ESF_APPROVED_BY_ESR', EXPERIMENT_ESF_REJECTED_BY_ESR = 'EXPERIMENT_ESF_REJECTED_BY_ESR', DATA_ACCESS_USERS_UPDATED = 'DATA_ACCESS_USERS_UPDATED', + DATA_ACCESS_USER_ADDED = 'DATA_ACCESS_USER_ADDED', EXPERIMENT_SAFETY_MANAGEMENT_DECISION_SUBMITTED_BY_IS = 'EXPERIMENT_SAFETY_MANAGEMENT_DECISION_SUBMITTED_BY_IS', EXPERIMENT_SAFETY_MANAGEMENT_DECISION_SUBMITTED_BY_ESR = 'EXPERIMENT_SAFETY_MANAGEMENT_DECISION_SUBMITTED_BY_ESR', EXPERIMENT_SAFETY_STATUS_CHANGED_BY_USER = 'EXPERIMENT_SAFETY_STATUS_CHANGED_BY_USER', @@ -652,6 +653,13 @@ export const EventMetadataByEvent = new Map([ Event.DATA_ACCESS_USERS_UPDATED, { label: 'Event occurs when data access users are updated' }, ], + [ + Event.DATA_ACCESS_USER_ADDED, + { + label: + 'Event occurs when an existing user is added as a data access user', + }, + ], [ Event.EXPERIMENT_SAFETY_MANAGEMENT_DECISION_SUBMITTED_BY_IS, { diff --git a/apps/backend/src/mutations/DataAccessUsersMutations.ts b/apps/backend/src/mutations/DataAccessUsersMutations.ts index 147355e382..9685752475 100644 --- a/apps/backend/src/mutations/DataAccessUsersMutations.ts +++ b/apps/backend/src/mutations/DataAccessUsersMutations.ts @@ -58,6 +58,17 @@ export default class DataAccessUsersMutations { ); } + // Data access users are stored via delete-all + reinsert, so capture the + // current members first to work out who is genuinely newly invited. + const existingDataAccessUsers = + await this.dataSource.findByProposalPk(proposalPk); + const existingUserIds = new Set( + existingDataAccessUsers.map((user) => user.id) + ); + const invitedUserIds = userIds.filter( + (userId) => !existingUserIds.has(userId) + ); + const result = await this.dataSource.updateDataAccessUsers( proposalPk, userIds @@ -73,6 +84,17 @@ export default class DataAccessUsersMutations { loggedInUserId: agent ? agent.id : null, isRejection: false, }); + + if (invitedUserIds.length > 0) { + await eventBus.publish({ + type: Event.DATA_ACCESS_USER_ADDED, + proposalPKey: proposalPk, + invitedUserIds, + key: 'proposalPk', + loggedInUserId: agent ? agent.id : null, + isRejection: false, + }); + } } return result; From 3e673a5c963234f9b43249277e2ae4cd4354713f Mon Sep 17 00:00:00 2001 From: Shivam K <292110273+shivoomiess@users.noreply.github.com> Date: Mon, 6 Jul 2026 11:28:38 +0200 Subject: [PATCH 2/3] fix: remove duplicate add event and reuse DAU updated event for logging --- .../src/eventHandlers/email/essEmailHandler.ts | 8 +++++++- apps/backend/src/events/applicationEvents.ts | 8 ++------ apps/backend/src/events/event.enum.ts | 8 -------- .../src/mutations/DataAccessUsersMutations.ts | 12 +----------- 4 files changed, 10 insertions(+), 26 deletions(-) diff --git a/apps/backend/src/eventHandlers/email/essEmailHandler.ts b/apps/backend/src/eventHandlers/email/essEmailHandler.ts index e89210c984..cf571f61d2 100644 --- a/apps/backend/src/eventHandlers/email/essEmailHandler.ts +++ b/apps/backend/src/eventHandlers/email/essEmailHandler.ts @@ -350,7 +350,13 @@ export async function essEmailHandler(event: ApplicationEvent) { break; } - case Event.DATA_ACCESS_USER_ADDED: { + case Event.DATA_ACCESS_USERS_UPDATED: { + // Only newly added users are notified; a pure removal or no-op update + // carries an empty list and sends nothing. + if (event.invitedUserIds.length === 0) { + return; + } + const proposal = await proposalDataSource.get(event.proposalPKey); if (!proposal) { logger.logError('No proposal found when trying to send email', { diff --git a/apps/backend/src/events/applicationEvents.ts b/apps/backend/src/events/applicationEvents.ts index dc58146726..a182f720af 100644 --- a/apps/backend/src/events/applicationEvents.ts +++ b/apps/backend/src/events/applicationEvents.ts @@ -415,11 +415,8 @@ interface VisitRegistrationCancelledEvent extends GeneralEvent { interface UserDataAccessUpdatedEvent extends GeneralEvent { type: Event.DATA_ACCESS_USERS_UPDATED; proposalPKey: number; -} - -interface DataAccessUserAddedEvent extends GeneralEvent { - type: Event.DATA_ACCESS_USER_ADDED; - proposalPKey: number; + // Users newly added as data access users in this update (empty on removals + // or no-op updates). Used to notify only the freshly invited users by email. invitedUserIds: number[]; } @@ -561,7 +558,6 @@ export type ApplicationEvent = | VisitRegistrationApprovedEvent | VisitRegistrationCancelledEvent | UserDataAccessUpdatedEvent - | DataAccessUserAddedEvent | ExperimentESFApprovedByIsEvent | ExperimentESFApprovedByESREvent | ExperimentESFRejectedByIsEvent diff --git a/apps/backend/src/events/event.enum.ts b/apps/backend/src/events/event.enum.ts index fa9bf0f1b6..04013d83e4 100644 --- a/apps/backend/src/events/event.enum.ts +++ b/apps/backend/src/events/event.enum.ts @@ -125,7 +125,6 @@ export enum Event { EXPERIMENT_ESF_APPROVED_BY_ESR = 'EXPERIMENT_ESF_APPROVED_BY_ESR', EXPERIMENT_ESF_REJECTED_BY_ESR = 'EXPERIMENT_ESF_REJECTED_BY_ESR', DATA_ACCESS_USERS_UPDATED = 'DATA_ACCESS_USERS_UPDATED', - DATA_ACCESS_USER_ADDED = 'DATA_ACCESS_USER_ADDED', EXPERIMENT_SAFETY_MANAGEMENT_DECISION_SUBMITTED_BY_IS = 'EXPERIMENT_SAFETY_MANAGEMENT_DECISION_SUBMITTED_BY_IS', EXPERIMENT_SAFETY_MANAGEMENT_DECISION_SUBMITTED_BY_ESR = 'EXPERIMENT_SAFETY_MANAGEMENT_DECISION_SUBMITTED_BY_ESR', EXPERIMENT_SAFETY_STATUS_CHANGED_BY_USER = 'EXPERIMENT_SAFETY_STATUS_CHANGED_BY_USER', @@ -653,13 +652,6 @@ export const EventMetadataByEvent = new Map([ Event.DATA_ACCESS_USERS_UPDATED, { label: 'Event occurs when data access users are updated' }, ], - [ - Event.DATA_ACCESS_USER_ADDED, - { - label: - 'Event occurs when an existing user is added as a data access user', - }, - ], [ Event.EXPERIMENT_SAFETY_MANAGEMENT_DECISION_SUBMITTED_BY_IS, { diff --git a/apps/backend/src/mutations/DataAccessUsersMutations.ts b/apps/backend/src/mutations/DataAccessUsersMutations.ts index 9685752475..6ea05cf5ab 100644 --- a/apps/backend/src/mutations/DataAccessUsersMutations.ts +++ b/apps/backend/src/mutations/DataAccessUsersMutations.ts @@ -80,21 +80,11 @@ export default class DataAccessUsersMutations { await eventBus.publish({ type: Event.DATA_ACCESS_USERS_UPDATED, proposalPKey: proposalPk, + invitedUserIds, key: 'proposalPk', loggedInUserId: agent ? agent.id : null, isRejection: false, }); - - if (invitedUserIds.length > 0) { - await eventBus.publish({ - type: Event.DATA_ACCESS_USER_ADDED, - proposalPKey: proposalPk, - invitedUserIds, - key: 'proposalPk', - loggedInUserId: agent ? agent.id : null, - isRejection: false, - }); - } } return result; From 07525996cc32071beda958ffc7cb9334d5a574bc Mon Sep 17 00:00:00 2001 From: Shivam K <292110273+shivoomiess@users.noreply.github.com> Date: Tue, 7 Jul 2026 09:25:34 +0200 Subject: [PATCH 3/3] refactor: implement variable rename suggestion to newlyAddedUserIds --- apps/backend/src/eventHandlers/email/essEmailHandler.ts | 4 ++-- apps/backend/src/events/applicationEvents.ts | 2 +- apps/backend/src/mutations/DataAccessUsersMutations.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/backend/src/eventHandlers/email/essEmailHandler.ts b/apps/backend/src/eventHandlers/email/essEmailHandler.ts index cf571f61d2..003f796d4c 100644 --- a/apps/backend/src/eventHandlers/email/essEmailHandler.ts +++ b/apps/backend/src/eventHandlers/email/essEmailHandler.ts @@ -353,7 +353,7 @@ export async function essEmailHandler(event: ApplicationEvent) { case Event.DATA_ACCESS_USERS_UPDATED: { // Only newly added users are notified; a pure removal or no-op update // carries an empty list and sends nothing. - if (event.invitedUserIds.length === 0) { + if (event.newlyAddedUserIds.length === 0) { return; } @@ -368,7 +368,7 @@ export async function essEmailHandler(event: ApplicationEvent) { } const invitedUsers = await userDataSource.getBasicUsersInfo( - event.invitedUserIds + event.newlyAddedUserIds ); for (const user of invitedUsers) { diff --git a/apps/backend/src/events/applicationEvents.ts b/apps/backend/src/events/applicationEvents.ts index a182f720af..7bb77acb80 100644 --- a/apps/backend/src/events/applicationEvents.ts +++ b/apps/backend/src/events/applicationEvents.ts @@ -417,7 +417,7 @@ interface UserDataAccessUpdatedEvent extends GeneralEvent { proposalPKey: number; // Users newly added as data access users in this update (empty on removals // or no-op updates). Used to notify only the freshly invited users by email. - invitedUserIds: number[]; + newlyAddedUserIds: number[]; } interface ExperimentSafetyManagementDecisionSubmittedByISEvent diff --git a/apps/backend/src/mutations/DataAccessUsersMutations.ts b/apps/backend/src/mutations/DataAccessUsersMutations.ts index 6ea05cf5ab..1557ec06be 100644 --- a/apps/backend/src/mutations/DataAccessUsersMutations.ts +++ b/apps/backend/src/mutations/DataAccessUsersMutations.ts @@ -65,7 +65,7 @@ export default class DataAccessUsersMutations { const existingUserIds = new Set( existingDataAccessUsers.map((user) => user.id) ); - const invitedUserIds = userIds.filter( + const newlyAddedUserIds = userIds.filter( (userId) => !existingUserIds.has(userId) ); @@ -80,7 +80,7 @@ export default class DataAccessUsersMutations { await eventBus.publish({ type: Event.DATA_ACCESS_USERS_UPDATED, proposalPKey: proposalPk, - invitedUserIds, + newlyAddedUserIds: newlyAddedUserIds, key: 'proposalPk', loggedInUserId: agent ? agent.id : null, isRejection: false,