feat: add feature flag for sending workflow emails with smtp (#21187)
* add feature flag * add unit tests --------- Co-authored-by: CarinaWolli <wollencarina@gmail.com>
This commit is contained in:
co-authored by
CarinaWolli
parent
50173658f8
commit
80fa172f79
@@ -423,6 +423,8 @@ async function handleWorkflowsUpdate({
|
||||
template: true,
|
||||
workflow: {
|
||||
select: {
|
||||
userId: true,
|
||||
teamId: true,
|
||||
trigger: true,
|
||||
time: true,
|
||||
timeUnit: true,
|
||||
@@ -468,6 +470,8 @@ async function handleWorkflowsUpdate({
|
||||
includeCalendarEvent: workflowStep.includeCalendarEvent,
|
||||
workflowStepId: workflowStep.id,
|
||||
verifiedAt: workflowStep.verifiedAt,
|
||||
userId: workflow.userId,
|
||||
teamId: workflow.teamId,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -460,6 +460,8 @@ export const roundRobinReassignment = async ({
|
||||
template: true,
|
||||
workflow: {
|
||||
select: {
|
||||
userId: true,
|
||||
teamId: true,
|
||||
trigger: true,
|
||||
time: true,
|
||||
timeUnit: true,
|
||||
@@ -506,6 +508,8 @@ export const roundRobinReassignment = async ({
|
||||
includeCalendarEvent: workflowStep.includeCalendarEvent,
|
||||
workflowStepId: workflowStep.id,
|
||||
verifiedAt: workflowStep.verifiedAt,
|
||||
userId: workflow.userId,
|
||||
teamId: workflow.teamId,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ export async function handler(req: NextRequest) {
|
||||
return NextResponse.json({ message: "Not authenticated" }, { status: 401 });
|
||||
}
|
||||
|
||||
const isSendgridEnabled = process.env.SENDGRID_API_KEY && process.env.SENDGRID_EMAIL;
|
||||
const isSendgridEnabled = !!(process.env.SENDGRID_API_KEY && process.env.SENDGRID_EMAIL);
|
||||
|
||||
if (isSendgridEnabled) {
|
||||
const remindersToDelete: { referenceId: string | null; id: number }[] = await getAllRemindersToDelete();
|
||||
@@ -65,9 +65,7 @@ export async function handler(req: NextRequest) {
|
||||
});
|
||||
|
||||
await Promise.allSettled(handlePastCancelledReminders);
|
||||
}
|
||||
|
||||
if (isSendgridEnabled) {
|
||||
//cancel reminders for cancelled/rescheduled bookings that are scheduled within the next hour
|
||||
const remindersToCancel: { referenceId: string | null; id: number }[] = await getAllRemindersToCancel();
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import generateIcsString from "@calcom/emails/lib/generateIcsString";
|
||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||
import { preprocessNameFieldDataWithVariant } from "@calcom/features/form-builder/utils";
|
||||
import tasker from "@calcom/features/tasker";
|
||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||
@@ -50,6 +51,8 @@ interface scheduleEmailReminderArgs extends ScheduleReminderArgs {
|
||||
evt: BookingInfo;
|
||||
sendTo: string[];
|
||||
action: ScheduleEmailReminderAction;
|
||||
userId?: number | null;
|
||||
teamId?: number | null;
|
||||
emailSubject?: string;
|
||||
emailBody?: string;
|
||||
hideBranding?: boolean;
|
||||
@@ -75,6 +78,8 @@ export const scheduleEmailReminder = async (args: scheduleEmailReminderArgs) =>
|
||||
isMandatoryReminder,
|
||||
action,
|
||||
verifiedAt,
|
||||
userId,
|
||||
teamId,
|
||||
} = args;
|
||||
|
||||
if (!verifiedAt) {
|
||||
@@ -259,9 +264,17 @@ export const scheduleEmailReminder = async (args: scheduleEmailReminderArgs) =>
|
||||
|
||||
const mailData = await prepareEmailData();
|
||||
|
||||
const isSendgridEnabled = process.env.SENDGRID_API_KEY && process.env.SENDGRID_EMAIL;
|
||||
const isSendgridEnabled = !!(process.env.SENDGRID_API_KEY && process.env.SENDGRID_EMAIL);
|
||||
|
||||
if (!isSendgridEnabled) {
|
||||
const featureRepo = new FeaturesRepository();
|
||||
|
||||
const isWorkflowSmtpEmailsEnabled = teamId
|
||||
? await featureRepo.checkIfTeamHasFeature(teamId, "workflow-smtp-emails")
|
||||
: userId
|
||||
? await featureRepo.checkIfUserHasFeature(userId, "workflow-smtp-emails")
|
||||
: false;
|
||||
|
||||
if (isWorkflowSmtpEmailsEnabled || !isSendgridEnabled) {
|
||||
let reminderUid;
|
||||
if (scheduledDate) {
|
||||
const reminder = await prisma.workflowReminder.create({
|
||||
|
||||
@@ -151,6 +151,8 @@ const processWorkflowStep = async (
|
||||
seatReferenceUid,
|
||||
includeCalendarEvent: step.includeCalendarEvent,
|
||||
verifiedAt: step.verifiedAt,
|
||||
userId: workflow.userId,
|
||||
teamId: workflow.teamId,
|
||||
});
|
||||
} else if (isWhatsappAction(step.action)) {
|
||||
const sendTo = step.action === WorkflowActions.WHATSAPP_ATTENDEE ? smsReminderNumber : step.sendTo;
|
||||
|
||||
@@ -65,6 +65,7 @@ export async function scheduleMandatoryReminder({
|
||||
isMandatoryReminder: true,
|
||||
// Template is fixed so we don't have to verify
|
||||
verifiedAt: new Date(),
|
||||
userId: evt.organizer.id,
|
||||
});
|
||||
} catch (error) {
|
||||
log.error("Error while scheduling mandatory reminders", JSON.stringify({ error }));
|
||||
|
||||
@@ -13,10 +13,16 @@ import {
|
||||
} from "@calcom/web/test/utils/bookingScenario/expects";
|
||||
import { setupAndTeardown } from "@calcom/web/test/utils/bookingScenario/setupAndTeardown";
|
||||
|
||||
import { describe, expect, beforeAll, vi } from "vitest";
|
||||
import { describe, expect, beforeAll, vi, beforeEach } from "vitest";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { BookingStatus, WorkflowMethods, TimeUnit } from "@calcom/prisma/enums";
|
||||
import {
|
||||
BookingStatus,
|
||||
WorkflowMethods,
|
||||
TimeUnit,
|
||||
WorkflowTriggerEvents,
|
||||
WorkflowActions,
|
||||
} from "@calcom/prisma/enums";
|
||||
import {
|
||||
deleteRemindersOfActiveOnIds,
|
||||
scheduleBookingReminders,
|
||||
@@ -24,7 +30,11 @@ import {
|
||||
} from "@calcom/trpc/server/routers/viewer/workflows/util";
|
||||
import { test } from "@calcom/web/test/fixtures/fixtures";
|
||||
|
||||
import { FeaturesRepository } from "../../../../flags/features.repository";
|
||||
import { deleteWorkfowRemindersOfRemovedMember } from "../../../teams/lib/deleteWorkflowRemindersOfRemovedMember";
|
||||
import { scheduleEmailReminder } from "../reminders/emailReminderManager";
|
||||
import * as emailProvider from "../reminders/providers/emailProvider";
|
||||
import * as sendgridProvider from "../reminders/providers/sendgridProvider";
|
||||
|
||||
const workflowSelect = {
|
||||
id: true,
|
||||
@@ -949,3 +959,114 @@ describe("deleteWorkfowRemindersOfRemovedMember", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Workflow SMTP Emails Feature Flag", () => {
|
||||
vi.spyOn(sendgridProvider, "sendSendgridMail");
|
||||
vi.spyOn(emailProvider, "sendOrScheduleWorkflowEmails");
|
||||
|
||||
const mockEvt = {
|
||||
uid: "test-uid",
|
||||
title: "Test Event",
|
||||
startTime: new Date().toISOString(),
|
||||
endTime: new Date().toISOString(),
|
||||
bookerUrl: "https://cal.com",
|
||||
attendees: [
|
||||
{
|
||||
name: "Test Attendee",
|
||||
email: "attendee@test.com",
|
||||
timeZone: "UTC",
|
||||
language: { locale: "en" },
|
||||
},
|
||||
],
|
||||
organizer: {
|
||||
name: "Test Organizer",
|
||||
email: "organizer@test.com",
|
||||
timeZone: "UTC",
|
||||
language: { locale: "en" },
|
||||
},
|
||||
};
|
||||
|
||||
const baseArgs = {
|
||||
evt: mockEvt,
|
||||
triggerEvent: WorkflowTriggerEvents.NEW_EVENT,
|
||||
timeSpan: { time: 1, timeUnit: TimeUnit.HOUR },
|
||||
sendTo: ["test@example.com"],
|
||||
action: WorkflowActions.EMAIL_ATTENDEE,
|
||||
verifiedAt: new Date(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
// Mock SendGrid environment variables
|
||||
process.env.SENDGRID_API_KEY = "test-key";
|
||||
process.env.SENDGRID_EMAIL = "test@example.com";
|
||||
});
|
||||
|
||||
test("should use SMTP when team has workflow-smtp-emails feature", async () => {
|
||||
const featuresRepository = new FeaturesRepository();
|
||||
vi.spyOn(FeaturesRepository.prototype, "checkIfTeamHasFeature").mockResolvedValue(true);
|
||||
|
||||
await scheduleEmailReminder({
|
||||
...baseArgs,
|
||||
teamId: 123,
|
||||
});
|
||||
expect(sendgridProvider.sendSendgridMail).not.toHaveBeenCalled();
|
||||
expect(emailProvider.sendOrScheduleWorkflowEmails).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should use SMTP when user has workflow-smtp-emails feature", async () => {
|
||||
const featuresRepository = new FeaturesRepository();
|
||||
vi.spyOn(FeaturesRepository.prototype, "checkIfUserHasFeature").mockResolvedValue(true);
|
||||
|
||||
await scheduleEmailReminder({
|
||||
...baseArgs,
|
||||
userId: 123,
|
||||
});
|
||||
expect(sendgridProvider.sendSendgridMail).not.toHaveBeenCalled();
|
||||
expect(emailProvider.sendOrScheduleWorkflowEmails).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should use SendGrid when workflow-smtp-emails feature is not enabled for team", async () => {
|
||||
const featuresRepository = new FeaturesRepository();
|
||||
vi.spyOn(FeaturesRepository.prototype, "checkIfTeamHasFeature").mockResolvedValue(false);
|
||||
|
||||
await scheduleEmailReminder({
|
||||
...baseArgs,
|
||||
teamId: 123,
|
||||
});
|
||||
|
||||
expect(sendgridProvider.sendSendgridMail).toHaveBeenCalled();
|
||||
expect(emailProvider.sendOrScheduleWorkflowEmails).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should use SendGrid when workflow-smtp-emails feature is not enabled for user", async () => {
|
||||
const featuresRepository = new FeaturesRepository();
|
||||
vi.spyOn(FeaturesRepository.prototype, "checkIfUserHasFeature").mockResolvedValue(false);
|
||||
|
||||
await scheduleEmailReminder({
|
||||
...baseArgs,
|
||||
userId: 123,
|
||||
});
|
||||
|
||||
expect(sendgridProvider.sendSendgridMail).toHaveBeenCalled();
|
||||
expect(emailProvider.sendOrScheduleWorkflowEmails).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should use SMTP when SendGrid is not configured", async () => {
|
||||
const featuresRepository = new FeaturesRepository();
|
||||
vi.spyOn(featuresRepository, "checkIfTeamHasFeature").mockResolvedValue(false);
|
||||
vi.spyOn(featuresRepository, "checkIfUserHasFeature").mockResolvedValue(false);
|
||||
|
||||
delete process.env.SENDGRID_API_KEY;
|
||||
delete process.env.SENDGRID_EMAIL;
|
||||
|
||||
await scheduleEmailReminder({
|
||||
...baseArgs,
|
||||
teamId: 123,
|
||||
userId: 456,
|
||||
});
|
||||
|
||||
expect(sendgridProvider.sendSendgridMail).not.toHaveBeenCalled();
|
||||
expect(emailProvider.sendOrScheduleWorkflowEmails).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,5 +18,6 @@ export type AppFlags = {
|
||||
"organizer-request-email-v2": boolean;
|
||||
"delegation-credential": boolean;
|
||||
"salesforce-crm-tasker": boolean;
|
||||
"workflow-smtp-emails": boolean;
|
||||
"cal-video-log-in-overlay": boolean;
|
||||
};
|
||||
|
||||
@@ -17,6 +17,7 @@ const initialData: AppFlags = {
|
||||
"organizer-request-email-v2": false,
|
||||
"delegation-credential": false,
|
||||
"salesforce-crm-tasker": false,
|
||||
"workflow-smtp-emails": false,
|
||||
"cal-video-log-in-overlay": false,
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
INSERT INTO
|
||||
"Feature" (slug, enabled, description, "type")
|
||||
VALUES
|
||||
(
|
||||
'workflow-smtp-emails',
|
||||
false,
|
||||
'Whether to use SMTP for workflow emails or SendGrid on a team/user basis.',
|
||||
'OPERATIONAL'
|
||||
) ON CONFLICT (slug) DO NOTHING;
|
||||
@@ -314,6 +314,8 @@ export const activateEventTypeHandler = async ({ ctx, input }: ActivateEventType
|
||||
sender: step.sender,
|
||||
workflowStepId: step.id,
|
||||
verifiedAt: step.verifiedAt,
|
||||
userId: eventTypeWorkflow.userId,
|
||||
teamId: eventTypeWorkflow.teamId,
|
||||
});
|
||||
} else if (step.action === WorkflowActions.SMS_NUMBER && step.sendTo) {
|
||||
await scheduleSMSReminder({
|
||||
|
||||
@@ -704,6 +704,8 @@ export async function scheduleBookingReminders(
|
||||
sender: step.sender,
|
||||
workflowStepId: step.id,
|
||||
verifiedAt: step?.verifiedAt ?? null,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
} else if (step.action === WorkflowActions.SMS_NUMBER && step.sendTo) {
|
||||
await scheduleSMSReminder({
|
||||
|
||||
Reference in New Issue
Block a user