Files
calendar/packages/features/ee/workflows/lib/service/EmailWorkflowService.test.ts
T
Udit TakkarGitHubUdit Takkarunknown <>Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>Udit Takkar
4081d11fbe feat: workflow auto translation (#27087)
* feat: workflow auto translation

* tests: add unit tests

* refactor: tests and workflow

* fix: type err

* fix: type err

* fix: remove redundant index on WorkflowStepTranslation

The @@index on [workflowStepId, field, targetLocale] duplicates the @@unique
constraint on the same columns. A unique index already provides efficient
lookups, so the separate @@index adds storage overhead and write latency
without benefit.

Addresses Cubic AI review feedback (confidence 9/10).

Co-Authored-By: unknown <>

* fix: correct locale mapping when translation API returns null

Map translations with their corresponding locales before filtering to
preserve correct locale-to-translation associations. Previously, filtering
out null translations would reindex the array, causing incorrect locale
mappings when any translation in the batch failed.

Also fixes pre-existing lint warnings:
- Move exports to end of file
- Add explicit return type to processTranslations
- Replace ternary with if-else for upsertMany selection

Co-Authored-By: udit@cal.com <udit222001@gmail.com>

* fix: address review feedback for workflow auto-translation

- Add change detection before creating translation tasks
- Rename userLocale to sourceLocale in task props for clarity
- Show source language in UI with new translation key
- Extract SUPPORTED_LOCALES to shared translationConstants.ts
- Fix locale mapping bug in translateEventTypeData.ts
- Add WhatsApp translation support
- Abstract translation lookup into shared translationLookup.ts helper
- Restore if-else readability for SCANNING_WORKFLOW_STEPS

Co-authored-by: Udit Takkar <udit.takkar@cal.com>
Co-Authored-By: unknown <>

* fix: update test to use sourceLocale instead of userLocale

Co-Authored-By: unknown <>

* refactor: feedback

* fix: handle first time

* fix: tests

* fix: tests

* fix: address Cubic AI review feedback (confidence 9/10 issues)

- WhatsApp translation: Apply variable substitution using getSMSMessageWithVariables
  and clear contentSid when using translated body to ensure Twilio uses the
  translated text instead of the original template

- update.handler.ts: Change sourceLocale assignment from ?? to || for consistency
  with tasker payload behavior (line 481)

- ITranslationService.ts: Rename methods from plural to singular naming:
  - getWorkflowStepTranslations -> getWorkflowStepTranslation
  - getEventTypeTranslations -> getEventTypeTranslation
  Updated all call sites and tests accordingly

Co-Authored-By: unknown <>

* fix: address Cubic AI review feedback (confidence 9/10+ issues)

- Fix getSMSMessageWithVariables to handle WHATSAPP_ATTENDEE action for
  locale and timezone (confidence 9/10)
- Remove WhatsApp translation feature that set contentSid to undefined
  since Twilio ignores body parameter for WhatsApp and requires
  pre-approved Message Templates (confidence 10/10)

Co-Authored-By: unknown <>

* fix: translatio

* Add tests: packages/features/eventTypeTranslation/repositories/EventTypeTranslationRepository.test.ts

Generated by Paragon from proposal for PR #27087

* Add tests: packages/features/tasker/tasks/translateWorkflowStepData.test.ts

Generated by Paragon from proposal for PR #27087

* chore: nit

* chore: verfied atg

* fix: set sourceLocale for new steps, add shouldDirty to checkbox, remove spec docs

- Set sourceLocale fallback in addedSteps mapping to fix stale detection mismatch
- Add { shouldDirty: true } to autoTranslateEnabled checkbox onChange
- Remove specs/workflow-translation/ directory (planning docs, not for repo)

Co-authored-by: Udit Takkar <udit.07.takkar@gmail.com>
Co-Authored-By: unknown <>

* chore: add specs back

* fix: type error

* fix: type error

* fix: type err

* fix: tests

* refactor: feedback

* fix: type err

* refactor

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit.takkar@cal.com>
Co-authored-by: Udit Takkar <udit.07.takkar@gmail.com>
2026-02-25 01:03:55 +05:30

836 lines
28 KiB
TypeScript

import type { BookingSeatRepository } from "@calcom/features/bookings/repositories/BookingSeatRepository";
import type { WorkflowReminderRepository } from "@calcom/features/ee/workflows/repositories/WorkflowReminderRepository";
import { TimeFormat } from "@calcom/lib/timeFormat";
import {
SchedulingType,
TimeUnit,
WorkflowActions,
WorkflowTemplates,
WorkflowTriggerEvents,
} from "@calcom/prisma/enums";
import type { CalendarEvent } from "@calcom/types/Calendar";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { EmailWorkflowService } from "./EmailWorkflowService";
vi.mock("@calcom/emails/workflow-email-service", () => ({
sendCustomWorkflowEmail: vi.fn(),
}));
vi.mock("@calcom/features/profile/lib/hideBranding", () => ({
getHideBranding: vi.fn().mockResolvedValue(false),
}));
vi.mock("@calcom/i18n/server", () => ({
getTranslation: vi.fn().mockResolvedValue((key: string) => key),
}));
vi.mock("@calcom/prisma", () => ({
default: {},
prisma: {},
}));
vi.mock("@calcom/emails/lib/generateIcsString", () => ({
default: vi.fn().mockReturnValue("mock-ics-content"),
}));
const mockTranslationService = vi.hoisted(() => ({
getWorkflowStepTranslation: vi.fn(),
}));
vi.mock("@calcom/features/di/containers/TranslationService", () => ({
getTranslationService: vi.fn().mockResolvedValue(mockTranslationService),
}));
vi.mock("@calcom/lib/constants", async () => {
const actual = await vi.importActual<typeof import("@calcom/lib/constants")>("@calcom/lib/constants");
return {
...actual,
WEBAPP_URL: "https://cal.com",
APP_NAME: "Cal.com",
};
});
vi.mock("short-uuid", () => ({
__esModule: true,
default: () => ({ fromUUID: (uid: string) => uid }),
}));
const mockWorkflowReminderRepository: Pick<WorkflowReminderRepository, "findByIdIncludeStepAndWorkflow"> = {
findByIdIncludeStepAndWorkflow: vi.fn(),
};
const mockBookingSeatRepository: Pick<BookingSeatRepository, "getByUidIncludeAttendee"> = {
getByUidIncludeAttendee: vi.fn(),
};
describe("EmailWorkflowService", () => {
let emailWorkflowService: EmailWorkflowService;
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(mockTranslationService.getWorkflowStepTranslation).mockReset();
emailWorkflowService = new EmailWorkflowService(
mockWorkflowReminderRepository as WorkflowReminderRepository,
mockBookingSeatRepository as BookingSeatRepository
);
});
describe("handleSendEmailWorkflowTask", () => {
const mockEvt: Partial<CalendarEvent> = {
uid: "booking-123",
bookerUrl: "https://cal.com",
title: "Test Meeting",
startTime: "2024-12-01T10:00:00Z",
endTime: "2024-12-01T11:00:00Z",
organizer: {
name: "Organizer Name",
email: "organizer@example.com",
timeZone: "UTC",
language: { locale: "en", translate: (key: string) => key },
timeFormat: 12,
},
attendees: [
{
name: "Attendee Name",
email: "attendee@example.com",
timeZone: "UTC",
language: { locale: "en", translate: (key: string) => key },
},
],
};
test("should throw error if workflow reminder not found", async () => {
vi.mocked(mockWorkflowReminderRepository.findByIdIncludeStepAndWorkflow).mockResolvedValue(null);
await expect(
emailWorkflowService.handleSendEmailWorkflowTask({
evt: mockEvt as CalendarEvent,
workflowReminderId: 1,
})
).rejects.toThrow("Workflow reminder not found with id 1");
});
test("should throw error if workflow step not verified", async () => {
vi.mocked(mockWorkflowReminderRepository.findByIdIncludeStepAndWorkflow).mockResolvedValue({
id: 1,
workflowStep: {
id: 1,
verifiedAt: null,
action: WorkflowActions.EMAIL_ATTENDEE,
workflow: { userId: 1, teamId: null },
},
});
await expect(
emailWorkflowService.handleSendEmailWorkflowTask({
evt: mockEvt as CalendarEvent,
workflowReminderId: 1,
})
).rejects.toThrow("Workflow step id 1 is not verified");
});
test("should throw error if workflow step not found on reminder", async () => {
vi.mocked(mockWorkflowReminderRepository.findByIdIncludeStepAndWorkflow).mockResolvedValue({
id: 1,
workflowStep: null,
});
await expect(
emailWorkflowService.handleSendEmailWorkflowTask({
evt: mockEvt as CalendarEvent,
workflowReminderId: 1,
})
).rejects.toThrow("Workflow step not found on reminder with id 1");
});
test("should fetch seat attendee email for seated events", async () => {
const mockWorkflowReminder = {
id: 1,
seatReferenceId: "seat-123",
workflowStep: {
id: 1,
action: WorkflowActions.EMAIL_ATTENDEE,
sendTo: null,
template: WorkflowTemplates.REMINDER,
reminderBody: null,
emailSubject: null,
sender: null,
includeCalendarEvent: false,
verifiedAt: new Date(),
workflow: {
userId: 1,
teamId: null,
trigger: WorkflowTriggerEvents.BEFORE_EVENT,
time: 24,
timeUnit: "HOUR",
},
},
};
vi.mocked(mockWorkflowReminderRepository.findByIdIncludeStepAndWorkflow).mockResolvedValue(
mockWorkflowReminder
);
vi.mocked(mockBookingSeatRepository.getByUidIncludeAttendee).mockResolvedValue({
attendee: {
email: "seat-attendee@example.com",
},
});
try {
await emailWorkflowService.handleSendEmailWorkflowTask({
evt: mockEvt as CalendarEvent,
workflowReminderId: 1,
});
} catch {
// Expected to throw due to incomplete mock setup - we only care about the seat lookup
}
expect(mockBookingSeatRepository.getByUidIncludeAttendee).toHaveBeenCalledWith("seat-123");
});
});
describe("generateParametersToBuildEmailWorkflowContent - EMAIL_HOST", () => {
const mockCommonScheduleFunctionParams = {
triggerEvent: WorkflowTriggerEvents.BEFORE_EVENT,
timeSpan: {
time: 24,
timeUnit: TimeUnit.HOUR,
},
workflowStepId: 1,
template: WorkflowTemplates.REMINDER,
userId: 1,
teamId: null,
seatReferenceUid: undefined,
verifiedAt: new Date(),
creditCheckFn: vi.fn().mockResolvedValue(true),
};
const baseMockEvt: Partial<CalendarEvent> = {
uid: "booking-123",
bookerUrl: "https://cal.com",
title: "Test Meeting",
startTime: "2024-12-01T10:00:00Z",
endTime: "2024-12-01T11:00:00Z",
organizer: {
name: "Organizer Name",
email: "organizer@example.com",
timeZone: "UTC",
// eslint-disable-next-line @typescript-eslint/no-explicit-any
language: { locale: "en", translate: (() => "") as any },
// eslint-disable-next-line @typescript-eslint/no-explicit-any
timeFormat: "h:mma" as any,
},
attendees: [
{
name: "Attendee Name",
email: "attendee@example.com",
timeZone: "UTC",
// eslint-disable-next-line @typescript-eslint/no-explicit-any
language: { locale: "en", translate: (() => "") as any },
},
],
};
const mockWorkflowStep = {
id: 1,
action: WorkflowActions.EMAIL_HOST,
verifiedAt: new Date(),
sendTo: null,
template: WorkflowTemplates.REMINDER,
reminderBody: null,
emailSubject: null,
sender: null,
includeCalendarEvent: false,
numberVerificationPending: false,
numberRequired: false,
};
test("should send to organizer and team members for ROUND_ROBIN scheduling type", async () => {
// Note: For ROUND_ROBIN, the CalendarEventBuilder filters team members to only include
// those assigned to the booking. EmailWorkflowService sends to all team members in evt.team.members.
const mockEvt: Partial<CalendarEvent> = {
...baseMockEvt,
schedulingType: SchedulingType.ROUND_ROBIN,
team: {
id: 1,
name: "Test Team",
members: [
{
id: 1,
name: "Team Member 1",
email: "team1@example.com",
timeZone: "UTC",
// eslint-disable-next-line @typescript-eslint/no-explicit-any
language: { locale: "en", translate: (() => "") as any },
},
{
id: 2,
name: "Team Member 2",
email: "team2@example.com",
timeZone: "UTC",
// eslint-disable-next-line @typescript-eslint/no-explicit-any
language: { locale: "en", translate: (() => "") as any },
},
],
},
};
const result = await emailWorkflowService.generateParametersToBuildEmailWorkflowContent({
evt: mockEvt as CalendarEvent,
workflowStep: mockWorkflowStep,
workflow: { userId: 1 },
commonScheduleFunctionParams: mockCommonScheduleFunctionParams,
hideBranding: false,
});
// EmailWorkflowService sends to organizer + all team members in evt.team.members
// The filtering of team members happens in CalendarEventBuilder, not here
expect(result.sendTo).toContain("organizer@example.com");
expect(result.sendTo).toContain("team1@example.com");
expect(result.sendTo).toContain("team2@example.com");
expect(result.sendTo.length).toBe(3);
});
test("should send to organizer and team members for COLLECTIVE scheduling type", async () => {
const mockEvt: Partial<CalendarEvent> = {
...baseMockEvt,
schedulingType: SchedulingType.COLLECTIVE,
team: {
id: 1,
name: "Test Team",
members: [
{
id: 1,
name: "Team Member 1",
email: "team1@example.com",
timeZone: "UTC",
// eslint-disable-next-line @typescript-eslint/no-explicit-any
language: { locale: "en", translate: (() => "") as any },
},
{
id: 2,
name: "Team Member 2",
email: "team2@example.com",
timeZone: "UTC",
// eslint-disable-next-line @typescript-eslint/no-explicit-any
language: { locale: "en", translate: (() => "") as any },
},
],
},
};
const result = await emailWorkflowService.generateParametersToBuildEmailWorkflowContent({
evt: mockEvt as CalendarEvent,
workflowStep: mockWorkflowStep,
workflow: { userId: 1 },
commonScheduleFunctionParams: mockCommonScheduleFunctionParams,
hideBranding: false,
});
expect(result.sendTo).toContain("organizer@example.com");
expect(result.sendTo).toContain("team1@example.com");
expect(result.sendTo).toContain("team2@example.com");
expect(result.sendTo.length).toBe(3);
});
test("should send to organizer only when team is undefined for COLLECTIVE", async () => {
const mockEvt: Partial<CalendarEvent> = {
...baseMockEvt,
schedulingType: SchedulingType.COLLECTIVE,
team: undefined,
} as Partial<CalendarEvent>;
const result = await emailWorkflowService.generateParametersToBuildEmailWorkflowContent({
evt: mockEvt as CalendarEvent,
workflowStep: mockWorkflowStep,
workflow: { userId: 1 },
commonScheduleFunctionParams: mockCommonScheduleFunctionParams,
hideBranding: false,
});
expect(result.sendTo).toEqual(["organizer@example.com"]);
expect(result.sendTo.length).toBe(1);
});
test("should send to organizer only when team members array is empty for COLLECTIVE", async () => {
const mockEvt: Partial<CalendarEvent> = {
...baseMockEvt,
schedulingType: SchedulingType.COLLECTIVE,
team: {
id: 1,
name: "Test Team",
members: [],
},
};
const result = await emailWorkflowService.generateParametersToBuildEmailWorkflowContent({
evt: mockEvt as CalendarEvent,
workflowStep: mockWorkflowStep,
workflow: { userId: 1 },
commonScheduleFunctionParams: mockCommonScheduleFunctionParams,
hideBranding: false,
});
expect(result.sendTo).toEqual(["organizer@example.com"]);
expect(result.sendTo.length).toBe(1);
});
test("should send to organizer only for other scheduling types (e.g., null)", async () => {
const mockEvt: Partial<CalendarEvent> = {
...baseMockEvt,
schedulingType: null,
team: {
id: 1,
name: "Test Team",
members: [
{
id: 1,
name: "Team Member 1",
email: "team1@example.com",
timeZone: "UTC",
// eslint-disable-next-line @typescript-eslint/no-explicit-any
language: { locale: "en", translate: (() => "") as any },
},
],
},
} as Partial<CalendarEvent>;
const result = await emailWorkflowService.generateParametersToBuildEmailWorkflowContent({
evt: mockEvt as CalendarEvent,
workflowStep: mockWorkflowStep,
workflow: { userId: 1 },
commonScheduleFunctionParams: mockCommonScheduleFunctionParams,
hideBranding: false,
});
expect(result.sendTo).toEqual(["organizer@example.com"]);
expect(result.sendTo.length).toBe(1);
});
});
describe("generateParametersToBuildEmailWorkflowContent - Form Data", () => {
const mockCommonScheduleFunctionParams = {
triggerEvent: WorkflowTriggerEvents.FORM_SUBMITTED,
timeSpan: {
time: null,
timeUnit: null,
},
workflowStepId: 1,
template: WorkflowTemplates.REMINDER,
userId: 1,
teamId: null,
seatReferenceUid: undefined,
verifiedAt: new Date(),
creditCheckFn: vi.fn().mockResolvedValue(true),
};
const mockFormData = {
responses: {
"contact person": {
value: "Jane Smith",
response: "Jane Smith",
},
email: {
value: "jane@example.com",
response: "jane@example.com",
},
},
routedEventTypeId: null,
user: {
email: "user@test.com",
timeFormat: 12,
locale: "en",
},
};
test("should work with EMAIL_ADDRESS action and formData (no evt)", async () => {
const mockWorkflowStep = {
id: 1,
action: WorkflowActions.EMAIL_ADDRESS,
verifiedAt: new Date(),
sendTo: "recipient@example.com",
template: WorkflowTemplates.REMINDER,
reminderBody: null,
emailSubject: null,
sender: null,
includeCalendarEvent: false,
numberVerificationPending: false,
numberRequired: false,
};
const result = await emailWorkflowService.generateParametersToBuildEmailWorkflowContent({
formData: mockFormData,
workflowStep: mockWorkflowStep,
workflow: { userId: 1 },
commonScheduleFunctionParams: mockCommonScheduleFunctionParams,
hideBranding: false,
});
expect(result.sendTo).toEqual(["recipient@example.com"]);
expect(result.formData).toEqual(mockFormData);
expect(result.evt).toBeUndefined();
});
test("should work with EMAIL_ATTENDEE action and formData (no evt)", async () => {
const mockWorkflowStep = {
id: 1,
action: WorkflowActions.EMAIL_ATTENDEE,
verifiedAt: new Date(),
sendTo: null,
template: WorkflowTemplates.REMINDER,
reminderBody: null,
emailSubject: null,
sender: null,
includeCalendarEvent: false,
numberVerificationPending: false,
numberRequired: false,
};
const result = await emailWorkflowService.generateParametersToBuildEmailWorkflowContent({
formData: mockFormData,
workflowStep: mockWorkflowStep,
workflow: { userId: 1 },
commonScheduleFunctionParams: mockCommonScheduleFunctionParams,
hideBranding: false,
});
// Should extract email from formData responses
expect(result.sendTo).toEqual(["jane@example.com"]);
expect(result.formData).toEqual(mockFormData);
expect(result.evt).toBeUndefined();
});
test("should throw error if neither evt nor formData is provided", async () => {
const mockWorkflowStep = {
id: 1,
action: WorkflowActions.EMAIL_ADDRESS,
verifiedAt: new Date(),
sendTo: "recipient@example.com",
template: WorkflowTemplates.REMINDER,
reminderBody: null,
emailSubject: null,
sender: null,
includeCalendarEvent: false,
numberVerificationPending: false,
numberRequired: false,
};
await expect(
emailWorkflowService.generateParametersToBuildEmailWorkflowContent({
workflowStep: mockWorkflowStep,
workflow: { userId: 1 },
commonScheduleFunctionParams: mockCommonScheduleFunctionParams,
hideBranding: false,
})
).rejects.toThrow("Either evt or formData must be provided");
});
});
describe("generateEmailPayloadForEvtWorkflow - ICS attachment", () => {
const mockBookingInfo = {
uid: "booking-123",
bookerUrl: "https://cal.com",
title: "Test Meeting",
startTime: "2024-12-01T10:00:00Z",
endTime: "2024-12-01T11:00:00Z",
organizer: {
name: "Organizer Name",
email: "organizer@example.com",
timeZone: "UTC",
language: { locale: "en" },
timeFormat: "h:mma",
},
attendees: [
{
name: "Attendee Name",
email: "attendee@example.com",
timeZone: "UTC",
language: { locale: "en" },
},
],
};
test("should NOT include ICS attachment for BOOKING_REQUESTED trigger even when includeCalendarEvent is true", async () => {
const result = await emailWorkflowService.generateEmailPayloadForEvtWorkflow({
evt: mockBookingInfo,
sendTo: ["attendee@example.com"],
hideBranding: false,
emailSubject: "Test Subject",
emailBody: "Test Body",
sender: "Cal.com",
action: WorkflowActions.EMAIL_ATTENDEE,
template: WorkflowTemplates.REMINDER,
includeCalendarEvent: true,
triggerEvent: WorkflowTriggerEvents.BOOKING_REQUESTED,
});
expect(result.attachments).toBeUndefined();
});
test("should include ICS attachment for BEFORE_EVENT trigger when includeCalendarEvent is true", async () => {
const result = await emailWorkflowService.generateEmailPayloadForEvtWorkflow({
evt: mockBookingInfo,
sendTo: ["attendee@example.com"],
hideBranding: false,
emailSubject: "Test Subject",
emailBody: "Test Body",
sender: "Cal.com",
action: WorkflowActions.EMAIL_ATTENDEE,
template: WorkflowTemplates.REMINDER,
includeCalendarEvent: true,
triggerEvent: WorkflowTriggerEvents.BEFORE_EVENT,
});
expect(result.attachments).toBeDefined();
expect(result.attachments).toHaveLength(1);
expect(result.attachments?.[0].filename).toBe("event.ics");
expect(result.attachments?.[0].contentType).toBe("text/calendar; charset=UTF-8; method=REQUEST");
});
test("should NOT include ICS attachment when includeCalendarEvent is false", async () => {
const result = await emailWorkflowService.generateEmailPayloadForEvtWorkflow({
evt: mockBookingInfo,
sendTo: ["attendee@example.com"],
hideBranding: false,
emailSubject: "Test Subject",
emailBody: "Test Body",
sender: "Cal.com",
action: WorkflowActions.EMAIL_ATTENDEE,
template: WorkflowTemplates.REMINDER,
includeCalendarEvent: false,
triggerEvent: WorkflowTriggerEvents.BEFORE_EVENT,
});
expect(result.attachments).toBeUndefined();
});
});
describe("generateEmailPayloadForEvtWorkflow - Auto Translation", () => {
const mockBookingInfo = {
uid: "booking-123",
bookerUrl: "https://cal.com",
title: "Test Meeting",
startTime: "2024-12-01T10:00:00Z",
endTime: "2024-12-01T11:00:00Z",
organizer: {
name: "Organizer Name",
email: "organizer@example.com",
timeZone: "UTC",
language: { locale: "en" },
timeFormat: "h:mma",
},
attendees: [
{
name: "Spanish Attendee",
email: "attendee@example.com",
timeZone: "Europe/Madrid",
language: { locale: "es" },
},
],
};
beforeEach(() => {
vi.mocked(mockTranslationService.getWorkflowStepTranslation).mockReset();
});
test("should use translated content when autoTranslateEnabled is true and translation exists", async () => {
vi.mocked(mockTranslationService.getWorkflowStepTranslation).mockResolvedValue({
translatedBody: "Cuerpo traducido",
translatedSubject: "Asunto traducido",
});
const result = await emailWorkflowService.generateEmailPayloadForEvtWorkflow({
evt: mockBookingInfo,
sendTo: ["attendee@example.com"],
hideBranding: false,
emailSubject: "Original Subject",
emailBody: "Original Body",
sender: "Cal.com",
action: WorkflowActions.EMAIL_ATTENDEE,
template: WorkflowTemplates.CUSTOM,
includeCalendarEvent: false,
triggerEvent: WorkflowTriggerEvents.BEFORE_EVENT,
workflowStepId: 123,
autoTranslateEnabled: true,
sourceLocale: "en",
});
expect(mockTranslationService.getWorkflowStepTranslation).toHaveBeenCalledTimes(1);
expect(result.subject).toBe("Asunto traducido");
expect(result.html).toContain("Cuerpo traducido");
});
test("should use original content when autoTranslateEnabled is false", async () => {
const result = await emailWorkflowService.generateEmailPayloadForEvtWorkflow({
evt: mockBookingInfo,
sendTo: ["attendee@example.com"],
hideBranding: false,
emailSubject: "Original Subject",
emailBody: "Original Body",
sender: "Cal.com",
action: WorkflowActions.EMAIL_ATTENDEE,
template: WorkflowTemplates.CUSTOM,
includeCalendarEvent: false,
triggerEvent: WorkflowTriggerEvents.BEFORE_EVENT,
workflowStepId: 123,
autoTranslateEnabled: false,
});
expect(mockTranslationService.getWorkflowStepTranslation).not.toHaveBeenCalled();
expect(result.subject).toBe("Original Subject");
});
test("should fallback to original content when translation not found", async () => {
vi.mocked(mockTranslationService.getWorkflowStepTranslation).mockResolvedValue({
translatedBody: null,
translatedSubject: null,
});
const result = await emailWorkflowService.generateEmailPayloadForEvtWorkflow({
evt: mockBookingInfo,
sendTo: ["attendee@example.com"],
hideBranding: false,
emailSubject: "Original Subject",
emailBody: "Original Body",
sender: "Cal.com",
action: WorkflowActions.EMAIL_ATTENDEE,
template: WorkflowTemplates.CUSTOM,
includeCalendarEvent: false,
triggerEvent: WorkflowTriggerEvents.BEFORE_EVENT,
workflowStepId: 123,
autoTranslateEnabled: true,
sourceLocale: "en",
});
expect(mockTranslationService.getWorkflowStepTranslation).toHaveBeenCalledTimes(1);
expect(result.subject).toBe("Original Subject");
});
test("should not translate for non-attendee actions", async () => {
const result = await emailWorkflowService.generateEmailPayloadForEvtWorkflow({
evt: mockBookingInfo,
sendTo: ["organizer@example.com"],
hideBranding: false,
emailSubject: "Original Subject",
emailBody: "Original Body",
sender: "Cal.com",
action: WorkflowActions.EMAIL_HOST,
template: WorkflowTemplates.CUSTOM,
includeCalendarEvent: false,
triggerEvent: WorkflowTriggerEvents.BEFORE_EVENT,
workflowStepId: 123,
autoTranslateEnabled: true,
sourceLocale: "en",
});
expect(mockTranslationService.getWorkflowStepTranslation).not.toHaveBeenCalled();
expect(result.subject).toBe("Original Subject");
});
});
describe("generateEmailPayloadForEvtWorkflow - Cal Video meeting URL", () => {
const mockBookingInfoWithCalVideo = {
uid: "booking-cal-video-123",
bookerUrl: "https://cal.com",
title: "Test Meeting with Cal Video",
startTime: "2024-12-01T10:00:00Z",
endTime: "2024-12-01T11:00:00Z",
organizer: {
name: "Organizer Name",
email: "organizer@example.com",
timeZone: "UTC",
language: { locale: "en" },
timeFormat: TimeFormat.TWELVE_HOUR,
},
attendees: [
{
name: "Attendee Name",
email: "attendee@example.com",
timeZone: "UTC",
language: { locale: "en" },
},
],
videoCallData: {
type: "daily_video",
url: "https://test-org.daily.co/test-room-name",
id: "test-room-name",
password: "test-password",
},
location: "Cal Video",
};
test("should use Cal.com video URL instead of daily.co URL for REMINDER template when using Cal video", async () => {
const result = await emailWorkflowService.generateEmailPayloadForEvtWorkflow({
evt: mockBookingInfoWithCalVideo,
sendTo: ["attendee@example.com"],
hideBranding: false,
emailSubject: "Test Subject",
emailBody: "", // Empty body with REMINDER template triggers REMINDER template
sender: "Cal.com",
action: WorkflowActions.EMAIL_ATTENDEE,
template: WorkflowTemplates.REMINDER,
includeCalendarEvent: false,
triggerEvent: WorkflowTriggerEvents.BEFORE_EVENT,
});
// The meetingUrl should NOT contain daily.co
expect(result.html).not.toContain("daily.co");
// The meetingUrl should contain the Cal.com video URL format
expect(result.html).toContain("/video/booking-cal-video-123");
});
test("should use Cal.com video URL instead of daily.co URL for custom template when using Cal video", async () => {
const customEmailBody = "Join the meeting at {MEETING_URL}";
const result = await emailWorkflowService.generateEmailPayloadForEvtWorkflow({
evt: mockBookingInfoWithCalVideo,
sendTo: ["attendee@example.com"],
hideBranding: false,
emailSubject: "Test Subject",
emailBody: customEmailBody,
sender: "Cal.com",
action: WorkflowActions.EMAIL_ATTENDEE,
template: WorkflowTemplates.CUSTOM,
includeCalendarEvent: false,
triggerEvent: WorkflowTriggerEvents.BEFORE_EVENT,
});
// The meetingUrl should NOT contain daily.co
expect(result.html).not.toContain("daily.co");
// The meetingUrl should contain the Cal.com video URL format
expect(result.html).toContain("/video/booking-cal-video-123");
});
test("should use Cal.com video URL format when videoCallData.url contains daily.co domain", async () => {
const mockBookingInfoWithDailyCoUrl = {
...mockBookingInfoWithCalVideo,
videoCallData: {
type: "daily_video",
url: "https://some-org.daily.co/another-room-name",
id: "another-room-name",
password: "test-password",
},
};
const result = await emailWorkflowService.generateEmailPayloadForEvtWorkflow({
evt: mockBookingInfoWithDailyCoUrl,
sendTo: ["attendee@example.com"],
hideBranding: false,
emailSubject: "Test Subject",
emailBody: "", // Empty body with REMINDER template triggers REMINDER template
sender: "Cal.com",
action: WorkflowActions.EMAIL_ATTENDEE,
template: WorkflowTemplates.REMINDER,
includeCalendarEvent: false,
triggerEvent: WorkflowTriggerEvents.BEFORE_EVENT,
});
// Even though videoCallData.url contains daily.co, the meetingUrl should NOT contain it
expect(result.html).not.toContain("daily.co");
expect(result.html).not.toContain("some-org.daily.co");
expect(result.html).not.toContain("another-room-name");
// Should use Cal.com video URL instead
expect(result.html).toContain("/video/booking-cal-video-123");
});
});
});