Skip to content
1 change: 1 addition & 0 deletions apps/backend/src/eventHandlers/email/emailTemplateId.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
}
55 changes: 55 additions & 0 deletions apps/backend/src/eventHandlers/email/essEmailHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,61 @@ export async function essEmailHandler(event: ApplicationEvent) {
break;
}

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.newlyAddedUserIds.length === 0) {
return;
}

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.newlyAddedUserIds
);

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);
Expand Down
3 changes: 3 additions & 0 deletions apps/backend/src/events/applicationEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,9 @@ interface VisitRegistrationCancelledEvent extends GeneralEvent {
interface UserDataAccessUpdatedEvent extends GeneralEvent {
type: Event.DATA_ACCESS_USERS_UPDATED;
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.
newlyAddedUserIds: number[];
}

interface ExperimentSafetyManagementDecisionSubmittedByISEvent
Expand Down
12 changes: 12 additions & 0 deletions apps/backend/src/mutations/DataAccessUsersMutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 newlyAddedUserIds = userIds.filter(
(userId) => !existingUserIds.has(userId)
);

const result = await this.dataSource.updateDataAccessUsers(
proposalPk,
userIds
Expand All @@ -69,6 +80,7 @@ export default class DataAccessUsersMutations {
await eventBus.publish({
type: Event.DATA_ACCESS_USERS_UPDATED,
proposalPKey: proposalPk,
newlyAddedUserIds: newlyAddedUserIds,
key: 'proposalPk',
loggedInUserId: agent ? agent.id : null,
isRejection: false,
Expand Down
Loading