* wip * wip * feature: Booking Tasker without DI yet * feature: Booking Tasker with DI * fix type check 1 * fix type check 2 * fix * comment booking tasker for now * fix: DI regularBookingService api v2 * fix: convert trigger.dev SDK imports to dynamic imports to fix unit tests The unit tests were failing because BookingEmailAndSmsTriggerTasker.ts had static imports of trigger files that depend on @trigger.dev/sdk. This caused Vitest to try to resolve the SDK at module load time, even though it should be optional. Changed all imports in BookingEmailAndSmsTriggerTasker.ts from static to dynamic (using await import()) so the trigger files are only loaded when the tasker methods are actually called, not at module load time during tests. This fixes the 'Failed to load url @trigger.dev/sdk' errors that were causing 28+ test failures. Co-Authored-By: morgan@cal.com <morgan@cal.com> * fix unit tests * keep inline smsAndEmailHandler.send calls * chore: add team feature flag * add satisfies ModuleLoader * fix type check app flags * move trigger in feature * fix: add trigger.dev prisma generator * fix: email app statuses * fix: CalEvtBuilder unit test * chore: improvements, schema, config, retry * fixup! chore: improvements, schema, config, retry * chore: cleanup code * chore: cleanup code * chore: clean code and give full payload * remove log * add booking notifications queue * add attendee phone number for sms * bump trigger to 4.1.0 * add missing booking seat data in attendee * update config * fix logger regular booking service * fix: prisma as external deps of trigger * fix yarn.lock * revert change to example app booking page * fix: resolve circular dependencies and improve cold start performance in trigger tasks - Convert BookingRepository import to type-only in CalendarEventBuilder.ts to eliminate circular dependency risk - Convert EventNameObjectType, CalendarEvent, and JsonObject imports to type-only in BookingEmailAndSmsTaskService.ts - Use dynamic imports in all trigger notification tasks (confirm, request, reschedule, rr-reschedule) to reduce cold start time - Move heavy imports (BookingEmailSmsHandler, BookingRepository, prisma, TriggerDevLogger, BookingEmailAndSmsTaskService) inside run functions - Eliminates module-level prisma import which violates repo guidelines and adds cold start overhead - Reduces initial module dependency graph by deferring heavy imports (email templates, workflows, large repositories) until task execution Co-Authored-By: morgan@cal.com <morgan@cal.com> * fix: improve cold start performance in reminderScheduler with dynamic imports - Remove module-level prisma import (violates 'No prisma outside repositories' guideline) - Use dynamic imports for UserRepository (1,168 lines) - only loaded when needed in EMAIL_ATTENDEE action - Use dynamic imports for twilio provider (386 lines) - only loaded in cancelScheduledMessagesAndScheduleEmails - Use dynamic imports for all manager functions by action type: - scheduleSMSReminder (387 lines) - loaded only for SMS actions - scheduleEmailReminder (459 lines) - loaded only for Email actions - scheduleWhatsappReminder (266 lines) - loaded only for WhatsApp actions - scheduleAIPhoneCall (478 lines) - loaded only for AI phone call actions - Use dynamic imports for sendOrScheduleWorkflowEmails in cancelScheduledMessagesAndScheduleEmails - Significantly reduces cold start time by deferring heavy module loading until execution paths need them - Eliminates module-level prisma import that violated repository pattern guidelines Co-Authored-By: morgan@cal.com <morgan@cal.com> * fix: improve cold start performance in BookingEmailSmsHandler with dynamic imports - Remove module-level imports of all email-manager functions (653 LOC + 30+ email templates) - Add dynamic imports in each method (_handleRescheduled, _handleRoundRobinRescheduled, _handleConfirmed, _handleRequested, handleAddGuests) - Defer heavy email-manager loading until method execution - Verified no circular dependencies between email-manager and bookings - Significantly reduces cold start time for RegularBookingService and BookingEmailAndSmsTaskService Co-Authored-By: morgan@cal.com <morgan@cal.com> * fix: use dynamic imports * update yarn lock * code review * trigger config project ref in env * update yarn lock * add .env.example trigger variables * add .env.example trigger variables * fix: cleanup error handling and loggin * fix: trigger config from env * fix: small typo fix * fix: ai review comments * fix: ai review comments * ai review * Add `create` on `WorkflowReminderRepository` * `sendWorkflowEmails` tasker to accept lazy payload * Add `scheduleLazyEmailWorkflow` to `WorkflowService * Process scheduled date in `scheduleLazyEmailWorkflow` * Type fixes * Use `WorkflowService` to schedule * Refactor `scheduleEmailReminderForEvt` to use `WorkflowService.processWorkflowScheduledDate` * Pass seat reference to lazy scheduled workflow reminder * Refactor `WorkflowReminderRepository` to accept prisma as constructor * Abstract `FormSubmissionData` type * Abstract select statement and add get by uid to `BookingRepository` * Add `FormSubmissionData` type * Add `findByIdIncludeStepAndWorkflow` to `WorkflowReminderRepository` * Tasker payload to accept `workflowReminderId` * Create `BookingSeatRepository` * Write `workflowReminderId` to tasker payload * Add `generateCommonScheduleFunctionParams` to `WorkflowService` * Init * Use services in tasker * Abstract types * In reminderScheduler use workflowService to generate common params * Type fix * Return params from emailWorkflowService.generateParametersToBuildEmailWorkflowContent * Use emailWorkflowService to generate params * Abstract types * Generate email content and send in EmailWorkflowService * Move check to caller * Use EmailWorkflowService to generate email payload in emailReminderManager * Fix initalizing repository * Use evt.videoCallData first before the booking metadata * Only get non-deleted references to build calendar event * Remove check for videoCallData.id * Dynamic import credit service * BookingRepository.getByUid to only return what we need from attendee * Type fixes * test: Add comprehensive tests for lazy email workflow generation and fix broken tests - Fix prisma mocks in sms-manager.test.ts and outOfOfficeCreateOrUpdate.handler.test.ts to export both 'default' and named 'prisma' exports - Add EmailWorkflowService.test.ts with 4 tests for error handling paths - Add sendWorkflowEmails.test.ts with 7 tests for schema validation and email sending - Add tests to WorkflowService.test.ts for scheduleLazyEmailWorkflow, processWorkflowScheduledDate, and generateCommonScheduleFunctionParams methods Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * Write `seatReferenceUid` * Return promise * Fix log * Change to `Promise.allSettled` * Type fix * Fix failing test * fix: reorder workflow step checks to fix test failure The test 'should throw error if workflow step not found on reminder' was failing because the code checked workflowStep.verifiedAt before checking if workflowStep exists. When workflowStep is null, this caused the error message to include 'undefined' instead of the expected workflow step id. Fixed by reordering the checks: 1. First check if workflowStep exists 2. Then check if workflowStep.verifiedAt exists Also updated the test expectation to match the correct error message. Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> --------- Co-authored-by: cal.com <morgan@cal.com> Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com> Co-authored-by: hbjORbj <sldisek783@gmail.com> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Alex van Andel <me@alexvanandel.com>
479 lines
14 KiB
TypeScript
479 lines
14 KiB
TypeScript
import { v4 as uuidv4 } from "uuid";
|
|
|
|
import type { FORM_SUBMITTED_WEBHOOK_RESPONSES } from "@calcom/app-store/routing-forms/lib/formSubmissionUtils";
|
|
import dayjs from "@calcom/dayjs";
|
|
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
|
import tasker from "@calcom/features/tasker";
|
|
import { CAL_AI_AGENT_PHONE_NUMBER_FIELD } from "@calcom/lib/bookings/SystemField";
|
|
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
|
|
import logger from "@calcom/lib/logger";
|
|
import prisma from "@calcom/prisma";
|
|
import { WorkflowMethods, WorkflowTriggerEvents } from "@calcom/prisma/enums";
|
|
import type { TimeUnit } from "@calcom/prisma/enums";
|
|
import { PhoneNumberSubscriptionStatus } from "@calcom/prisma/enums";
|
|
|
|
import type { BookingInfo, FormSubmissionData } from "../types";
|
|
import type { WorkflowContextData } from "./reminderScheduler";
|
|
|
|
type timeUnitLowerCase = "day" | "hour" | "minute";
|
|
|
|
function extractPhoneNumber(responses: BookingInfo["responses"]): string | undefined {
|
|
if (!responses) return undefined;
|
|
|
|
// Priority 1: CAL_AI_AGENT_PHONE_NUMBER_FIELD first
|
|
const aiAgentPhoneResponse = responses[CAL_AI_AGENT_PHONE_NUMBER_FIELD];
|
|
if (aiAgentPhoneResponse && typeof aiAgentPhoneResponse === "object" && "value" in aiAgentPhoneResponse) {
|
|
return aiAgentPhoneResponse.value as string;
|
|
}
|
|
|
|
// Priority 2: attendeePhoneNumber as fallback
|
|
const attendeePhoneResponse = responses.attendeePhoneNumber;
|
|
if (
|
|
attendeePhoneResponse &&
|
|
typeof attendeePhoneResponse === "object" &&
|
|
"value" in attendeePhoneResponse
|
|
) {
|
|
return attendeePhoneResponse.value as string;
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
type CreateWorkflowReminderAndExtractPhoneArgs = WorkflowContextData & {
|
|
workflowStepId: number;
|
|
scheduledDate: dayjs.Dayjs;
|
|
seatReferenceUid?: string;
|
|
submittedPhoneNumber?: string | null;
|
|
};
|
|
|
|
interface CreateWorkflowReminderAndExtractPhoneResult {
|
|
workflowReminder: { id: number; uuid: string | null };
|
|
attendeePhoneNumber: string;
|
|
}
|
|
|
|
const createWorkflowReminderAndExtractPhone = async (
|
|
args: CreateWorkflowReminderAndExtractPhoneArgs
|
|
): Promise<CreateWorkflowReminderAndExtractPhoneResult | null> => {
|
|
const { evt, workflowStepId, scheduledDate, seatReferenceUid, submittedPhoneNumber } = args;
|
|
// 1) Determine attendee phone first (fail early)
|
|
let attendeePhoneNumber = extractPhoneNumber(evt?.responses) || submittedPhoneNumber;
|
|
if (!attendeePhoneNumber) {
|
|
if (!evt) {
|
|
return null;
|
|
}
|
|
|
|
if (evt.attendees?.[0]?.phoneNumber) {
|
|
attendeePhoneNumber = evt.attendees?.[0]?.phoneNumber;
|
|
} else {
|
|
throw new Error(`No attendee phone number found for workflow step ${workflowStepId}`);
|
|
}
|
|
}
|
|
|
|
const workflowReminder = await prisma.workflowReminder.create({
|
|
data: {
|
|
bookingUid: evt?.uid,
|
|
workflowStepId,
|
|
method: WorkflowMethods.AI_PHONE_CALL,
|
|
scheduledDate: scheduledDate.toDate(),
|
|
scheduled: true,
|
|
seatReferenceId: seatReferenceUid,
|
|
},
|
|
});
|
|
|
|
return { workflowReminder, attendeePhoneNumber };
|
|
};
|
|
|
|
type ScheduleAIPhoneCallArgs = {
|
|
submittedPhoneNumber: string | null;
|
|
triggerEvent: WorkflowTriggerEvents;
|
|
timeSpan: {
|
|
time: number | null;
|
|
timeUnit: TimeUnit | null;
|
|
};
|
|
workflowStepId: number | undefined;
|
|
userId: number | null;
|
|
teamId: number | null;
|
|
seatReferenceUid?: string;
|
|
verifiedAt: Date | null;
|
|
routedEventTypeId: number | null;
|
|
} & WorkflowContextData;
|
|
|
|
export type ScheduleAIPhoneCallArgsWithRequiredFields = Omit<ScheduleAIPhoneCallArgs, "workflowStepId"> & {
|
|
workflowStepId: number; // Required
|
|
agent: Agent;
|
|
activePhoneNumber: string;
|
|
};
|
|
|
|
type Agent = {
|
|
id: string;
|
|
providerAgentId: string;
|
|
outboundPhoneNumbers: {
|
|
phoneNumber: string;
|
|
subscriptionStatus: PhoneNumberSubscriptionStatus | null;
|
|
}[];
|
|
};
|
|
|
|
export const scheduleAIPhoneCall = async (args: ScheduleAIPhoneCallArgs) => {
|
|
const { workflowStepId, verifiedAt } = args;
|
|
if (!workflowStepId) {
|
|
logger.warn(`Workflow step ID is required for AI phone call scheduling`);
|
|
return;
|
|
}
|
|
|
|
if (!verifiedAt) {
|
|
logger.warn(`Workflow step ${workflowStepId} not yet verified`);
|
|
return;
|
|
}
|
|
|
|
// Get the workflow step to check if it has an agent configured
|
|
const workflowStep = await prisma.workflowStep.findUnique({
|
|
where: { id: workflowStepId },
|
|
select: {
|
|
agent: {
|
|
select: {
|
|
id: true,
|
|
providerAgentId: true,
|
|
outboundPhoneNumbers: {
|
|
select: {
|
|
phoneNumber: true,
|
|
subscriptionStatus: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const activePhoneNumbers = workflowStep?.agent?.outboundPhoneNumbers?.filter(
|
|
(phoneNumber) =>
|
|
phoneNumber.subscriptionStatus === PhoneNumberSubscriptionStatus.ACTIVE ||
|
|
phoneNumber.subscriptionStatus === null ||
|
|
phoneNumber.subscriptionStatus === undefined
|
|
);
|
|
|
|
if (!workflowStep?.agent) {
|
|
logger.warn(`No agent configured for workflow step ${workflowStepId}`);
|
|
return;
|
|
}
|
|
|
|
if (!workflowStep.agent.outboundPhoneNumbers?.length || !activePhoneNumbers?.length) {
|
|
logger.warn(`No active outbound phone number configured for agent ${workflowStep.agent.id}`);
|
|
return;
|
|
}
|
|
|
|
const featuresRepository = new FeaturesRepository(prisma);
|
|
const calAIVoiceAgents = await featuresRepository.checkIfFeatureIsEnabledGlobally("cal-ai-voice-agents");
|
|
if (!calAIVoiceAgents) {
|
|
logger.warn("Cal.ai voice agents are disabled - skipping AI phone call scheduling");
|
|
return;
|
|
}
|
|
const params = { ...args, agent: workflowStep.agent, activePhoneNumber: activePhoneNumbers[0].phoneNumber };
|
|
|
|
if (params.evt) {
|
|
await scheduleAIPhoneCallForEvt(
|
|
params as ScheduleAIPhoneCallArgsWithRequiredFields & { evt: BookingInfo }
|
|
);
|
|
} else {
|
|
await scheduleAIPhoneCallForForm(
|
|
params as ScheduleAIPhoneCallArgsWithRequiredFields & {
|
|
formData: FormSubmissionData;
|
|
}
|
|
);
|
|
}
|
|
};
|
|
|
|
const scheduleAIPhoneCallForEvt = async (
|
|
args: ScheduleAIPhoneCallArgsWithRequiredFields & { evt: BookingInfo }
|
|
) => {
|
|
const {
|
|
evt,
|
|
triggerEvent,
|
|
timeSpan,
|
|
workflowStepId,
|
|
userId,
|
|
teamId,
|
|
seatReferenceUid,
|
|
agent,
|
|
activePhoneNumber,
|
|
} = args;
|
|
|
|
const { startTime, endTime } = evt;
|
|
const uid = evt.uid as string;
|
|
const currentDate = dayjs();
|
|
const timeUnit: timeUnitLowerCase | undefined = timeSpan.timeUnit?.toLocaleLowerCase() as timeUnitLowerCase;
|
|
let scheduledDate = null;
|
|
|
|
// Calculate when the AI phone call should be made
|
|
if (triggerEvent === WorkflowTriggerEvents.BEFORE_EVENT) {
|
|
scheduledDate = timeSpan.time && timeUnit ? dayjs(startTime).subtract(timeSpan.time, timeUnit) : null;
|
|
} else if (triggerEvent === WorkflowTriggerEvents.AFTER_EVENT) {
|
|
scheduledDate = timeSpan.time && timeUnit ? dayjs(endTime).add(timeSpan.time, timeUnit) : null;
|
|
}
|
|
|
|
// For immediate triggers (like NEW_EVENT, EVENT_CANCELLED, etc.), schedule immediately
|
|
if (!scheduledDate) {
|
|
scheduledDate = currentDate;
|
|
}
|
|
|
|
// Determine if we should execute immediately or schedule for later
|
|
const shouldExecuteImmediately =
|
|
// Immediate triggers (NEW_EVENT, EVENT_CANCELLED, etc.)
|
|
!timeSpan.time ||
|
|
!timeSpan.timeUnit ||
|
|
// Or if the scheduled time has already passed
|
|
(scheduledDate && currentDate.isAfter(scheduledDate));
|
|
|
|
if (!shouldExecuteImmediately) {
|
|
try {
|
|
const reminderAndPhone = await createWorkflowReminderAndExtractPhone({
|
|
evt,
|
|
workflowStepId,
|
|
scheduledDate,
|
|
seatReferenceUid,
|
|
});
|
|
|
|
if (!reminderAndPhone) {
|
|
logger.warn(`No phone number found for AI phone call workflow step ${workflowStepId} - skipping`);
|
|
return;
|
|
}
|
|
|
|
const { workflowReminder, attendeePhoneNumber } = reminderAndPhone;
|
|
|
|
// Schedule the actual AI phone call
|
|
await scheduleAIPhoneCallTask({
|
|
workflowReminderId: workflowReminder.id,
|
|
scheduledDate: scheduledDate.toDate(),
|
|
agentId: agent.id,
|
|
phoneNumber: activePhoneNumber,
|
|
attendeePhoneNumber,
|
|
bookingUid: uid,
|
|
userId,
|
|
teamId,
|
|
providerAgentId: agent.providerAgentId,
|
|
referenceUid: workflowReminder.uuid || uuidv4(),
|
|
formResponses: null,
|
|
routedEventTypeId: null,
|
|
});
|
|
|
|
logger.info(`AI phone call scheduled for workflow step ${workflowStepId} at ${scheduledDate}`);
|
|
} catch (error) {
|
|
logger.error(`Error scheduling AI phone call with error ${error}`);
|
|
}
|
|
} else {
|
|
// Execute immediately
|
|
try {
|
|
const reminderAndPhone = await createWorkflowReminderAndExtractPhone({
|
|
evt,
|
|
workflowStepId,
|
|
scheduledDate: currentDate,
|
|
seatReferenceUid,
|
|
});
|
|
|
|
if (!reminderAndPhone) {
|
|
logger.warn(`No phone number found for AI phone call workflow step ${workflowStepId} - skipping`);
|
|
return;
|
|
}
|
|
|
|
const { workflowReminder, attendeePhoneNumber } = reminderAndPhone;
|
|
|
|
// Schedule the actual AI phone call immediately
|
|
// Should i execute the task immediately or schedule it for later?
|
|
await scheduleAIPhoneCallTask({
|
|
workflowReminderId: workflowReminder.id,
|
|
scheduledDate: currentDate.toDate(),
|
|
agentId: agent.id,
|
|
phoneNumber: activePhoneNumber,
|
|
attendeePhoneNumber,
|
|
bookingUid: uid,
|
|
userId,
|
|
teamId,
|
|
providerAgentId: agent.providerAgentId,
|
|
referenceUid: workflowReminder.uuid || uuidv4(),
|
|
formResponses: null,
|
|
routedEventTypeId: null,
|
|
});
|
|
|
|
logger.info(`AI phone call scheduled for immediate execution for workflow step ${workflowStepId}`);
|
|
} catch (error) {
|
|
logger.error(`Error scheduling immediate AI phone call with error ${error}`);
|
|
}
|
|
}
|
|
};
|
|
|
|
// sends all immediately, no scheduling needed (tasker handles scheduling)
|
|
const scheduleAIPhoneCallForForm = async (
|
|
args: ScheduleAIPhoneCallArgsWithRequiredFields & {
|
|
formData: FormSubmissionData;
|
|
}
|
|
) => {
|
|
const {
|
|
formData,
|
|
workflowStepId,
|
|
userId,
|
|
teamId,
|
|
seatReferenceUid,
|
|
submittedPhoneNumber,
|
|
agent,
|
|
activePhoneNumber,
|
|
routedEventTypeId,
|
|
} = args;
|
|
|
|
try {
|
|
const reminderAndPhone = await createWorkflowReminderAndExtractPhone({
|
|
formData,
|
|
workflowStepId,
|
|
submittedPhoneNumber,
|
|
scheduledDate: dayjs(),
|
|
seatReferenceUid,
|
|
});
|
|
|
|
if (!reminderAndPhone) {
|
|
logger.warn(`No phone number found for AI phone call workflow step ${workflowStepId} - skipping`);
|
|
return;
|
|
}
|
|
|
|
const { workflowReminder, attendeePhoneNumber } = reminderAndPhone;
|
|
|
|
await scheduleAIPhoneCallTask({
|
|
workflowReminderId: workflowReminder.id,
|
|
agentId: agent.id,
|
|
phoneNumber: activePhoneNumber,
|
|
attendeePhoneNumber,
|
|
userId,
|
|
teamId,
|
|
scheduledDate: null,
|
|
formResponses: formData.responses,
|
|
bookingUid: null,
|
|
providerAgentId: agent.providerAgentId,
|
|
referenceUid: workflowReminder.uuid || uuidv4(),
|
|
routedEventTypeId: routedEventTypeId ?? null,
|
|
});
|
|
logger.info(`AI phone call scheduled for immediate execution for workflow step ${workflowStepId}`);
|
|
} catch (error) {
|
|
logger.error(`Error scheduling immediate AI phone call with error ${error}`);
|
|
}
|
|
};
|
|
|
|
interface ScheduleAIPhoneCallTaskArgs {
|
|
workflowReminderId: number;
|
|
scheduledDate: Date | null;
|
|
agentId: string;
|
|
phoneNumber: string;
|
|
attendeePhoneNumber: string;
|
|
bookingUid: string | null;
|
|
userId: number | null;
|
|
teamId: number | null;
|
|
providerAgentId: string;
|
|
referenceUid: string;
|
|
formResponses: FORM_SUBMITTED_WEBHOOK_RESPONSES | null;
|
|
routedEventTypeId: number | null;
|
|
}
|
|
|
|
const scheduleAIPhoneCallTask = async (args: ScheduleAIPhoneCallTaskArgs) => {
|
|
const {
|
|
workflowReminderId,
|
|
scheduledDate,
|
|
agentId,
|
|
phoneNumber,
|
|
attendeePhoneNumber,
|
|
bookingUid,
|
|
userId,
|
|
teamId,
|
|
providerAgentId,
|
|
referenceUid,
|
|
formResponses,
|
|
routedEventTypeId,
|
|
} = args;
|
|
|
|
if (!formResponses && !bookingUid) {
|
|
logger.warn("context missing for creating AI phone call task");
|
|
return;
|
|
}
|
|
|
|
const featuresRepository = new FeaturesRepository(prisma);
|
|
const calAIVoiceAgents = await featuresRepository.checkIfFeatureIsEnabledGlobally("cal-ai-voice-agents");
|
|
if (!calAIVoiceAgents) {
|
|
logger.warn("Cal.ai voice agents are disabled - skipping AI phone call");
|
|
return;
|
|
}
|
|
|
|
if (userId) {
|
|
await checkRateLimitAndThrowError({
|
|
rateLimitingType: "core",
|
|
identifier: `ai-phone-call:${userId}`,
|
|
});
|
|
}
|
|
|
|
try {
|
|
await tasker.create(
|
|
"executeAIPhoneCall",
|
|
{
|
|
workflowReminderId,
|
|
agentId,
|
|
fromNumber: phoneNumber,
|
|
toNumber: attendeePhoneNumber,
|
|
bookingUid,
|
|
userId,
|
|
teamId,
|
|
providerAgentId,
|
|
responses: formResponses,
|
|
routedEventTypeId,
|
|
},
|
|
{
|
|
scheduledAt: scheduledDate || undefined,
|
|
maxAttempts: 1,
|
|
referenceUid,
|
|
}
|
|
);
|
|
} catch (error) {
|
|
console.error("Error creating AI phone call task:", error);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
export const deleteScheduledAIPhoneCall = async (reminderId: number, referenceId: string | null) => {
|
|
const workflowReminder = await prisma.workflowReminder.findUnique({
|
|
where: {
|
|
id: reminderId,
|
|
},
|
|
});
|
|
|
|
if (!workflowReminder) {
|
|
logger.error("AI Phone Call workflow reminder not found");
|
|
return;
|
|
}
|
|
|
|
const { uuid } = workflowReminder;
|
|
const taskReferenceId = referenceId || uuid;
|
|
|
|
if (taskReferenceId) {
|
|
try {
|
|
const taskId = await tasker.cancelWithReference(taskReferenceId, "executeAIPhoneCall");
|
|
if (taskId) {
|
|
await prisma.workflowReminder.delete({
|
|
where: {
|
|
id: reminderId,
|
|
},
|
|
});
|
|
|
|
logger.info(`AI phone call reminder ${reminderId} cancelled and deleted`);
|
|
return;
|
|
}
|
|
} catch (error) {
|
|
logger.error(`Error canceling/deleting AI phone call reminder with tasker. Error: ${error}`);
|
|
}
|
|
}
|
|
|
|
// Fallback: If tasker cancellation fails or uuid is not found, just delete the reminder
|
|
try {
|
|
await prisma.workflowReminder.delete({
|
|
where: {
|
|
id: reminderId,
|
|
},
|
|
});
|
|
logger.info(`AI phone call reminder ${reminderId} deleted (no task to cancel)`);
|
|
} catch (error) {
|
|
logger.error(`Error deleting AI phone call reminder ${reminderId}: ${error}`);
|
|
}
|
|
};
|