From 8c9eb184639288d3fe0e07c00c5887f68e00da74 Mon Sep 17 00:00:00 2001 From: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> Date: Wed, 2 Apr 2025 11:16:26 -0400 Subject: [PATCH] chore: Add spam checking for workflow bodies (#18822) * Add akismet package to tasker * Create scanWorkflowBody task * Schedule workflow body scan * Add AKISMET_API_KEY .env * Auto lock user if spam is detected * Uncommit key * Add safe param to workflow step * Migration for safe field * Do not process workflow steps is `safe` is false * Update migration to set previous records to true * Address comments * Refactor `scheduleWorkflowNotifications` to accept an object * If new steps or editing old ones send to tasker * Call `scheduleWorkflowNotifications` in task * Fix `IS_SELF_HOSTED` * Remove unused function * Make `safe` optional in schema * Type fix * Revert "Make `safe` optional in schema" This reverts commit d0964702affa87c35562300301473d25635c565b. * Revert "Type fix" This reverts commit d9a031303269a2994ae46f576ab2a3d31e4d977b. * Type fixes * Type fixes * Address comments * Fix tests * Add tests * Update tests * Typo fix * Update `safe` to `verifiedAt` * feat: Compare workflow reminder bodies to default template (#19060) * Add `getTemplateForAction` function * Use `getTemplateForAction` when creating a new step * Use `getTemplateForAction` when action changes * Have `emailReminderTemplate` accept an object as a param * Rename `getTemplateForAction` to `getTemplateBodyForAction` * Simplify changing body when changing templates * Create `compareReminderBodyToTemplate` * In task, compare if reminderBody is a template * Linting * Add tests * refactor: `emailReminderTemplate` to accept object as param (#19288) * Add `getTemplateForAction` function * Use `getTemplateForAction` when creating a new step * Use `getTemplateForAction` when action changes * Have `emailReminderTemplate` accept an object as a param * Rename `getTemplateForAction` to `getTemplateBodyForAction` * Simplify changing body when changing templates * Create `compareReminderBodyToTemplate` * In task, compare if reminderBody is a template * Linting * Add tests * Refactor `scheduleEmailReminders` * Refactor `create.handler` for new workflows * Refactor `emailReminderManager` * Refactor `getEmailTemplateText` * Fix typo * Type fix - whatsapp plain text template imports * Type fix - no template found * Type fix - add `isBrandingDisabled` to `emailReminderTemplate` * Add workflow and user to prisma mock * Fix imports for akismet dependencies * Record user lock reason * Undo linting changes * Fix tests * New workflow, at verify created step * Handle if `SCANNING_WORKFLOW_STEPS` is toggled * Move `verifiedAt` checks to specific schedule functions - `scheduleWhatsappReminder` - `scheduleEmailReminder` - `scheduleSMSReminder` * Update logic * Do not fallback verifiedAt * Add comment to next.config.js --- .env.example | 2 + apps/web/next.config.js | 4 + apps/web/playwright/fixtures/workflows.ts | 12 +- .../utils/bookingScenario/bookingScenario.ts | 2 + .../roundRobinManualReassignment.ts | 2 + .../ee/round-robin/roundRobinReassignment.ts | 2 + .../workflows/api/scheduleEmailReminders.ts | 60 +++--- .../components/WorkflowDetailsPage.tsx | 3 +- .../components/WorkflowStepContainer.tsx | 195 +++++------------- .../ee/workflows/lib/actionHelperFunctions.ts | 39 ++++ .../lib/compareReminderBodyToTemplate.ts | 16 ++ .../ee/workflows/lib/getAllWorkflows.ts | 1 + .../lib/reminders/emailReminderManager.ts | 28 ++- .../lib/reminders/reminderScheduler.ts | 5 + .../reminders/scheduleMandatoryReminder.ts | 2 + .../lib/reminders/smsReminderManager.ts | 7 + .../templates/emailRatingTemplate.ts | 3 + .../templates/emailReminderTemplate.ts | 47 +++-- .../reminders/templates/plainTextTemplates.ts | 25 +++ .../templates/smsReminderTemplate.ts | 3 + .../lib/reminders/templates/whatsapp/index.ts | 8 +- .../whatsappEventCancelledTemplate.ts | 3 + .../whatsappEventCompletedTemplate.ts | 3 + .../whatsapp/whatsappEventReminderTemplate.ts | 3 + .../whatsappEventRescheduledTemplate.ts | 3 + .../lib/reminders/whatsappReminderManager.ts | 6 + .../compareReminderBodyToTemplate.test.ts | 126 +++++++++++ packages/features/ee/workflows/lib/types.ts | 1 + packages/features/package.json | 1 + packages/features/tasker/tasker.ts | 1 + packages/features/tasker/tasks/index.ts | 1 + .../tasker/tasks/scanWorkflowBody.test.ts | 175 ++++++++++++++++ .../features/tasker/tasks/scanWorkflowBody.ts | 152 ++++++++++++++ packages/lib/autoLock.ts | 11 +- packages/lib/constants.ts | 1 + .../migration.sql | 5 + packages/prisma/schema.prisma | 1 + .../workflows/activateEventType.handler.ts | 3 + .../viewer/workflows/create.handler.ts | 13 +- .../viewer/workflows/update.handler.ts | 96 +++++---- .../server/routers/viewer/workflows/util.ts | 42 ++-- turbo.json | 1 + yarn.lock | 24 ++- 43 files changed, 876 insertions(+), 262 deletions(-) create mode 100644 packages/features/ee/workflows/lib/compareReminderBodyToTemplate.ts create mode 100644 packages/features/ee/workflows/lib/reminders/templates/plainTextTemplates.ts create mode 100644 packages/features/ee/workflows/lib/test/compareReminderBodyToTemplate.test.ts create mode 100644 packages/features/tasker/tasks/scanWorkflowBody.test.ts create mode 100644 packages/features/tasker/tasks/scanWorkflowBody.ts create mode 100644 packages/prisma/migrations/20250123170615_add_workflowstep_safe/migration.sql diff --git a/.env.example b/.env.example index 338f651554..0fbdde4fad 100644 --- a/.env.example +++ b/.env.example @@ -424,6 +424,8 @@ DIRECTORY_IDS_TO_LOG= # Read more about it in the README.md NEXT_PUBLIC_SINGLE_ORG_SLUG= +AKISMET_API_KEY= + ## Env variables related to avoiding booking failures # Request for checking reservation would be attempted to send every these seconds if the request is stale at that time NEXT_PUBLIC_QUERY_RESERVATION_INTERVAL_SECONDS= diff --git a/apps/web/next.config.js b/apps/web/next.config.js index 61ba259665..bf49b6cfa3 100644 --- a/apps/web/next.config.js +++ b/apps/web/next.config.js @@ -184,6 +184,8 @@ const nextConfig = { "http-cookie-agent", // Dependencies of @ewsjs/xhr "rest-facade", "superagent-proxy", // Dependencies of @tryvital/vital-node + "superagent", // Dependencies of akismet + "formidable", // Dependencies of akismet ], experimental: { // externalize server-side node_modules with size > 1mb, to improve dev mode performance/RAM usage @@ -237,6 +239,8 @@ const nextConfig = { /(^@google-cloud\/spanner|^@mongodb-js\/zstd|^@sap\/hana-client\/extension\/Stream$|^@sap\/hana-client|^@sap\/hana-client$|^aws-crt|^aws4$|^better-sqlite3$|^bson-ext$|^cardinal$|^cloudflare:sockets$|^hdb-pool$|^ioredis$|^kerberos$|^mongodb-client-encryption$|^mysql$|^oracledb$|^pg-native$|^pg-query-stream$|^react-native-sqlite-storage$|^snappy\/package\.json$|^snappy$|^sql.js$|^sqlite3$|^typeorm-aurora-data-api-driver$)/, }) ); + + config.externals.push("formidable"); } config.plugins.push( diff --git a/apps/web/playwright/fixtures/workflows.ts b/apps/web/playwright/fixtures/workflows.ts index 228297dde7..cde5f365e5 100644 --- a/apps/web/playwright/fixtures/workflows.ts +++ b/apps/web/playwright/fixtures/workflows.ts @@ -29,7 +29,15 @@ export function createWorkflowPageFixture(page: Page) { page.getByText(trigger); await selectEventType("30 min"); } - await saveWorkflow(); + const workflow = await saveWorkflow(); + + for (const step of workflow.steps) { + await prisma.workflowStep.update({ + where: { id: step.id }, + data: { verifiedAt: new Date() }, + }); + } + await page.getByTestId("go-back-button").click(); }; @@ -38,6 +46,8 @@ export function createWorkflowPageFixture(page: Page) { await page.getByTestId("save-workflow").click(); const response = await submitPromise; expect(response.status()).toBe(200); + const responseData = await response.json(); + return responseData[0].result.data.json.workflow; }; const assertListCount = async (count: number) => { diff --git a/apps/web/test/utils/bookingScenario/bookingScenario.ts b/apps/web/test/utils/bookingScenario/bookingScenario.ts index d363c660f5..56059ab8ca 100644 --- a/apps/web/test/utils/bookingScenario/bookingScenario.ts +++ b/apps/web/test/utils/bookingScenario/bookingScenario.ts @@ -89,6 +89,7 @@ type InputWorkflow = { time?: number | null; timeUnit?: TimeUnit | null; sendTo?: string; + verifiedAt?: Date; }; type InputPayment = { @@ -642,6 +643,7 @@ async function addWorkflowsToDb(workflows: InputWorkflow[]) { id: createdWorkflow.id, }, }, + verifiedAt: workflow?.verifiedAt ?? new Date(), }, }); diff --git a/packages/features/ee/round-robin/roundRobinManualReassignment.ts b/packages/features/ee/round-robin/roundRobinManualReassignment.ts index 24d5de5779..1bf187a90a 100644 --- a/packages/features/ee/round-robin/roundRobinManualReassignment.ts +++ b/packages/features/ee/round-robin/roundRobinManualReassignment.ts @@ -430,6 +430,7 @@ async function handleWorkflowsUpdate({ reminderBody: true, sender: true, includeCalendarEvent: true, + verifiedAt: true, }, }, }, @@ -464,6 +465,7 @@ async function handleWorkflowsUpdate({ hideBranding: true, includeCalendarEvent: workflowStep.includeCalendarEvent, workflowStepId: workflowStep.id, + verifiedAt: workflowStep.verifiedAt, }); } diff --git a/packages/features/ee/round-robin/roundRobinReassignment.ts b/packages/features/ee/round-robin/roundRobinReassignment.ts index 8e00628e36..6eab627a9b 100644 --- a/packages/features/ee/round-robin/roundRobinReassignment.ts +++ b/packages/features/ee/round-robin/roundRobinReassignment.ts @@ -467,6 +467,7 @@ export const roundRobinReassignment = async ({ reminderBody: true, sender: true, includeCalendarEvent: true, + verifiedAt: true, }, }, }, @@ -502,6 +503,7 @@ export const roundRobinReassignment = async ({ hideBranding: true, includeCalendarEvent: workflowStep.includeCalendarEvent, workflowStepId: workflowStep.id, + verifiedAt: workflowStep.verifiedAt, }); } diff --git a/packages/features/ee/workflows/api/scheduleEmailReminders.ts b/packages/features/ee/workflows/api/scheduleEmailReminders.ts index 4b411f4a5b..27d8194bcf 100644 --- a/packages/features/ee/workflows/api/scheduleEmailReminders.ts +++ b/packages/features/ee/workflows/api/scheduleEmailReminders.ts @@ -244,21 +244,21 @@ export async function handler(req: NextRequest) { ? !!reminder.booking.eventType?.team?.hideBranding : !!reminder.booking.user?.hideBranding; - emailContent = emailReminderTemplate( - false, - reminder.booking.user?.locale || "en", - reminder.workflowStep.action, - getTimeFormatStringFromUserTimeFormat(reminder.booking.user?.timeFormat), - reminder.booking.startTime.toISOString() || "", - reminder.booking.endTime.toISOString() || "", - reminder.booking.eventType?.title || "", - timeZone || "", - reminder.booking.location || "", - bookingMetadataSchema.parse(reminder.booking.metadata || {})?.videoCallUrl || "", - attendeeName || "", - name || "", - brandingDisabled - ); + emailContent = emailReminderTemplate({ + isEditingMode: false, + locale: reminder.booking.user?.locale || "en", + action: reminder.workflowStep.action, + timeFormat: getTimeFormatStringFromUserTimeFormat(reminder.booking.user?.timeFormat), + startTime: reminder.booking.startTime.toISOString() || "", + endTime: reminder.booking.endTime.toISOString() || "", + eventName: reminder.booking.eventType?.title || "", + timeZone: timeZone || "", + location: reminder.booking.location || "", + meetingUrl: bookingMetadataSchema.parse(reminder.booking.metadata || {})?.videoCallUrl || "", + otherPerson: attendeeName || "", + name: name || "", + isBrandingDisabled: brandingDisabled, + }); } else if (reminder.workflowStep.template === WorkflowTemplates.RATING) { const organizerOrganizationProfile = await prisma.profile.findFirst({ where: { @@ -380,21 +380,21 @@ export async function handler(req: NextRequest) { ? !!reminder.booking.eventType?.team?.hideBranding : !!reminder.booking.user?.hideBranding; - emailContent = emailReminderTemplate( - false, - reminder.booking.user?.locale || "en", - WorkflowActions.EMAIL_ATTENDEE, - getTimeFormatStringFromUserTimeFormat(reminder.booking.user?.timeFormat), - reminder.booking.startTime.toISOString() || "", - reminder.booking.endTime.toISOString() || "", - reminder.booking.eventType?.title || "", - timeZone || "", - reminder.booking.location || "", - bookingMetadataSchema.parse(reminder.booking.metadata || {})?.videoCallUrl || "", - attendeeName || "", - name || "", - brandingDisabled - ); + emailContent = emailReminderTemplate({ + isEditingMode: false, + locale: reminder.booking.user?.locale || "en", + action: WorkflowActions.EMAIL_ATTENDEE, + timeFormat: getTimeFormatStringFromUserTimeFormat(reminder.booking.user?.timeFormat), + startTime: reminder.booking.startTime.toISOString() || "", + endTime: reminder.booking.endTime.toISOString() || "", + eventName: reminder.booking.eventType?.title || "", + timeZone: timeZone || "", + location: reminder.booking.location || "", + meetingUrl: bookingMetadataSchema.parse(reminder.booking.metadata || {})?.videoCallUrl || "", + otherPerson: attendeeName || "", + name: name || "", + isBrandingDisabled: brandingDisabled, + }); if (emailContent.emailSubject.length > 0 && !emailBodyEmpty && sendTo) { const batchId = await getBatchId(); diff --git a/packages/features/ee/workflows/components/WorkflowDetailsPage.tsx b/packages/features/ee/workflows/components/WorkflowDetailsPage.tsx index d93fd79e3b..bb7aa5c6f8 100644 --- a/packages/features/ee/workflows/components/WorkflowDetailsPage.tsx +++ b/packages/features/ee/workflows/components/WorkflowDetailsPage.tsx @@ -4,7 +4,7 @@ import { useState, useEffect } from "react"; import type { UseFormReturn } from "react-hook-form"; import { Controller } from "react-hook-form"; -import { SENDER_ID, SENDER_NAME } from "@calcom/lib/constants"; +import { SENDER_ID, SENDER_NAME, SCANNING_WORKFLOW_STEPS } from "@calcom/lib/constants"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import type { WorkflowActions } from "@calcom/prisma/enums"; import { WorkflowTemplates } from "@calcom/prisma/enums"; @@ -92,6 +92,7 @@ export default function WorkflowDetailsPage(props: Props) { senderName: !isSMSAction(action) ? senderName || SENDER_NAME : SENDER_NAME, numberVerificationPending: false, includeCalendarEvent: false, + verifiedAt: SCANNING_WORKFLOW_STEPS ? null : new Date(), }; steps?.push(step); form.setValue("steps", steps); diff --git a/packages/features/ee/workflows/components/WorkflowStepContainer.tsx b/packages/features/ee/workflows/components/WorkflowStepContainer.tsx index 4dfc89370d..dcb4c64491 100644 --- a/packages/features/ee/workflows/components/WorkflowStepContainer.tsx +++ b/packages/features/ee/workflows/components/WorkflowStepContainer.tsx @@ -41,18 +41,15 @@ import { showToast } from "@calcom/ui/components/toast"; import { Tooltip } from "@calcom/ui/components/tooltip"; import { - getWhatsappTemplateForAction, - isAttendeeAction, isSMSAction, - isSMSOrWhatsappAction, isWhatsappAction, + getTemplateBodyForAction, + shouldScheduleEmailReminder, } from "../lib/actionHelperFunctions"; import { DYNAMIC_TEXT_VARIABLES } from "../lib/constants"; import { getWorkflowTemplateOptions, getWorkflowTriggerOptions } from "../lib/getOptions"; import emailRatingTemplate from "../lib/reminders/templates/emailRatingTemplate"; import emailReminderTemplate from "../lib/reminders/templates/emailReminderTemplate"; -import smsReminderTemplate from "../lib/reminders/templates/smsReminderTemplate"; -import { whatsappReminderTemplate } from "../lib/reminders/templates/whatsapp"; import type { FormValues } from "../pages/workflow"; import { TimeTimeUnitInput } from "./TimeTimeUnitInput"; @@ -131,37 +128,25 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { const { data: actionOptions } = trpc.viewer.workflows.getWorkflowActionOptions.useQuery(); const triggerOptions = getWorkflowTriggerOptions(t); const templateOptions = getWorkflowTemplateOptions(t, step?.action, hasActiveTeamPlan); + if (step && !form.getValues(`steps.${step.stepNumber - 1}.reminderBody`)) { + const action = form.getValues(`steps.${step.stepNumber - 1}.action`); + const template = getTemplateBodyForAction({ + action, + locale: i18n.language, + template: step.template ?? WorkflowTemplates.REMINDER, + timeFormat, + }); + form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, template); + } - if (step && form.getValues(`steps.${step.stepNumber - 1}.template`) === WorkflowTemplates.REMINDER) { - if (!form.getValues(`steps.${step.stepNumber - 1}.reminderBody`)) { - const action = form.getValues(`steps.${step.stepNumber - 1}.action`); - if (isSMSAction(action)) { - form.setValue( - `steps.${step.stepNumber - 1}.reminderBody`, - smsReminderTemplate(true, i18n.language, action, timeFormat) - ); - } else if (isWhatsappAction(action)) { - form.setValue( - `steps.${step.stepNumber - 1}.reminderBody`, - whatsappReminderTemplate(true, i18n.language, action, timeFormat) - ); - } else { - const reminderBodyTemplate = emailReminderTemplate(true, i18n.language, action, timeFormat).emailBody; - form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, reminderBodyTemplate); - } - } - if (!form.getValues(`steps.${step.stepNumber - 1}.emailSubject`)) { - const subjectTemplate = emailReminderTemplate( - true, - i18n.language, - form.getValues(`steps.${step.stepNumber - 1}.action`), - timeFormat - ).emailSubject; - form.setValue(`steps.${step.stepNumber - 1}.emailSubject`, subjectTemplate); - } - } else if (step && isWhatsappAction(step.action)) { - const templateBody = getWhatsappTemplateForAction(step.action, i18n.language, step.template, timeFormat); - form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, templateBody); + if (step && !form.getValues(`steps.${step.stepNumber - 1}.emailSubject`)) { + const subjectTemplate = emailReminderTemplate({ + isEditingMode: true, + locale: i18n.language, + action: form.getValues(`steps.${step.stepNumber - 1}.action`), + timeFormat, + }).emailSubject; + form.setValue(`steps.${step.stepNumber - 1}.emailSubject`, subjectTemplate); } const { ref: emailSubjectFormRef, ...restEmailSubjectForm } = step @@ -455,6 +440,15 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { if (val) { const oldValue = form.getValues(`steps.${step.stepNumber - 1}.action`); + const template = getTemplateBodyForAction({ + action: val.value, + locale: i18n.language, + template: WorkflowTemplates.REMINDER, + timeFormat, + }); + + form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, template); + const setNumberRequiredConfigs = ( phoneNumberIsNeeded: boolean, senderNeeded = true @@ -469,7 +463,6 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { setNumberRequiredConfigs(val.value === WorkflowActions.SMS_NUMBER); // email action changes to sms action if (!isSMSAction(oldValue)) { - form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, ""); form.setValue(`steps.${step.stepNumber - 1}.sender`, SENDER_ID); } @@ -478,7 +471,6 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { setNumberRequiredConfigs(val.value === WorkflowActions.WHATSAPP_NUMBER, false); if (!isWhatsappAction(oldValue)) { - form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, ""); form.setValue(`steps.${step.stepNumber - 1}.sender`, ""); } @@ -490,65 +482,6 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { setIsEmailSubjectNeeded(true); } - if ( - form.getValues(`steps.${step.stepNumber - 1}.template`) === - WorkflowTemplates.REMINDER - ) { - if (isSMSOrWhatsappAction(val.value) === isSMSOrWhatsappAction(oldValue)) { - if (isAttendeeAction(oldValue) !== isAttendeeAction(val.value)) { - const currentReminderBody = - form.getValues(`steps.${step.stepNumber - 1}.reminderBody`) || ""; - const newReminderBody = currentReminderBody - .replaceAll("{ORGANIZER}", "{PLACEHOLDER}") - .replaceAll("{ATTENDEE}", "{ORGANIZER}") - .replaceAll("{PLACEHOLDER}", "{ATTENDEE}"); - form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, newReminderBody); - - if (!isSMSOrWhatsappAction(val.value)) { - const currentEmailSubject = - form.getValues(`steps.${step.stepNumber - 1}.emailSubject`) || ""; - const newEmailSubject = isAttendeeAction(val.value) - ? currentEmailSubject.replace("{ORGANIZER}", "{ATTENDEE}") - : currentEmailSubject.replace("{ATTENDEE}", "{ORGANIZER}"); - - form.setValue( - `steps.${step.stepNumber - 1}.emailSubject`, - newEmailSubject || "" - ); - } - } - } else { - if (isSMSAction(val.value)) { - form.setValue( - `steps.${step.stepNumber - 1}.reminderBody`, - smsReminderTemplate(true, i18n.language, val.value, timeFormat) - ); - } else if (isWhatsappAction(val.value)) { - form.setValue( - `steps.${step.stepNumber - 1}.reminderBody`, - whatsappReminderTemplate(true, i18n.language, val.value, timeFormat) - ); - } else { - const emailReminderBody = emailReminderTemplate( - true, - i18n.language, - val.value, - timeFormat - ); - form.setValue( - `steps.${step.stepNumber - 1}.reminderBody`, - emailReminderBody.emailBody - ); - form.setValue( - `steps.${step.stepNumber - 1}.emailSubject`, - emailReminderBody.emailSubject - ); - } - } - } else { - const template = isWhatsappAction(val.value) ? "REMINDER" : "CUSTOM"; - template && form.setValue(`steps.${step.stepNumber - 1}.template`, template); - } form.unregister(`steps.${step.stepNumber - 1}.sendTo`); form.clearErrors(`steps.${step.stepNumber - 1}.sendTo`); form.setValue(`steps.${step.stepNumber - 1}.action`, val.value); @@ -825,55 +758,37 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { onChange={(val) => { if (val) { const action = form.getValues(`steps.${step.stepNumber - 1}.action`); - if (val.value === WorkflowTemplates.REMINDER) { - if (isWhatsappAction(action)) { - form.setValue( - `steps.${step.stepNumber - 1}.reminderBody`, - whatsappReminderTemplate(true, i18n.language, action, timeFormat) - ); - } else if (isSMSAction(action)) { - form.setValue( - `steps.${step.stepNumber - 1}.reminderBody`, - smsReminderTemplate(true, i18n.language, action, timeFormat) - ); - } else { - form.setValue( - `steps.${step.stepNumber - 1}.reminderBody`, - emailReminderTemplate(true, i18n.language, action, timeFormat).emailBody - ); + + const template = getTemplateBodyForAction({ + action, + locale: i18n.language, + template: val.value ?? WorkflowTemplates.REMINDER, + timeFormat, + }); + + form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, template); + + if (shouldScheduleEmailReminder(action)) { + if (val.value === WorkflowTemplates.REMINDER) { form.setValue( `steps.${step.stepNumber - 1}.emailSubject`, - emailReminderTemplate(true, i18n.language, action, timeFormat).emailSubject + emailReminderTemplate({ + isEditingMode: true, + locale: i18n.language, + action, + timeFormat, + }).emailSubject ); - } - } else if (val.value === WorkflowTemplates.RATING) { - form.setValue( - `steps.${step.stepNumber - 1}.reminderBody`, - emailRatingTemplate({ - isEditingMode: true, - locale: i18n.language, - action, - timeFormat, - }).emailBody - ); - form.setValue( - `steps.${step.stepNumber - 1}.emailSubject`, - emailRatingTemplate({ - isEditingMode: true, - locale: i18n.language, - action, - timeFormat, - }).emailSubject - ); - } else { - if (isWhatsappAction(action)) { + } else if (val.value === WorkflowTemplates.RATING) { form.setValue( - `steps.${step.stepNumber - 1}.reminderBody`, - getWhatsappTemplateForAction(action, i18n.language, val.value, timeFormat) + `steps.${step.stepNumber - 1}.emailSubject`, + emailRatingTemplate({ + isEditingMode: true, + locale: i18n.language, + action, + timeFormat, + }).emailSubject ); - } else { - form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, ""); - form.setValue(`steps.${step.stepNumber - 1}.emailSubject`, ""); } } field.onChange(val.value); diff --git a/packages/features/ee/workflows/lib/actionHelperFunctions.ts b/packages/features/ee/workflows/lib/actionHelperFunctions.ts index 4fdd8672d1..8cb8c7b9e1 100644 --- a/packages/features/ee/workflows/lib/actionHelperFunctions.ts +++ b/packages/features/ee/workflows/lib/actionHelperFunctions.ts @@ -9,6 +9,9 @@ import { whatsappEventRescheduledTemplate, whatsappReminderTemplate, } from "../lib/reminders/templates/whatsapp"; +import emailRatingTemplate from "./reminders/templates/emailRatingTemplate"; +import emailReminderTemplate from "./reminders/templates/emailReminderTemplate"; +import smsReminderTemplate from "./reminders/templates/smsReminderTemplate"; export function shouldScheduleEmailReminder(action: WorkflowActions) { return action === WorkflowActions.EMAIL_ATTENDEE || action === WorkflowActions.EMAIL_HOST; @@ -86,6 +89,17 @@ export function getWhatsappTemplateFunction(template?: WorkflowTemplates): typeo } } +function getEmailTemplateFunction(template?: WorkflowTemplates) { + switch (template) { + case WorkflowTemplates.REMINDER: + return emailReminderTemplate; + case WorkflowTemplates.RATING: + return emailRatingTemplate; + default: + return emailReminderTemplate; + } +} + export function getWhatsappTemplateForAction( action: WorkflowActions, locale: string, @@ -95,3 +109,28 @@ export function getWhatsappTemplateForAction( const templateFunction = getWhatsappTemplateFunction(template); return templateFunction(true, locale, action, timeFormat); } + +export function getTemplateBodyForAction({ + action, + locale, + template, + timeFormat, +}: { + action: WorkflowActions; + locale: string; + template: WorkflowTemplates; + timeFormat: TimeFormat; +}): string | null { + if (isSMSAction(action)) { + return smsReminderTemplate(true, locale, action, timeFormat); + } + + if (isWhatsappAction(action)) { + const templateFunction = getWhatsappTemplateFunction(template); + return templateFunction(true, locale, action, timeFormat); + } + + // If not a whatsapp action then it's an email action + const templateFunction = getEmailTemplateFunction(template); + return templateFunction({ isEditingMode: true, locale, action, timeFormat }).emailBody; +} diff --git a/packages/features/ee/workflows/lib/compareReminderBodyToTemplate.ts b/packages/features/ee/workflows/lib/compareReminderBodyToTemplate.ts new file mode 100644 index 0000000000..0ef39955e5 --- /dev/null +++ b/packages/features/ee/workflows/lib/compareReminderBodyToTemplate.ts @@ -0,0 +1,16 @@ +const compareReminderBodyToTemplate = ({ + reminderBody, + template, +}: { + reminderBody: string; + template: string; +}) => { + const stripHTML = (html: string) => html.replace(/<[^>]+>/g, "").replace(/&/g, "&"); + + const stripedReminderBody = stripHTML(reminderBody); + const stripedTemplate = stripHTML(template); + + return stripedReminderBody === stripedTemplate; +}; + +export default compareReminderBodyToTemplate; diff --git a/packages/features/ee/workflows/lib/getAllWorkflows.ts b/packages/features/ee/workflows/lib/getAllWorkflows.ts index 5d6b20e6c9..569194255c 100644 --- a/packages/features/ee/workflows/lib/getAllWorkflows.ts +++ b/packages/features/ee/workflows/lib/getAllWorkflows.ts @@ -22,6 +22,7 @@ export const workflowSelect = { sender: true, includeCalendarEvent: true, numberRequired: true, + verifiedAt: true, }, }, }; diff --git a/packages/features/ee/workflows/lib/reminders/emailReminderManager.ts b/packages/features/ee/workflows/lib/reminders/emailReminderManager.ts index 1cc6453b2a..dbb749b3a5 100644 --- a/packages/features/ee/workflows/lib/reminders/emailReminderManager.ts +++ b/packages/features/ee/workflows/lib/reminders/emailReminderManager.ts @@ -54,6 +54,7 @@ interface scheduleEmailReminderArgs extends ScheduleReminderArgs { hideBranding?: boolean; includeCalendarEvent?: boolean; isMandatoryReminder?: boolean; + verifiedAt: Date | null; } export const scheduleEmailReminder = async (args: scheduleEmailReminderArgs) => { @@ -72,7 +73,14 @@ export const scheduleEmailReminder = async (args: scheduleEmailReminderArgs) => includeCalendarEvent, isMandatoryReminder, action, + verifiedAt, } = args; + + if (!verifiedAt) { + log.warn(`Workflow step ${workflowStepId} not yet verified`); + return; + } + const { startTime, endTime } = evt; const uid = evt.uid as string; const currentDate = dayjs(); @@ -185,20 +193,20 @@ export const scheduleEmailReminder = async (args: scheduleEmailReminderArgs) => hideBranding ).html; } else if (template === WorkflowTemplates.REMINDER) { - emailContent = emailReminderTemplate( - false, - evt.organizer.language.locale, + emailContent = emailReminderTemplate({ + isEditingMode: false, + locale: evt.organizer.language.locale, action, - evt.organizer.timeFormat, + timeFormat: evt.organizer.timeFormat, startTime, endTime, - evt.title, + eventName: evt.title, timeZone, - evt.location || "", - bookingMetadataSchema.parse(evt.metadata || {})?.videoCallUrl || "", - attendeeName, - name - ); + location: evt.location || "", + meetingUrl: bookingMetadataSchema.parse(evt.metadata || {})?.videoCallUrl || "", + otherPerson: attendeeName, + name, + }); } else if (template === WorkflowTemplates.RATING) { emailContent = emailRatingTemplate({ isEditingMode: true, diff --git a/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts b/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts index 95344e854f..01ec8d8c0f 100644 --- a/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts +++ b/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts @@ -54,6 +54,8 @@ const processWorkflowStep = async ( seatReferenceUid, }: ProcessWorkflowStepParams ) => { + if (!step?.verifiedAt) return; + if (isSMSOrWhatsappAction(step.action)) { await checkSMSRateLimit({ identifier: `sms:${workflow.teamId ? "team:" : "user:"}${workflow.teamId || workflow.userId}`, @@ -80,6 +82,7 @@ const processWorkflowStep = async ( teamId: workflow.teamId, isVerificationPending: step.numberVerificationPending, seatReferenceUid, + verifiedAt: step.verifiedAt, }); } else if ( step.action === WorkflowActions.EMAIL_ATTENDEE || @@ -147,6 +150,7 @@ const processWorkflowStep = async ( hideBranding, seatReferenceUid, includeCalendarEvent: step.includeCalendarEvent, + verifiedAt: step.verifiedAt, }); } else if (isWhatsappAction(step.action)) { const sendTo = step.action === WorkflowActions.WHATSAPP_ATTENDEE ? smsReminderNumber : step.sendTo; @@ -166,6 +170,7 @@ const processWorkflowStep = async ( teamId: workflow.teamId, isVerificationPending: step.numberVerificationPending, seatReferenceUid, + verifiedAt: step.verifiedAt, }); } }; diff --git a/packages/features/ee/workflows/lib/reminders/scheduleMandatoryReminder.ts b/packages/features/ee/workflows/lib/reminders/scheduleMandatoryReminder.ts index a8b3ec15a1..e3c450de46 100644 --- a/packages/features/ee/workflows/lib/reminders/scheduleMandatoryReminder.ts +++ b/packages/features/ee/workflows/lib/reminders/scheduleMandatoryReminder.ts @@ -63,6 +63,8 @@ export async function scheduleMandatoryReminder({ seatReferenceUid, includeCalendarEvent: false, isMandatoryReminder: true, + // Template is fixed so we don't have to verify + verifiedAt: new Date(), }); } catch (error) { log.error("Error while scheduling mandatory reminders", JSON.stringify({ error })); diff --git a/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts b/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts index c288d64216..25b9aff77c 100644 --- a/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts +++ b/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts @@ -74,6 +74,7 @@ export interface ScheduleTextReminderArgs extends ScheduleReminderArgs { teamId?: number | null; isVerificationPending?: boolean; prisma?: PrismaClient; + verifiedAt: Date | null; } export const scheduleSMSReminder = async (args: ScheduleTextReminderArgs) => { @@ -91,8 +92,14 @@ export const scheduleSMSReminder = async (args: ScheduleTextReminderArgs) => { teamId, isVerificationPending = false, seatReferenceUid, + verifiedAt, } = args; + if (!verifiedAt) { + log.warn(`Workflow step ${workflowStepId} not yet verified`); + return; + } + const { startTime, endTime } = evt; const uid = evt.uid as string; const currentDate = dayjs(); diff --git a/packages/features/ee/workflows/lib/reminders/templates/emailRatingTemplate.ts b/packages/features/ee/workflows/lib/reminders/templates/emailRatingTemplate.ts index a255a5dabf..1772ec96a1 100644 --- a/packages/features/ee/workflows/lib/reminders/templates/emailRatingTemplate.ts +++ b/packages/features/ee/workflows/lib/reminders/templates/emailRatingTemplate.ts @@ -79,3 +79,6 @@ const emailRatingTemplate = ({ }; export default emailRatingTemplate; + +export const plainTextTemplate = + "Hi {ORGANIZER},We're always looking to improve our customer's experience. How satisfied were you with your recent meeting?😠 🙁 😐 😄 😍{ORGANIZER} didn't join the meeting? Reschedule hereEvent: {EVENT_NAME}Date & Time: {EVENT_DATE_ddd, MMM D, YYYY h:mma} - {EVENT_END_TIME} ({TIMEZONE})Attendees: You & {ORGANIZER}This survey was triggered by a Workflow in Cal."; diff --git a/packages/features/ee/workflows/lib/reminders/templates/emailReminderTemplate.ts b/packages/features/ee/workflows/lib/reminders/templates/emailReminderTemplate.ts index 5cd183fae0..bba9330358 100644 --- a/packages/features/ee/workflows/lib/reminders/templates/emailReminderTemplate.ts +++ b/packages/features/ee/workflows/lib/reminders/templates/emailReminderTemplate.ts @@ -4,21 +4,35 @@ import { APP_NAME } from "@calcom/lib/constants"; import { TimeFormat } from "@calcom/lib/timeFormat"; import { WorkflowActions } from "@calcom/prisma/enums"; -const emailReminderTemplate = ( - isEditingMode: boolean, - locale: string, - action?: WorkflowActions, - timeFormat?: TimeFormat, - startTime?: string, - endTime?: string, - eventName?: string, - timeZone?: string, - location?: string, - meetingUrl?: string, - otherPerson?: string, - name?: string, - isBrandingDisabled?: boolean -) => { +const emailReminderTemplate = ({ + isEditingMode, + locale, + action, + timeFormat, + startTime, + endTime, + eventName, + timeZone, + location, + meetingUrl, + otherPerson, + name, + isBrandingDisabled, +}: { + isEditingMode: boolean; + locale: string; + action?: WorkflowActions; + timeFormat?: TimeFormat; + startTime?: string; + endTime?: string; + eventName?: string; + timeZone?: string; + location?: string; + meetingUrl?: string; + otherPerson?: string; + name?: string; + isBrandingDisabled?: boolean; +}) => { const currentTimeFormat = timeFormat || TimeFormat.TWELVE_HOUR; const dateTimeFormat = `ddd, MMM D, YYYY ${currentTimeFormat}`; @@ -63,3 +77,6 @@ const emailReminderTemplate = ( }; export default emailReminderTemplate; + +export const plainTextTemplate = + "Hi {ORGANIZER},This is a reminder about your upcoming event.Event: {EVENT_NAME}Date & Time: {EVENT_DATE_ddd, MMM D, YYYY h:mma} - {EVENT_END_TIME} ({TIMEZONE})Attendees: You & {ATTENDEE}Location: {LOCATION} {MEETING_URL}This reminder was triggered by a Workflow in Cal."; diff --git a/packages/features/ee/workflows/lib/reminders/templates/plainTextTemplates.ts b/packages/features/ee/workflows/lib/reminders/templates/plainTextTemplates.ts new file mode 100644 index 0000000000..1d8d788daf --- /dev/null +++ b/packages/features/ee/workflows/lib/reminders/templates/plainTextTemplates.ts @@ -0,0 +1,25 @@ +import { plainTextTemplate as plainTextEmailRatingTemplate } from "./emailRatingTemplate"; +import { plainTextTemplate as plainTextEmailReminderTemplate } from "./emailReminderTemplate"; +import { plainTextTemplate as plainTextSMSReminderTemplate } from "./smsReminderTemplate"; +import { plainTextTemplate as plainTextWhatsappCanceledTemplate } from "./whatsapp/whatsappEventCancelledTemplate"; +import { plainTextTemplate as plainTextWhatsappCompletedTemplate } from "./whatsapp/whatsappEventCompletedTemplate"; +import { plainTextTemplate as plainTextWhatsappReminderTemplate } from "./whatsapp/whatsappEventReminderTemplate"; +import { plainTextTemplate as plainTextWhatsappRescheduledTemplate } from "./whatsapp/whatsappEventRescheduledTemplate"; + +const plainTextTemplates = { + email: { + reminder: plainTextEmailReminderTemplate, + rating: plainTextEmailRatingTemplate, + }, + sms: { + reminder: plainTextSMSReminderTemplate, + }, + whatsapp: { + reminder: plainTextWhatsappReminderTemplate, + rescheduled: plainTextWhatsappRescheduledTemplate, + completed: plainTextWhatsappCompletedTemplate, + canceled: plainTextWhatsappCanceledTemplate, + }, +}; + +export default plainTextTemplates; diff --git a/packages/features/ee/workflows/lib/reminders/templates/smsReminderTemplate.ts b/packages/features/ee/workflows/lib/reminders/templates/smsReminderTemplate.ts index be3ab1c5be..10295fede1 100644 --- a/packages/features/ee/workflows/lib/reminders/templates/smsReminderTemplate.ts +++ b/packages/features/ee/workflows/lib/reminders/templates/smsReminderTemplate.ts @@ -45,3 +45,6 @@ const smsReminderTemplate = ( }; export default smsReminderTemplate; + +export const plainTextTemplate = + "Hi {ATTENDEE}, this is a reminder that your meeting ({EVENT_NAME}) with {ORGANIZER} is on {EVENT_DATE_YYYY MMM D} at {EVENT_TIME_h:mma} {TIMEZONE}."; diff --git a/packages/features/ee/workflows/lib/reminders/templates/whatsapp/index.ts b/packages/features/ee/workflows/lib/reminders/templates/whatsapp/index.ts index 36b6eb63e7..6d8c4bce09 100644 --- a/packages/features/ee/workflows/lib/reminders/templates/whatsapp/index.ts +++ b/packages/features/ee/workflows/lib/reminders/templates/whatsapp/index.ts @@ -1,4 +1,4 @@ -export * from "./whatsappEventCancelledTemplate"; -export * from "./whatsappEventCompletedTemplate"; -export * from "./whatsappEventReminderTemplate"; -export * from "./whatsappEventRescheduledTemplate"; +export { whatsappEventCancelledTemplate } from "./whatsappEventCancelledTemplate"; +export { whatsappEventCompletedTemplate } from "./whatsappEventCompletedTemplate"; +export { whatsappReminderTemplate } from "./whatsappEventReminderTemplate"; +export { whatsappEventRescheduledTemplate } from "./whatsappEventRescheduledTemplate"; diff --git a/packages/features/ee/workflows/lib/reminders/templates/whatsapp/whatsappEventCancelledTemplate.ts b/packages/features/ee/workflows/lib/reminders/templates/whatsapp/whatsappEventCancelledTemplate.ts index 925d309700..138d68793a 100644 --- a/packages/features/ee/workflows/lib/reminders/templates/whatsapp/whatsappEventCancelledTemplate.ts +++ b/packages/features/ee/workflows/lib/reminders/templates/whatsapp/whatsappEventCancelledTemplate.ts @@ -39,3 +39,6 @@ export const whatsappEventCancelledTemplate = ( return null; }; + +export const plainTextTemplate = + "Hi {ATTENDEE}, your meeting (*{EVENT_NAME}*) with {ORGANIZER} on {EVENT_DATE_ddd, MMM D, YYYY h:mma} at {START_TIME_h:mma} {TIMEZONE} has been canceled."; diff --git a/packages/features/ee/workflows/lib/reminders/templates/whatsapp/whatsappEventCompletedTemplate.ts b/packages/features/ee/workflows/lib/reminders/templates/whatsapp/whatsappEventCompletedTemplate.ts index 27a00d53c6..1d54dc3392 100644 --- a/packages/features/ee/workflows/lib/reminders/templates/whatsapp/whatsappEventCompletedTemplate.ts +++ b/packages/features/ee/workflows/lib/reminders/templates/whatsapp/whatsappEventCompletedTemplate.ts @@ -39,3 +39,6 @@ export const whatsappEventCompletedTemplate = ( return null; }; + +export const plainTextTemplate = + "Hi {ATTENDEE}, thank you for attending the event (*{EVENT_NAME}*) on {EVENT_DATE_ddd, MMM D, YYYY h:mma} at {START_TIME_h:mma} {TIMEZONE}."; diff --git a/packages/features/ee/workflows/lib/reminders/templates/whatsapp/whatsappEventReminderTemplate.ts b/packages/features/ee/workflows/lib/reminders/templates/whatsapp/whatsappEventReminderTemplate.ts index 443dfdb7d3..80166e0766 100644 --- a/packages/features/ee/workflows/lib/reminders/templates/whatsapp/whatsappEventReminderTemplate.ts +++ b/packages/features/ee/workflows/lib/reminders/templates/whatsapp/whatsappEventReminderTemplate.ts @@ -39,3 +39,6 @@ export const whatsappReminderTemplate = ( return null; }; + +export const plainTextTemplate = + "Hi {ATTENDEE}, this is a reminder that your meeting (*{EVENT_NAME}*) with {ORGANIZER} is on {EVENT_DATE_ddd, MMM D, YYYY h:mma} at {START_TIME_h:mma} {TIMEZONE}."; diff --git a/packages/features/ee/workflows/lib/reminders/templates/whatsapp/whatsappEventRescheduledTemplate.ts b/packages/features/ee/workflows/lib/reminders/templates/whatsapp/whatsappEventRescheduledTemplate.ts index 6e28495540..60b256d99a 100644 --- a/packages/features/ee/workflows/lib/reminders/templates/whatsapp/whatsappEventRescheduledTemplate.ts +++ b/packages/features/ee/workflows/lib/reminders/templates/whatsapp/whatsappEventRescheduledTemplate.ts @@ -39,3 +39,6 @@ export const whatsappEventRescheduledTemplate = ( return null; }; + +export const plainTextTemplate = + "Hi {ATTENDEE}, your meeting (*{EVENT_NAME}*) with {ORGANIZER} on {EVENT_DATE_ddd, MMM D, YYYY h:mma} at {START_TIME_h:mma} {TIMEZONE} has been rescheduled."; diff --git a/packages/features/ee/workflows/lib/reminders/whatsappReminderManager.ts b/packages/features/ee/workflows/lib/reminders/whatsappReminderManager.ts index fd8bdb3ff4..a76f4ebf0c 100644 --- a/packages/features/ee/workflows/lib/reminders/whatsappReminderManager.ts +++ b/packages/features/ee/workflows/lib/reminders/whatsappReminderManager.ts @@ -34,8 +34,14 @@ export const scheduleWhatsappReminder = async (args: ScheduleTextReminderArgs) = teamId, isVerificationPending = false, seatReferenceUid, + verifiedAt, } = args; + if (!verifiedAt) { + log.warn(`Workflow step ${workflowStepId} not verified`); + return; + } + const { startTime, endTime } = evt; const uid = evt.uid as string; const currentDate = dayjs(); diff --git a/packages/features/ee/workflows/lib/test/compareReminderBodyToTemplate.test.ts b/packages/features/ee/workflows/lib/test/compareReminderBodyToTemplate.test.ts new file mode 100644 index 0000000000..b3193eeae4 --- /dev/null +++ b/packages/features/ee/workflows/lib/test/compareReminderBodyToTemplate.test.ts @@ -0,0 +1,126 @@ +import { expect, test, describe } from "vitest"; + +import { TimeFormat } from "@calcom/lib/timeFormat"; +import { WorkflowActions, WorkflowTemplates } from "@calcom/prisma/enums"; + +import { getTemplateBodyForAction } from "../actionHelperFunctions"; +import compareReminderBodyToTemplate from "../compareReminderBodyToTemplate"; +import plainTextReminderTemplates from "../reminders/templates/plainTextTemplates"; + +describe("compareReminderBodyToTemplate", () => { + test("should return true if reminderBody and template are the same", () => { + const reminderBody = "
Test
"; + const template = "Test
"; + expect(compareReminderBodyToTemplate({ reminderBody, template })).toBe(true); + }); + + test("should return false if reminderBody and template are different", () => { + const reminderBody = "Test
"; + const template = "Test2
"; + expect(compareReminderBodyToTemplate({ reminderBody, template })).toBe(false); + }); + + describe("email templates", () => { + test("reminder", () => { + const template = getTemplateBodyForAction({ + action: WorkflowActions.EMAIL_HOST, + template: WorkflowTemplates.REMINDER, + timeFormat: TimeFormat.TWELVE_HOUR, + locale: "en", + }); + + if (!template) throw new Error("template not found"); + + const reminderBody = plainTextReminderTemplates.email.reminder; + expect(compareReminderBodyToTemplate({ reminderBody, template })).toBe(true); + }); + + test("rating", () => { + const template = getTemplateBodyForAction({ + action: WorkflowActions.EMAIL_HOST, + template: WorkflowTemplates.RATING, + timeFormat: TimeFormat.TWELVE_HOUR, + locale: "en", + }); + + if (!template) throw new Error("template not found"); + + const reminderBody = plainTextReminderTemplates.email?.rating ?? ""; + expect(compareReminderBodyToTemplate({ reminderBody, template })).toBe(true); + }); + }); + + describe("sms templates", () => { + test("reminder", () => { + const template = getTemplateBodyForAction({ + action: WorkflowActions.SMS_ATTENDEE, + template: WorkflowTemplates.REMINDER, + timeFormat: TimeFormat.TWELVE_HOUR, + locale: "en", + }); + + if (!template) throw new Error("template not found"); + + const reminderBody = plainTextReminderTemplates.sms.reminder; + expect(compareReminderBodyToTemplate({ reminderBody, template })).toBe(true); + }); + }); + + describe("whatsapp templates", () => { + test("reminder", () => { + const template = getTemplateBodyForAction({ + action: WorkflowActions.WHATSAPP_ATTENDEE, + template: WorkflowTemplates.REMINDER, + timeFormat: TimeFormat.TWELVE_HOUR, + locale: "en", + }); + + if (!template) throw new Error("template not found"); + + const reminderBody = plainTextReminderTemplates.whatsapp.reminder; + expect(compareReminderBodyToTemplate({ reminderBody, template })).toBe(true); + }); + + test("rescheduled", () => { + const template = getTemplateBodyForAction({ + action: WorkflowActions.WHATSAPP_ATTENDEE, + template: WorkflowTemplates.RESCHEDULED, + timeFormat: TimeFormat.TWELVE_HOUR, + locale: "en", + }); + + if (!template) throw new Error("template not found"); + + const reminderBody = plainTextReminderTemplates.whatsapp.rescheduled; + expect(compareReminderBodyToTemplate({ reminderBody, template })).toBe(true); + }); + + test("completed", () => { + const template = getTemplateBodyForAction({ + action: WorkflowActions.WHATSAPP_ATTENDEE, + template: WorkflowTemplates.COMPLETED, + timeFormat: TimeFormat.TWELVE_HOUR, + locale: "en", + }); + + if (!template) throw new Error("template not found"); + + const reminderBody = plainTextReminderTemplates.whatsapp.completed; + expect(compareReminderBodyToTemplate({ reminderBody, template })).toBe(true); + }); + + test("canceled", () => { + const template = getTemplateBodyForAction({ + action: WorkflowActions.WHATSAPP_ATTENDEE, + template: WorkflowTemplates.CANCELLED, + timeFormat: TimeFormat.TWELVE_HOUR, + locale: "en", + }); + + if (!template) throw new Error("template not found"); + + const reminderBody = plainTextReminderTemplates.whatsapp?.canceled ?? ""; + expect(compareReminderBodyToTemplate({ reminderBody, template })).toBe(true); + }); + }); +}); diff --git a/packages/features/ee/workflows/lib/types.ts b/packages/features/ee/workflows/lib/types.ts index f363e16c90..8c5ac00f42 100644 --- a/packages/features/ee/workflows/lib/types.ts +++ b/packages/features/ee/workflows/lib/types.ts @@ -27,4 +27,5 @@ export type WorkflowStep = { includeCalendarEvent: boolean; numberVerificationPending: boolean; numberRequired: boolean | null; + verifiedAt?: Date | null; }; diff --git a/packages/features/package.json b/packages/features/package.json index b47d3df082..8274575f7b 100644 --- a/packages/features/package.json +++ b/packages/features/package.json @@ -16,6 +16,7 @@ "@tanstack/react-table": "^8.20.6", "@tanstack/react-virtual": "^3.10.9", "@vercel/functions": "^1.4.0", + "akismet-api": "^6.0.0", "class-variance-authority": "^0.7.1", "framer-motion": "^10.12.8", "lexical": "^0.9.0", diff --git a/packages/features/tasker/tasker.ts b/packages/features/tasker/tasker.ts index c326af3434..53bd5e0266 100644 --- a/packages/features/tasker/tasker.ts +++ b/packages/features/tasker/tasker.ts @@ -18,6 +18,7 @@ type TaskPayloads = { typeof import("./tasks/translateEventTypeData").ZTranslateEventDataPayloadSchema >; createCRMEvent: z.infer