From f00cbea7f3a28ae06270734619c589d2281784c7 Mon Sep 17 00:00:00 2001 From: Mike Allison Date: Wed, 18 Mar 2026 01:48:10 +0000 Subject: [PATCH 1/4] refactor: replace Apollo Client with Prisma for user data retrieval in email service --- .../src/workers/email/service/service.spec.ts | 241 ++++++++---------- .../src/workers/email/service/service.ts | 195 +++++++------- 2 files changed, 207 insertions(+), 229 deletions(-) diff --git a/apis/api-journeys-modern/src/workers/email/service/service.spec.ts b/apis/api-journeys-modern/src/workers/email/service/service.spec.ts index 9a35647e1e9..5a6b19a97ac 100644 --- a/apis/api-journeys-modern/src/workers/email/service/service.spec.ts +++ b/apis/api-journeys-modern/src/workers/email/service/service.spec.ts @@ -1,4 +1,3 @@ -import { ApolloClient, ApolloQueryResult } from '@apollo/client' import { Job } from 'bullmq' import { @@ -21,7 +20,16 @@ import { } from './prisma.types' import { service } from './service' -jest.mock('@apollo/client') +jest.mock('@core/prisma/users/client', () => ({ + prisma: { + user: { + findUnique: jest.fn(), + findMany: jest.fn() + } + } +})) + +const { prisma: mockPrismaUsers } = jest.requireMock('@core/prisma/users/client') let args = {} jest.mock('@core/yoga/email', () => ({ @@ -182,17 +190,12 @@ describe('EmailConsumer', () => { describe('teamRemovedEmail', () => { it('should send an email', async () => { - jest.spyOn(ApolloClient.prototype, 'query').mockImplementationOnce( - async () => - await Promise.resolve({ - data: { - user: { - id: 'userid', - email: 'jsmith@exmaple.com' - } - } - } as unknown as ApolloQueryResult) - ) + mockPrismaUsers.user.findUnique.mockResolvedValueOnce({ + id: 'userid', + email: 'jsmith@exmaple.com', + firstName: 'John', + imageUrl: null + }) await service(teamRemoved) expect(sendEmail).toHaveBeenCalled() expect(args).toEqual({ @@ -210,17 +213,12 @@ describe('EmailConsumer', () => { accountNotifications: false }) - jest.spyOn(ApolloClient.prototype, 'query').mockImplementationOnce( - async () => - await Promise.resolve({ - data: { - user: { - id: 'userid', - email: 'jsmith@exmaple.com' - } - } - } as unknown as ApolloQueryResult) - ) + mockPrismaUsers.user.findUnique.mockResolvedValueOnce({ + id: 'userid', + email: 'jsmith@exmaple.com', + firstName: 'John', + imageUrl: null + }) await service(teamRemoved) expect(sendEmail).not.toHaveBeenCalled() }) @@ -228,17 +226,12 @@ describe('EmailConsumer', () => { describe('teamInviteEmail', () => { it('should send an email if user exists', async () => { - jest.spyOn(ApolloClient.prototype, 'query').mockImplementationOnce( - async () => - await Promise.resolve({ - data: { - user: { - id: 'userid', - email: 'jsmith@exmaple.com' - } - } - } as unknown as ApolloQueryResult) - ) + mockPrismaUsers.user.findUnique.mockResolvedValueOnce({ + id: 'userid', + email: 'jsmith@exmaple.com', + firstName: 'John', + imageUrl: null + }) await service(teamInviteJob) expect(sendEmail).toHaveBeenCalled() expect(args).toEqual({ @@ -250,14 +243,7 @@ describe('EmailConsumer', () => { }) it('should send an email if user does not exist', async () => { - jest.spyOn(ApolloClient.prototype, 'query').mockImplementationOnce( - async () => - await Promise.resolve({ - data: { - user: undefined - } - } as unknown as ApolloQueryResult) - ) + mockPrismaUsers.user.findUnique.mockResolvedValueOnce(null) await service(teamInviteJob) expect(sendEmail).toHaveBeenCalled() expect(args).toEqual({ @@ -282,18 +268,24 @@ describe('EmailConsumer', () => { describe('teamInviteAcceptedEmail', () => { it('should send an email', async () => { - jest.spyOn(ApolloClient.prototype, 'query').mockImplementation( - async () => - await Promise.resolve({ - data: { - user: { - id: 'userid', - email: 'jsmith@exmaple.com' - } - } - } as unknown as ApolloQueryResult) - ) + mockPrismaUsers.user.findMany.mockResolvedValue([ + { + id: 'userid', + userId: 'userId', + email: 'jsmith@exmaple.com', + firstName: 'John', + imageUrl: null + }, + { + id: 'userid2', + userId: 'userId2', + email: 'jsmith@exmaple.com', + firstName: 'Jane', + imageUrl: null + } + ]) await service(teamInviteAccepted) + expect(mockPrismaUsers.user.findMany).toHaveBeenCalledTimes(1) expect(sendEmail).toHaveBeenCalledTimes(2) expect(args).toEqual({ to: 'jsmith@exmaple.com', @@ -310,35 +302,36 @@ describe('EmailConsumer', () => { accountNotifications: false }) - jest.spyOn(ApolloClient.prototype, 'query').mockImplementationOnce( - async () => - await Promise.resolve({ - data: { - user: { - id: 'userid', - email: 'jsmith@exmaple.com' - } - } - } as unknown as ApolloQueryResult) - ) + mockPrismaUsers.user.findMany.mockResolvedValueOnce([ + { + id: 'userid', + userId: 'userId', + email: 'jsmith@exmaple.com', + firstName: 'John', + imageUrl: null + }, + { + id: 'userid2', + userId: 'userId2', + email: 'jsmith@exmaple.com', + firstName: 'Jane', + imageUrl: null + } + ]) await service(teamInviteAccepted) + expect(mockPrismaUsers.user.findMany).toHaveBeenCalledTimes(1) expect(sendEmail).not.toHaveBeenCalled() }) }) describe('journeyAccessRequest', () => { it('should send an email', async () => { - jest.spyOn(ApolloClient.prototype, 'query').mockImplementationOnce( - async () => - await Promise.resolve({ - data: { - user: { - id: 'userid', - email: 'jsmith@exmaple.com' - } - } - } as unknown as ApolloQueryResult) - ) + mockPrismaUsers.user.findUnique.mockResolvedValueOnce({ + id: 'userid', + email: 'jsmith@exmaple.com', + firstName: 'John', + imageUrl: null + }) await service(journeyAccessRequest) expect(sendEmail).toHaveBeenCalled() expect(args).toEqual({ @@ -356,17 +349,12 @@ describe('EmailConsumer', () => { accountNotifications: false }) - jest.spyOn(ApolloClient.prototype, 'query').mockImplementationOnce( - async () => - await Promise.resolve({ - data: { - user: { - id: 'userid', - email: 'jsmith@exmaple.com' - } - } - } as unknown as ApolloQueryResult) - ) + mockPrismaUsers.user.findUnique.mockResolvedValueOnce({ + id: 'userid', + email: 'jsmith@exmaple.com', + firstName: 'John', + imageUrl: null + }) await service(journeyAccessRequest) expect(sendEmail).not.toHaveBeenCalled() }) @@ -374,17 +362,12 @@ describe('EmailConsumer', () => { describe('journeyRequestApproved', () => { it('should send an email', async () => { - jest.spyOn(ApolloClient.prototype, 'query').mockImplementationOnce( - async () => - await Promise.resolve({ - data: { - user: { - id: 'userid', - email: 'jsmith@exmaple.com' - } - } - } as unknown as ApolloQueryResult) - ) + mockPrismaUsers.user.findUnique.mockResolvedValueOnce({ + id: 'userid', + email: 'jsmith@exmaple.com', + firstName: 'John', + imageUrl: null + }) await service(journeyRequestApproved) expect(sendEmail).toHaveBeenCalled() expect(args).toEqual({ @@ -402,17 +385,12 @@ describe('EmailConsumer', () => { accountNotifications: false }) - jest.spyOn(ApolloClient.prototype, 'query').mockImplementationOnce( - async () => - await Promise.resolve({ - data: { - user: { - id: 'userid', - email: 'jsmith@exmaple.com' - } - } - } as unknown as ApolloQueryResult) - ) + mockPrismaUsers.user.findUnique.mockResolvedValueOnce({ + id: 'userid', + email: 'jsmith@exmaple.com', + firstName: 'John', + imageUrl: null + }) await service(journeyRequestApproved) expect(sendEmail).not.toHaveBeenCalled() }) @@ -420,36 +398,24 @@ describe('EmailConsumer', () => { describe('journeyEditInvite', () => { it('should send an email if user exists', async () => { - jest.spyOn(ApolloClient.prototype, 'query').mockImplementationOnce( - async () => - await Promise.resolve({ - data: { - user: { - id: 'userid', - email: 'jsmith@exmaple.com' - } - } - } as unknown as ApolloQueryResult) - ) + mockPrismaUsers.user.findUnique.mockResolvedValueOnce({ + id: 'userid', + email: 'jsmith@exmaple.com', + firstName: 'John', + imageUrl: null + }) await service(journeyEditJob) expect(sendEmail).toHaveBeenCalled() expect(args).toEqual({ to: journeyEditJob.data.email, - subject: 'Journey Title has been shared with you', + subject: 'Journey Title has been shared with you on NextSteps', html: expect.any(String), text: expect.any(String) }) }) it('should send an email if user does not exist', async () => { - jest.spyOn(ApolloClient.prototype, 'query').mockImplementationOnce( - async () => - await Promise.resolve({ - data: { - user: undefined - } - } as unknown as ApolloQueryResult) - ) + mockPrismaUsers.user.findUnique.mockResolvedValueOnce(null) await service(journeyEditJob) expect(sendEmail).toHaveBeenCalled() expect(args).toEqual({ @@ -467,17 +433,12 @@ describe('EmailConsumer', () => { accountNotifications: false }) - jest.spyOn(ApolloClient.prototype, 'query').mockImplementationOnce( - async () => - await Promise.resolve({ - data: { - user: { - id: 'userid', - email: 'jsmith@exmaple.com' - } - } - } as unknown as ApolloQueryResult) - ) + mockPrismaUsers.user.findUnique.mockResolvedValueOnce({ + id: 'userid', + email: 'jsmith@exmaple.com', + firstName: 'John', + imageUrl: null + }) await service(journeyEditJob) expect(sendEmail).not.toHaveBeenCalled() }) diff --git a/apis/api-journeys-modern/src/workers/email/service/service.ts b/apis/api-journeys-modern/src/workers/email/service/service.ts index 5d1d36e603f..9c920fb1088 100644 --- a/apis/api-journeys-modern/src/workers/email/service/service.ts +++ b/apis/api-journeys-modern/src/workers/email/service/service.ts @@ -1,4 +1,3 @@ -import { ApolloClient, InMemoryCache, createHttpLink } from '@apollo/client' import { render } from '@react-email/render' import { Job } from 'bullmq' @@ -7,7 +6,7 @@ import { UserTeamRole, prisma } from '@core/prisma/journeys/client' -import { graphql } from '@core/shared/gql' +import { prisma as prismaUsers } from '@core/prisma/users/client' import { sendEmail } from '@core/yoga/email' import { JourneyAccessRequestEmail } from '../../../emails/templates/JourneyAccessRequest' @@ -29,42 +28,6 @@ import { TeamRemoved } from './prisma.types' -const httpLink = createHttpLink({ - uri: env.GATEWAY_URL, - headers: { - 'interop-token': env.INTEROP_TOKEN, - 'x-graphql-client-name': 'api-journeys-modern', - 'x-graphql-client-version': env.SERVICE_VERSION - } -}) - -const apollo = new ApolloClient({ - link: httpLink, - cache: new InMemoryCache() -}) - -const GET_USER = graphql(` - query GetUser($userId: ID!) { - user(id: $userId) { - id - email - firstName - imageUrl - } - } -`) - -const GET_USER_BY_EMAIL = graphql(` - query GetUserByEmail($email: String!) { - userByEmail(email: $email) { - id - email - firstName - imageUrl - } - } -`) - export async function service(job: Job): Promise { switch (job.name) { case 'team-invite': @@ -89,17 +52,25 @@ export async function service(job: Job): Promise { } export async function teamRemovedEmail(job: Job): Promise { - const { data } = await apollo.query({ - query: GET_USER, - variables: { userId: job.data.userId } + const recipientUser = await prismaUsers.user.findUnique({ + where: { userId: job.data.userId } }) - if (data.user == null) throw new Error('User not found') + const user = + recipientUser == null + ? null + : { + ...recipientUser, + email: recipientUser.email ?? '', + lastName: recipientUser.lastName ?? undefined + } + + if (user == null) throw new Error('User not found') // check recipient preferences const preferences = await prisma.journeysEmailPreference.findFirst({ where: { - email: data.user.email + email: user.email } }) // do not send email if team removed notification is not preferred @@ -112,14 +83,14 @@ export async function teamRemovedEmail(job: Job): Promise { const html = await render( TeamRemovedEmail({ teamName: job.data.teamName, - recipient: data.user + recipient: user }) ) const text = await render( TeamRemovedEmail({ teamName: job.data.teamName, - recipient: data.user + recipient: user }), { plainText: true @@ -127,7 +98,7 @@ export async function teamRemovedEmail(job: Job): Promise { ) await sendEmail({ - to: data.user.email, + to: user.email, subject: `You have been removed from team: ${job.data.teamName}`, text, html @@ -149,12 +120,20 @@ export async function teamInviteEmail(job: Job): Promise { ) return - const { data } = await apollo.query({ - query: GET_USER_BY_EMAIL, - variables: { email: job.data.email } + const recipientUser = await prismaUsers.user.findUnique({ + where: { email: job.data.email } }) - if (data.userByEmail == null) { + const user = + recipientUser == null + ? null + : { + ...recipientUser, + email: recipientUser.email ?? '', + lastName: recipientUser.lastName ?? undefined + } + + if (user == null) { const html = await render( TeamInviteNoAccountEmail({ teamName: job.data.team.title, @@ -184,7 +163,7 @@ export async function teamInviteEmail(job: Job): Promise { const html = await render( TeamInviteEmail({ teamName: job.data.team.title, - recipient: data.userByEmail, + recipient: user, inviteLink: url, sender: job.data.sender }) @@ -193,7 +172,7 @@ export async function teamInviteEmail(job: Job): Promise { const text = await render( TeamInviteEmail({ teamName: job.data.team.title, - recipient: data.userByEmail, + recipient: user, inviteLink: url, sender: job.data.sender }), @@ -218,28 +197,42 @@ export async function teamInviteAcceptedEmail( const recipientUserTeams = job.data.team.userTeams.filter( (userTeam) => userTeam.role === UserTeamRole.manager ) + const recipientUserIds = recipientUserTeams.map((userTeam) => userTeam.userId) - const recipientEmails = await Promise.all( - recipientUserTeams.map(async (userTeam) => { - const { data } = await apollo.query({ - query: GET_USER, - variables: { userId: userTeam.userId } - }) - return data - }) + const recipientUsers = await prismaUsers.user.findMany({ + where: { + userId: { + in: recipientUserIds + } + } + }) + + const recipientUsersByUserId = new Map( + recipientUsers.map((user) => [ + user.userId, + { + ...user, + email: user.email ?? '', + lastName: user.lastName ?? undefined + } + ]) ) - if (recipientEmails == null || recipientEmails.length === 0) { + const recipients = recipientUserIds.map((userId) => + recipientUsersByUserId.get(userId) + ) + + if (recipients.length === 0) { throw new Error('Team Managers not found') } - for (const recipient of recipientEmails) { - if (recipient.user == null) throw new Error('User not found') + for (const recipient of recipients) { + if (recipient == null) throw new Error('User not found') // check recipient preferences const preferences = await prisma.journeysEmailPreference.findFirst({ where: { - email: recipient.user.email + email: recipient.email } }) // do not send email if team removed notification is not preferred @@ -254,7 +247,7 @@ export async function teamInviteAcceptedEmail( teamName: job.data.team.title, inviteLink: url, sender: job.data.sender, - recipient: recipient.user + recipient }) ) @@ -263,7 +256,7 @@ export async function teamInviteAcceptedEmail( teamName: job.data.team.title, inviteLink: url, sender: job.data.sender, - recipient: recipient.user + recipient }), { plainText: true @@ -271,7 +264,7 @@ export async function teamInviteAcceptedEmail( ) await sendEmail({ - to: recipient.user.email, + to: recipient.email, subject: `${ job.data.sender.firstName ?? 'A new member' } has been added to your team`, @@ -290,17 +283,25 @@ export async function journeyAccessRequest( if (recipientUserId == null) throw new Error('User not found') - const { data } = await apollo.query({ - query: GET_USER, - variables: { userId: recipientUserId } + const recipientUser = await prismaUsers.user.findUnique({ + where: { userId: recipientUserId } }) - if (data.user == null) throw new Error('User not found') + const user = + recipientUser == null + ? null + : { + ...recipientUser, + email: recipientUser.email ?? '', + lastName: recipientUser.lastName ?? undefined + } + + if (user == null) throw new Error('User not found') // check recipient preferences const preferences = await prisma.journeysEmailPreference.findFirst({ where: { - email: data.user.email + email: user.email } }) // do not send email if team removed notification is not preferred @@ -314,7 +315,7 @@ export async function journeyAccessRequest( JourneyAccessRequestEmail({ journey: job.data.journey, inviteLink: job.data.url, - recipient: data.user, + recipient: user, sender: job.data.sender }) ) @@ -322,7 +323,7 @@ export async function journeyAccessRequest( JourneyAccessRequestEmail({ journey: job.data.journey, inviteLink: job.data.url, - recipient: data.user, + recipient: user, sender: job.data.sender }), { @@ -331,7 +332,7 @@ export async function journeyAccessRequest( ) await sendEmail({ - to: data.user.email, + to: user.email, subject: `${job.data.sender.firstName} requests access to a journey`, html, text @@ -341,17 +342,25 @@ export async function journeyAccessRequest( export async function journeyRequestApproved( job: Job ): Promise { - const { data } = await apollo.query({ - query: GET_USER, - variables: { userId: job.data.userId } + const recipientUser = await prismaUsers.user.findUnique({ + where: { userId: job.data.userId } }) - if (data.user == null) throw new Error('User not found') + const user = + recipientUser == null + ? null + : { + ...recipientUser, + email: recipientUser.email ?? '', + lastName: recipientUser.lastName ?? undefined + } + + if (user == null) throw new Error('User not found') // check recipient preferences const preferences = await prisma.journeysEmailPreference.findFirst({ where: { - email: data.user.email + email: user.email } }) // do not send email if team removed notification is not preferred @@ -366,7 +375,7 @@ export async function journeyRequestApproved( journey: job.data.journey, inviteLink: job.data.url, sender: job.data.sender, - recipient: data.user + recipient: user }) ) @@ -375,14 +384,14 @@ export async function journeyRequestApproved( journey: job.data.journey, inviteLink: job.data.url, sender: job.data.sender, - recipient: data.user + recipient: user }), { plainText: true } ) await sendEmail({ - to: data.user.email, + to: user.email, subject: `${job.data.journey.title} has been shared with you`, html, text @@ -405,12 +414,20 @@ export async function journeyEditInvite( ) return - const { data } = await apollo.query({ - query: GET_USER_BY_EMAIL, - variables: { email: job.data.email } + const recipientUser = await prismaUsers.user.findUnique({ + where: { email: job.data.email } }) - if (data.userByEmail == null) { + const user = + recipientUser == null + ? null + : { + ...recipientUser, + email: recipientUser.email ?? '', + lastName: recipientUser.lastName ?? undefined + } + + if (user == null) { const url = `${env.JOURNEYS_ADMIN_URL}/` const html = await render( JourneySharedNoAccountEmail({ @@ -443,7 +460,7 @@ export async function journeyEditInvite( sender: job.data.sender, journey: job.data.journey, inviteLink: job.data.url, - recipient: data.userByEmail + recipient: user }) ) const text = await render( @@ -451,7 +468,7 @@ export async function journeyEditInvite( journey: job.data.journey, inviteLink: job.data.url, sender: job.data.sender, - recipient: data.userByEmail + recipient: user }), { plainText: true From c39240af292a36c9d8f061d6ec6f09657bd33dca Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 01:52:00 +0000 Subject: [PATCH 2/4] fix: lint issues --- .../src/workers/email/service/service.spec.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apis/api-journeys-modern/src/workers/email/service/service.spec.ts b/apis/api-journeys-modern/src/workers/email/service/service.spec.ts index 5a6b19a97ac..0edf71b6573 100644 --- a/apis/api-journeys-modern/src/workers/email/service/service.spec.ts +++ b/apis/api-journeys-modern/src/workers/email/service/service.spec.ts @@ -29,7 +29,9 @@ jest.mock('@core/prisma/users/client', () => ({ } })) -const { prisma: mockPrismaUsers } = jest.requireMock('@core/prisma/users/client') +const { prisma: mockPrismaUsers } = jest.requireMock( + '@core/prisma/users/client' +) let args = {} jest.mock('@core/yoga/email', () => ({ From 3adb11e1d07eed3f601a59ad35baee8f714170cc Mon Sep 17 00:00:00 2001 From: Mike Allison Date: Tue, 31 Mar 2026 01:10:06 +0000 Subject: [PATCH 3/4] refactor: improve email recipient handling in service functions - Introduced EmailRecipient interface for better type safety. - Added toEmailRecipient function to standardize user data transformation. - Updated service functions to utilize the new recipient handling logic, ensuring email addresses are validated before sending. - Enhanced error handling for cases where users lack email addresses. --- .../src/workers/email/service/service.ts | 152 +++++++++--------- 1 file changed, 72 insertions(+), 80 deletions(-) diff --git a/apis/api-journeys-modern/src/workers/email/service/service.ts b/apis/api-journeys-modern/src/workers/email/service/service.ts index 9c920fb1088..4ad1e041d51 100644 --- a/apis/api-journeys-modern/src/workers/email/service/service.ts +++ b/apis/api-journeys-modern/src/workers/email/service/service.ts @@ -28,6 +28,28 @@ import { TeamRemoved } from './prisma.types' +export interface EmailRecipient { + firstName: string + lastName?: string + email: string + imageUrl?: string | null +} + +function toEmailRecipient(user: { + firstName: string + lastName?: string | null + email?: string | null + imageUrl?: string | null +}): EmailRecipient | null { + if (user.email == null) return null + return { + firstName: user.firstName, + lastName: user.lastName ?? undefined, + email: user.email, + imageUrl: user.imageUrl + } +} + export async function service(job: Job): Promise { switch (job.name) { case 'team-invite': @@ -56,21 +78,14 @@ export async function teamRemovedEmail(job: Job): Promise { where: { userId: job.data.userId } }) - const user = - recipientUser == null - ? null - : { - ...recipientUser, - email: recipientUser.email ?? '', - lastName: recipientUser.lastName ?? undefined - } - - if (user == null) throw new Error('User not found') + if (recipientUser == null) throw new Error('User not found') + const recipient = toEmailRecipient(recipientUser) + if (recipient == null) throw new Error('User has no email') // check recipient preferences const preferences = await prisma.journeysEmailPreference.findFirst({ where: { - email: user.email + email: recipient.email } }) // do not send email if team removed notification is not preferred @@ -83,14 +98,14 @@ export async function teamRemovedEmail(job: Job): Promise { const html = await render( TeamRemovedEmail({ teamName: job.data.teamName, - recipient: user + recipient }) ) const text = await render( TeamRemovedEmail({ teamName: job.data.teamName, - recipient: user + recipient }), { plainText: true @@ -98,7 +113,7 @@ export async function teamRemovedEmail(job: Job): Promise { ) await sendEmail({ - to: user.email, + to: recipient.email, subject: `You have been removed from team: ${job.data.teamName}`, text, html @@ -124,16 +139,7 @@ export async function teamInviteEmail(job: Job): Promise { where: { email: job.data.email } }) - const user = - recipientUser == null - ? null - : { - ...recipientUser, - email: recipientUser.email ?? '', - lastName: recipientUser.lastName ?? undefined - } - - if (user == null) { + if (recipientUser == null) { const html = await render( TeamInviteNoAccountEmail({ teamName: job.data.team.title, @@ -160,10 +166,13 @@ export async function teamInviteEmail(job: Job): Promise { html }) } else { + const recipient = toEmailRecipient(recipientUser) + if (recipient == null) throw new Error('User has no email') + const html = await render( TeamInviteEmail({ teamName: job.data.team.title, - recipient: user, + recipient, inviteLink: url, sender: job.data.sender }) @@ -172,7 +181,7 @@ export async function teamInviteEmail(job: Job): Promise { const text = await render( TeamInviteEmail({ teamName: job.data.team.title, - recipient: user, + recipient, inviteLink: url, sender: job.data.sender }), @@ -208,26 +217,29 @@ export async function teamInviteAcceptedEmail( }) const recipientUsersByUserId = new Map( - recipientUsers.map((user) => [ - user.userId, - { - ...user, - email: user.email ?? '', - lastName: user.lastName ?? undefined - } - ]) - ) - - const recipients = recipientUserIds.map((userId) => - recipientUsersByUserId.get(userId) + recipientUsers.map((user) => { + const recipient = toEmailRecipient(user) + if (recipient == null) throw new Error('User has no email') + return [user.userId, recipient] as const + }) ) - if (recipients.length === 0) { + if (recipientUserIds.length === 0) { throw new Error('Team Managers not found') } + const missingIds = recipientUserIds.filter( + (id) => !recipientUsersByUserId.has(id) + ) + if (missingIds.length > 0) { + throw new Error(`Team Managers not found for userIds: ${missingIds.join(', ')}`) + } + + const recipients = recipientUserIds.map( + (userId) => recipientUsersByUserId.get(userId)! + ) + for (const recipient of recipients) { - if (recipient == null) throw new Error('User not found') // check recipient preferences const preferences = await prisma.journeysEmailPreference.findFirst({ @@ -287,21 +299,14 @@ export async function journeyAccessRequest( where: { userId: recipientUserId } }) - const user = - recipientUser == null - ? null - : { - ...recipientUser, - email: recipientUser.email ?? '', - lastName: recipientUser.lastName ?? undefined - } - - if (user == null) throw new Error('User not found') + if (recipientUser == null) throw new Error('User not found') + const recipient = toEmailRecipient(recipientUser) + if (recipient == null) throw new Error('User has no email') // check recipient preferences const preferences = await prisma.journeysEmailPreference.findFirst({ where: { - email: user.email + email: recipient.email } }) // do not send email if team removed notification is not preferred @@ -315,7 +320,7 @@ export async function journeyAccessRequest( JourneyAccessRequestEmail({ journey: job.data.journey, inviteLink: job.data.url, - recipient: user, + recipient, sender: job.data.sender }) ) @@ -323,7 +328,7 @@ export async function journeyAccessRequest( JourneyAccessRequestEmail({ journey: job.data.journey, inviteLink: job.data.url, - recipient: user, + recipient, sender: job.data.sender }), { @@ -332,7 +337,7 @@ export async function journeyAccessRequest( ) await sendEmail({ - to: user.email, + to: recipient.email, subject: `${job.data.sender.firstName} requests access to a journey`, html, text @@ -346,21 +351,14 @@ export async function journeyRequestApproved( where: { userId: job.data.userId } }) - const user = - recipientUser == null - ? null - : { - ...recipientUser, - email: recipientUser.email ?? '', - lastName: recipientUser.lastName ?? undefined - } - - if (user == null) throw new Error('User not found') + if (recipientUser == null) throw new Error('User not found') + const recipient = toEmailRecipient(recipientUser) + if (recipient == null) throw new Error('User has no email') // check recipient preferences const preferences = await prisma.journeysEmailPreference.findFirst({ where: { - email: user.email + email: recipient.email } }) // do not send email if team removed notification is not preferred @@ -375,7 +373,7 @@ export async function journeyRequestApproved( journey: job.data.journey, inviteLink: job.data.url, sender: job.data.sender, - recipient: user + recipient }) ) @@ -384,14 +382,14 @@ export async function journeyRequestApproved( journey: job.data.journey, inviteLink: job.data.url, sender: job.data.sender, - recipient: user + recipient }), { plainText: true } ) await sendEmail({ - to: user.email, + to: recipient.email, subject: `${job.data.journey.title} has been shared with you`, html, text @@ -418,16 +416,7 @@ export async function journeyEditInvite( where: { email: job.data.email } }) - const user = - recipientUser == null - ? null - : { - ...recipientUser, - email: recipientUser.email ?? '', - lastName: recipientUser.lastName ?? undefined - } - - if (user == null) { + if (recipientUser == null) { const url = `${env.JOURNEYS_ADMIN_URL}/` const html = await render( JourneySharedNoAccountEmail({ @@ -455,12 +444,15 @@ export async function journeyEditInvite( text }) } else { + const recipient = toEmailRecipient(recipientUser) + if (recipient == null) throw new Error('User has no email') + const html = await render( JourneySharedEmail({ sender: job.data.sender, journey: job.data.journey, inviteLink: job.data.url, - recipient: user + recipient }) ) const text = await render( @@ -468,7 +460,7 @@ export async function journeyEditInvite( journey: job.data.journey, inviteLink: job.data.url, sender: job.data.sender, - recipient: user + recipient }), { plainText: true From 61528023432efe031052a9b5228a1c077a2a5125 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 31 Mar 2026 01:14:11 +0000 Subject: [PATCH 4/4] fix: lint issues --- .../api-journeys-modern/src/workers/email/service/service.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apis/api-journeys-modern/src/workers/email/service/service.ts b/apis/api-journeys-modern/src/workers/email/service/service.ts index 4ad1e041d51..0e447b8bf33 100644 --- a/apis/api-journeys-modern/src/workers/email/service/service.ts +++ b/apis/api-journeys-modern/src/workers/email/service/service.ts @@ -232,7 +232,9 @@ export async function teamInviteAcceptedEmail( (id) => !recipientUsersByUserId.has(id) ) if (missingIds.length > 0) { - throw new Error(`Team Managers not found for userIds: ${missingIds.join(', ')}`) + throw new Error( + `Team Managers not found for userIds: ${missingIds.join(', ')}` + ) } const recipients = recipientUserIds.map( @@ -240,7 +242,6 @@ export async function teamInviteAcceptedEmail( ) for (const recipient of recipients) { - // check recipient preferences const preferences = await prisma.journeysEmailPreference.findFirst({ where: {