Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 50 additions & 2 deletions apps/api/src/people/people-invite.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,11 @@ describe('PeopleInviteService', () => {
});

expect(results[0].success).toBe(true);
expect(results[0].emailSent).toBe(true);
// No portal invite was requested (sendPortalEmail omitted = false), so no
// email is sent and emailSent is not surfaced (the UI only warns on an
// actual send failure, never on an intentional skip).
expect(mockTriggerEmail).not.toHaveBeenCalled();
expect(results[0].emailSent).toBeUndefined();
expect(mockDb.member.create).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
Expand Down Expand Up @@ -437,9 +441,13 @@ describe('PeopleInviteService', () => {

const results = await service.inviteMembers({
...baseParams,
invites: [{ email: 'emp@example.com', roles: ['employee'] }],
invites: [
{ email: 'emp@example.com', roles: ['employee'], sendPortalEmail: true },
],
});

// A portal email was requested but the send failed — the member is still
// added and emailSent: false signals the UI to offer a resend.
expect(results[0].success).toBe(true);
expect(results[0].emailSent).toBe(false);
});
Expand Down Expand Up @@ -545,6 +553,46 @@ describe('PeopleInviteService', () => {
expect(mockInviteEmail).not.toHaveBeenCalled();
});

// Regression: unchecking "Send portal invite email" when adding an
// employee via "+ Add User" must add the member WITHOUT emailing them.
// Previously the else-branch still sent an InviteEmail with a portal link.
it('employee only with portal UNchecked: adds member silently, sends no email', async () => {
(mockDb.organization.findUnique as jest.Mock).mockResolvedValue({
name: 'Test Org',
});
(mockDb.user.findFirst as jest.Mock).mockResolvedValue(null);
(mockDb.user.create as jest.Mock).mockResolvedValue({
id: 'usr_emp',
email: 'emp@example.com',
});
(mockDb.member.findFirst as jest.Mock).mockResolvedValue(null);
(mockDb.member.create as jest.Mock).mockResolvedValue({ id: 'mem_emp' });
(
mockDb.employeeTrainingVideoCompletion.createMany as jest.Mock
).mockResolvedValue({ count: 5 });

const results = await service.inviteMembers({
...baseParams,
invites: [
{
email: 'emp@example.com',
roles: ['employee'],
sendPortalEmail: false,
},
],
});

// Member is still created...
expect(results[0].success).toBe(true);
expect(mockDb.member.create).toHaveBeenCalled();
// ...but NO email of any kind goes out when the portal invite is off.
expect(mockTriggerEmail).not.toHaveBeenCalled();
expect(mockInvitePortalEmail).not.toHaveBeenCalled();
expect(mockInviteEmail).not.toHaveBeenCalled();
// And no false "could not be sent" warning leaks to the UI.
expect(results[0].emailSent).toBeUndefined();
});

it('admin only (no portal): sends app email without portal link', async () => {
setupNewUserInvite();

Expand Down
35 changes: 17 additions & 18 deletions apps/api/src/people/people-invite.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,10 @@ export class PeopleInviteService {
results.push({
email: invite.email,
success: true,
emailSent: result.emailSent,
// Only surface email status when we actually attempted to send, so
// the UI's "invite email could not be sent" warning never fires for
// an intentional skip (portal invite unchecked).
...(shouldSendPortalEmail ? { emailSent: result.emailSent } : {}),
});
} else {
await this.inviteWithCheck({
Expand Down Expand Up @@ -208,10 +211,12 @@ export class PeopleInviteService {
await this.createTrainingVideoEntries(member.id, organizationId);
}

// Send invite email (non-fatal)
let emailSent = true;
try {
if (sendPortalEmail) {
// Send the portal invite email only when requested (non-fatal). When the
// admin opts out ("Send portal invite email" unchecked) we add the member
// silently and send no email at all.
let emailSent = false;
if (sendPortalEmail) {
try {
const inviteLink = this.buildPortalUrl(organizationId);
await triggerEmail({
to: email,
Expand All @@ -222,20 +227,14 @@ export class PeopleInviteService {
email,
}),
});
} else {
const inviteLink = this.buildPortalUrl(organizationId);
await triggerEmail({
to: email,
subject: `You've been invited to join ${organization.name} on Comp AI`,
react: InviteEmail({ organizationName: organization.name, inviteLink }),
});
emailSent = true;
} catch (emailErr) {
emailSent = false;
this.logger.error(
`Portal invite email failed after member was added: ${email}`,
emailErr instanceof Error ? emailErr.message : 'Unknown error',
);
}
} catch (emailErr) {
emailSent = false;
this.logger.error(
`Invite email failed after member was added: ${email}`,
emailErr instanceof Error ? emailErr.message : 'Unknown error',
);
}

return { emailSent };
Expand Down
Loading