Skip to content

Commit 80fa172

Browse files
CarinaWolliCarinaWolli
andauthored
feat: add feature flag for sending workflow emails with smtp (calcom#21187)
* add feature flag * add unit tests --------- Co-authored-by: CarinaWolli <wollencarina@gmail.com>
1 parent 5017365 commit 80fa172

12 files changed

Lines changed: 165 additions & 7 deletions

File tree

packages/features/ee/round-robin/roundRobinManualReassignment.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,8 @@ async function handleWorkflowsUpdate({
423423
template: true,
424424
workflow: {
425425
select: {
426+
userId: true,
427+
teamId: true,
426428
trigger: true,
427429
time: true,
428430
timeUnit: true,
@@ -468,6 +470,8 @@ async function handleWorkflowsUpdate({
468470
includeCalendarEvent: workflowStep.includeCalendarEvent,
469471
workflowStepId: workflowStep.id,
470472
verifiedAt: workflowStep.verifiedAt,
473+
userId: workflow.userId,
474+
teamId: workflow.teamId,
471475
});
472476
}
473477

packages/features/ee/round-robin/roundRobinReassignment.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,8 @@ export const roundRobinReassignment = async ({
460460
template: true,
461461
workflow: {
462462
select: {
463+
userId: true,
464+
teamId: true,
463465
trigger: true,
464466
time: true,
465467
timeUnit: true,
@@ -506,6 +508,8 @@ export const roundRobinReassignment = async ({
506508
includeCalendarEvent: workflowStep.includeCalendarEvent,
507509
workflowStepId: workflowStep.id,
508510
verifiedAt: workflowStep.verifiedAt,
511+
userId: workflow.userId,
512+
teamId: workflow.teamId,
509513
});
510514
}
511515

packages/features/ee/workflows/api/scheduleEmailReminders.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export async function handler(req: NextRequest) {
4040
return NextResponse.json({ message: "Not authenticated" }, { status: 401 });
4141
}
4242

43-
const isSendgridEnabled = process.env.SENDGRID_API_KEY && process.env.SENDGRID_EMAIL;
43+
const isSendgridEnabled = !!(process.env.SENDGRID_API_KEY && process.env.SENDGRID_EMAIL);
4444

4545
if (isSendgridEnabled) {
4646
const remindersToDelete: { referenceId: string | null; id: number }[] = await getAllRemindersToDelete();
@@ -65,9 +65,7 @@ export async function handler(req: NextRequest) {
6565
});
6666

6767
await Promise.allSettled(handlePastCancelledReminders);
68-
}
6968

70-
if (isSendgridEnabled) {
7169
//cancel reminders for cancelled/rescheduled bookings that are scheduled within the next hour
7270
const remindersToCancel: { referenceId: string | null; id: number }[] = await getAllRemindersToCancel();
7371

packages/features/ee/workflows/lib/reminders/emailReminderManager.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { v4 as uuidv4 } from "uuid";
33

44
import dayjs from "@calcom/dayjs";
55
import generateIcsString from "@calcom/emails/lib/generateIcsString";
6+
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
67
import { preprocessNameFieldDataWithVariant } from "@calcom/features/form-builder/utils";
78
import tasker from "@calcom/features/tasker";
89
import { WEBSITE_URL } from "@calcom/lib/constants";
@@ -50,6 +51,8 @@ interface scheduleEmailReminderArgs extends ScheduleReminderArgs {
5051
evt: BookingInfo;
5152
sendTo: string[];
5253
action: ScheduleEmailReminderAction;
54+
userId?: number | null;
55+
teamId?: number | null;
5356
emailSubject?: string;
5457
emailBody?: string;
5558
hideBranding?: boolean;
@@ -75,6 +78,8 @@ export const scheduleEmailReminder = async (args: scheduleEmailReminderArgs) =>
7578
isMandatoryReminder,
7679
action,
7780
verifiedAt,
81+
userId,
82+
teamId,
7883
} = args;
7984

8085
if (!verifiedAt) {
@@ -259,9 +264,17 @@ export const scheduleEmailReminder = async (args: scheduleEmailReminderArgs) =>
259264

260265
const mailData = await prepareEmailData();
261266

262-
const isSendgridEnabled = process.env.SENDGRID_API_KEY && process.env.SENDGRID_EMAIL;
267+
const isSendgridEnabled = !!(process.env.SENDGRID_API_KEY && process.env.SENDGRID_EMAIL);
263268

264-
if (!isSendgridEnabled) {
269+
const featureRepo = new FeaturesRepository();
270+
271+
const isWorkflowSmtpEmailsEnabled = teamId
272+
? await featureRepo.checkIfTeamHasFeature(teamId, "workflow-smtp-emails")
273+
: userId
274+
? await featureRepo.checkIfUserHasFeature(userId, "workflow-smtp-emails")
275+
: false;
276+
277+
if (isWorkflowSmtpEmailsEnabled || !isSendgridEnabled) {
265278
let reminderUid;
266279
if (scheduledDate) {
267280
const reminder = await prisma.workflowReminder.create({

packages/features/ee/workflows/lib/reminders/reminderScheduler.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,8 @@ const processWorkflowStep = async (
151151
seatReferenceUid,
152152
includeCalendarEvent: step.includeCalendarEvent,
153153
verifiedAt: step.verifiedAt,
154+
userId: workflow.userId,
155+
teamId: workflow.teamId,
154156
});
155157
} else if (isWhatsappAction(step.action)) {
156158
const sendTo = step.action === WorkflowActions.WHATSAPP_ATTENDEE ? smsReminderNumber : step.sendTo;

packages/features/ee/workflows/lib/reminders/scheduleMandatoryReminder.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export async function scheduleMandatoryReminder({
6565
isMandatoryReminder: true,
6666
// Template is fixed so we don't have to verify
6767
verifiedAt: new Date(),
68+
userId: evt.organizer.id,
6869
});
6970
} catch (error) {
7071
log.error("Error while scheduling mandatory reminders", JSON.stringify({ error }));

packages/features/ee/workflows/lib/test/workflows.test.ts

Lines changed: 123 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,28 @@ import {
1313
} from "@calcom/web/test/utils/bookingScenario/expects";
1414
import { setupAndTeardown } from "@calcom/web/test/utils/bookingScenario/setupAndTeardown";
1515

16-
import { describe, expect, beforeAll, vi } from "vitest";
16+
import { describe, expect, beforeAll, vi, beforeEach } from "vitest";
1717

1818
import dayjs from "@calcom/dayjs";
19-
import { BookingStatus, WorkflowMethods, TimeUnit } from "@calcom/prisma/enums";
19+
import {
20+
BookingStatus,
21+
WorkflowMethods,
22+
TimeUnit,
23+
WorkflowTriggerEvents,
24+
WorkflowActions,
25+
} from "@calcom/prisma/enums";
2026
import {
2127
deleteRemindersOfActiveOnIds,
2228
scheduleBookingReminders,
2329
bookingSelect,
2430
} from "@calcom/trpc/server/routers/viewer/workflows/util";
2531
import { test } from "@calcom/web/test/fixtures/fixtures";
2632

33+
import { FeaturesRepository } from "../../../../flags/features.repository";
2734
import { deleteWorkfowRemindersOfRemovedMember } from "../../../teams/lib/deleteWorkflowRemindersOfRemovedMember";
35+
import { scheduleEmailReminder } from "../reminders/emailReminderManager";
36+
import * as emailProvider from "../reminders/providers/emailProvider";
37+
import * as sendgridProvider from "../reminders/providers/sendgridProvider";
2838

2939
const workflowSelect = {
3040
id: true,
@@ -949,3 +959,114 @@ describe("deleteWorkfowRemindersOfRemovedMember", () => {
949959
);
950960
});
951961
});
962+
963+
describe("Workflow SMTP Emails Feature Flag", () => {
964+
vi.spyOn(sendgridProvider, "sendSendgridMail");
965+
vi.spyOn(emailProvider, "sendOrScheduleWorkflowEmails");
966+
967+
const mockEvt = {
968+
uid: "test-uid",
969+
title: "Test Event",
970+
startTime: new Date().toISOString(),
971+
endTime: new Date().toISOString(),
972+
bookerUrl: "https://cal.com",
973+
attendees: [
974+
{
975+
name: "Test Attendee",
976+
email: "attendee@test.com",
977+
timeZone: "UTC",
978+
language: { locale: "en" },
979+
},
980+
],
981+
organizer: {
982+
name: "Test Organizer",
983+
email: "organizer@test.com",
984+
timeZone: "UTC",
985+
language: { locale: "en" },
986+
},
987+
};
988+
989+
const baseArgs = {
990+
evt: mockEvt,
991+
triggerEvent: WorkflowTriggerEvents.NEW_EVENT,
992+
timeSpan: { time: 1, timeUnit: TimeUnit.HOUR },
993+
sendTo: ["test@example.com"],
994+
action: WorkflowActions.EMAIL_ATTENDEE,
995+
verifiedAt: new Date(),
996+
};
997+
998+
beforeEach(() => {
999+
vi.clearAllMocks();
1000+
// Mock SendGrid environment variables
1001+
process.env.SENDGRID_API_KEY = "test-key";
1002+
process.env.SENDGRID_EMAIL = "test@example.com";
1003+
});
1004+
1005+
test("should use SMTP when team has workflow-smtp-emails feature", async () => {
1006+
const featuresRepository = new FeaturesRepository();
1007+
vi.spyOn(FeaturesRepository.prototype, "checkIfTeamHasFeature").mockResolvedValue(true);
1008+
1009+
await scheduleEmailReminder({
1010+
...baseArgs,
1011+
teamId: 123,
1012+
});
1013+
expect(sendgridProvider.sendSendgridMail).not.toHaveBeenCalled();
1014+
expect(emailProvider.sendOrScheduleWorkflowEmails).toHaveBeenCalled();
1015+
});
1016+
1017+
test("should use SMTP when user has workflow-smtp-emails feature", async () => {
1018+
const featuresRepository = new FeaturesRepository();
1019+
vi.spyOn(FeaturesRepository.prototype, "checkIfUserHasFeature").mockResolvedValue(true);
1020+
1021+
await scheduleEmailReminder({
1022+
...baseArgs,
1023+
userId: 123,
1024+
});
1025+
expect(sendgridProvider.sendSendgridMail).not.toHaveBeenCalled();
1026+
expect(emailProvider.sendOrScheduleWorkflowEmails).toHaveBeenCalled();
1027+
});
1028+
1029+
test("should use SendGrid when workflow-smtp-emails feature is not enabled for team", async () => {
1030+
const featuresRepository = new FeaturesRepository();
1031+
vi.spyOn(FeaturesRepository.prototype, "checkIfTeamHasFeature").mockResolvedValue(false);
1032+
1033+
await scheduleEmailReminder({
1034+
...baseArgs,
1035+
teamId: 123,
1036+
});
1037+
1038+
expect(sendgridProvider.sendSendgridMail).toHaveBeenCalled();
1039+
expect(emailProvider.sendOrScheduleWorkflowEmails).not.toHaveBeenCalled();
1040+
});
1041+
1042+
test("should use SendGrid when workflow-smtp-emails feature is not enabled for user", async () => {
1043+
const featuresRepository = new FeaturesRepository();
1044+
vi.spyOn(FeaturesRepository.prototype, "checkIfUserHasFeature").mockResolvedValue(false);
1045+
1046+
await scheduleEmailReminder({
1047+
...baseArgs,
1048+
userId: 123,
1049+
});
1050+
1051+
expect(sendgridProvider.sendSendgridMail).toHaveBeenCalled();
1052+
expect(emailProvider.sendOrScheduleWorkflowEmails).not.toHaveBeenCalled();
1053+
});
1054+
1055+
test("should use SMTP when SendGrid is not configured", async () => {
1056+
const featuresRepository = new FeaturesRepository();
1057+
vi.spyOn(featuresRepository, "checkIfTeamHasFeature").mockResolvedValue(false);
1058+
vi.spyOn(featuresRepository, "checkIfUserHasFeature").mockResolvedValue(false);
1059+
1060+
delete process.env.SENDGRID_API_KEY;
1061+
delete process.env.SENDGRID_EMAIL;
1062+
1063+
await scheduleEmailReminder({
1064+
...baseArgs,
1065+
teamId: 123,
1066+
userId: 456,
1067+
});
1068+
1069+
expect(sendgridProvider.sendSendgridMail).not.toHaveBeenCalled();
1070+
expect(emailProvider.sendOrScheduleWorkflowEmails).toHaveBeenCalled();
1071+
});
1072+
});

packages/features/flags/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,6 @@ export type AppFlags = {
1818
"organizer-request-email-v2": boolean;
1919
"delegation-credential": boolean;
2020
"salesforce-crm-tasker": boolean;
21+
"workflow-smtp-emails": boolean;
2122
"cal-video-log-in-overlay": boolean;
2223
};

packages/features/flags/hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const initialData: AppFlags = {
1717
"organizer-request-email-v2": false,
1818
"delegation-credential": false,
1919
"salesforce-crm-tasker": false,
20+
"workflow-smtp-emails": false,
2021
"cal-video-log-in-overlay": false,
2122
};
2223

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
INSERT INTO
2+
"Feature" (slug, enabled, description, "type")
3+
VALUES
4+
(
5+
'workflow-smtp-emails',
6+
false,
7+
'Whether to use SMTP for workflow emails or SendGrid on a team/user basis.',
8+
'OPERATIONAL'
9+
) ON CONFLICT (slug) DO NOTHING;

0 commit comments

Comments
 (0)