diff --git a/packages/app-store/routing-forms/lib/formSubmissionUtils.test.ts b/packages/app-store/routing-forms/lib/formSubmissionUtils.test.ts index 16a00c943d49d1..6e34ae3e921490 100644 --- a/packages/app-store/routing-forms/lib/formSubmissionUtils.test.ts +++ b/packages/app-store/routing-forms/lib/formSubmissionUtils.test.ts @@ -158,25 +158,28 @@ describe("_onFormSubmission", () => { await _onFormSubmission(mockForm, mockResponse, responseId); expect(WorkflowService.getAllWorkflowsFromRoutingForm).toHaveBeenCalledWith(mockForm); - expect(WorkflowService.scheduleFormWorkflows).toHaveBeenCalledWith({ - workflows: mockWorkflows, - responses: { - email: { - value: "test@response.com", - response: "test@response.com", + expect(WorkflowService.scheduleFormWorkflows).toHaveBeenCalledWith( + expect.objectContaining({ + workflows: mockWorkflows, + responses: { + email: { + value: "test@response.com", + response: "test@response.com", + }, + name: { value: "Test Name", response: "Test Name" }, }, - name: { value: "Test Name", response: "Test Name" }, - }, - responseId, - routedEventTypeId: null, - form: { - ...mockForm, - fields: mockForm.fields.map((field) => ({ - type: field.type, - identifier: field.identifier, - })), - }, - }); + responseId, + routedEventTypeId: null, + form: { + ...mockForm, + fields: mockForm.fields.map((field) => ({ + type: field.type, + identifier: field.identifier, + })), + }, + creditCheckFn: expect.any(Function), + }) + ); }); it("should call WorkflowService.scheduleFormWorkflows for FORM_SUBMITTED_NO_EVENT workflows", async () => { @@ -212,25 +215,28 @@ describe("_onFormSubmission", () => { await _onFormSubmission(mockForm, mockResponse, responseId); expect(WorkflowService.getAllWorkflowsFromRoutingForm).toHaveBeenCalledWith(mockForm); - expect(WorkflowService.scheduleFormWorkflows).toHaveBeenCalledWith({ - workflows: mockWorkflows, - responses: { - email: { - value: "test@response.com", - response: "test@response.com", + expect(WorkflowService.scheduleFormWorkflows).toHaveBeenCalledWith( + expect.objectContaining({ + workflows: mockWorkflows, + responses: { + email: { + value: "test@response.com", + response: "test@response.com", + }, + name: { value: "Test Name", response: "Test Name" }, }, - name: { value: "Test Name", response: "Test Name" }, - }, - routedEventTypeId: null, - responseId, - form: { - ...mockForm, - fields: mockForm.fields.map((field) => ({ - type: field.type, - identifier: field.identifier, - })), - }, - }); + routedEventTypeId: null, + responseId, + form: { + ...mockForm, + fields: mockForm.fields.map((field) => ({ + type: field.type, + identifier: field.identifier, + })), + }, + creditCheckFn: expect.any(Function), + }) + ); }); it("should pass routedEventTypeId when chosenAction is eventTypeRedirectUrl", async () => { @@ -271,25 +277,28 @@ describe("_onFormSubmission", () => { await _onFormSubmission(mockForm, mockResponse, responseId, chosenAction); - expect(WorkflowService.scheduleFormWorkflows).toHaveBeenCalledWith({ - workflows: mockWorkflows, - responses: { - email: { - value: "test@response.com", - response: "test@response.com", + expect(WorkflowService.scheduleFormWorkflows).toHaveBeenCalledWith( + expect.objectContaining({ + workflows: mockWorkflows, + responses: { + email: { + value: "test@response.com", + response: "test@response.com", + }, + name: { value: "Test Name", response: "Test Name" }, }, - name: { value: "Test Name", response: "Test Name" }, - }, - routedEventTypeId: 42, - responseId, - form: { - ...mockForm, - fields: mockForm.fields.map((field) => ({ - type: field.type, - identifier: field.identifier, - })), - }, - }); + routedEventTypeId: 42, + responseId, + form: { + ...mockForm, + fields: mockForm.fields.map((field) => ({ + type: field.type, + identifier: field.identifier, + })), + }, + creditCheckFn: expect.any(Function), + }) + ); }); }); diff --git a/packages/app-store/routing-forms/lib/formSubmissionUtils.ts b/packages/app-store/routing-forms/lib/formSubmissionUtils.ts index 965eb0ba79d9da..98f02306fcc69d 100644 --- a/packages/app-store/routing-forms/lib/formSubmissionUtils.ts +++ b/packages/app-store/routing-forms/lib/formSubmissionUtils.ts @@ -1,4 +1,5 @@ import dayjs from "@calcom/dayjs"; +import { CreditService } from "@calcom/features/ee/billing/credit-service"; import { WorkflowService } from "@calcom/features/ee/workflows/lib/service/WorkflowService"; import type { Tasker } from "@calcom/features/tasker/tasker"; import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks"; @@ -224,11 +225,15 @@ export async function _onFormSubmission( chosenAction && chosenAction.type === "eventTypeRedirectUrl" && chosenAction.eventTypeId ? chosenAction.eventTypeId : null; + + const creditService = new CreditService(); + await WorkflowService.scheduleFormWorkflows({ workflows, responseId, responses: fieldResponsesByIdentifier, routedEventTypeId, + creditCheckFn: creditService.hasAvailableCredits.bind(creditService), form: { ...form, fields: form.fields.map((field) => ({ diff --git a/packages/features/bookings/lib/handleBookingRequested.ts b/packages/features/bookings/lib/handleBookingRequested.ts index 603290f3eb9aba..05a28d3bc4d532 100644 --- a/packages/features/bookings/lib/handleBookingRequested.ts +++ b/packages/features/bookings/lib/handleBookingRequested.ts @@ -1,5 +1,6 @@ import { sendAttendeeRequestEmailAndSMS, sendOrganizerRequestEmail } from "@calcom/emails/email-manager"; import { getWebhookPayloadForBooking } from "@calcom/features/bookings/lib/getWebhookPayloadForBooking"; +import { CreditService } from "@calcom/features/ee/billing/credit-service"; import { WorkflowService } from "@calcom/features/ee/workflows/lib/service/WorkflowService"; import type { Workflow } from "@calcom/features/ee/workflows/lib/types"; import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks"; @@ -103,6 +104,8 @@ export async function handleBookingRequested(args: { const workflows = await getAllWorkflowsFromEventType(booking.eventType, booking.userId); if (workflows.length > 0) { + const creditService = new CreditService(); + await WorkflowService.scheduleWorkflowsFilteredByTriggerEvent({ workflows, smsReminderNumber: booking.smsReminderNumber, @@ -117,6 +120,7 @@ export async function handleBookingRequested(args: { }, }, triggers: [WorkflowTriggerEvents.BOOKING_REQUESTED], + creditCheckFn: creditService.hasAvailableCredits.bind(creditService), }); } } catch (error) { diff --git a/packages/features/bookings/lib/handleCancelBooking.ts b/packages/features/bookings/lib/handleCancelBooking.ts index e3944ee8516f7b..b9d96d1814382e 100644 --- a/packages/features/bookings/lib/handleCancelBooking.ts +++ b/packages/features/bookings/lib/handleCancelBooking.ts @@ -9,6 +9,7 @@ import EventManager from "@calcom/features/bookings/lib/EventManager"; import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses"; import { processNoShowFeeOnCancellation } from "@calcom/features/bookings/lib/payment/processNoShowFeeOnCancellation"; import { processPaymentRefund } from "@calcom/features/bookings/lib/payment/processPaymentRefund"; +import { CreditService } from "@calcom/features/ee/billing/credit-service"; import { getBookerBaseUrl } from "@calcom/features/ee/organizations/lib/getBookerUrlServer"; import { sendCancelledReminders } from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler"; import { WorkflowRepository } from "@calcom/features/ee/workflows/repositories/WorkflowRepository"; @@ -353,6 +354,8 @@ async function handler(input: CancelBookingInput) { const workflows = await getAllWorkflowsFromEventType(bookingToDelete.eventType, bookingToDelete.userId); const parsedMetadata = bookingMetadataSchema.safeParse(bookingToDelete.metadata || {}); + const creditService = new CreditService(); + await sendCancelledReminders({ workflows, smsReminderNumber: bookingToDelete.smsReminderNumber, @@ -371,6 +374,7 @@ async function handler(input: CancelBookingInput) { }, }, hideBranding: !!bookingToDelete.eventType?.owner?.hideBranding, + creditCheckFn: creditService.hasAvailableCredits.bind(creditService), }); let updatedBookings: { diff --git a/packages/features/bookings/lib/handleConfirmation.ts b/packages/features/bookings/lib/handleConfirmation.ts index 179b6f725cc9e7..c037943f92ab37 100644 --- a/packages/features/bookings/lib/handleConfirmation.ts +++ b/packages/features/bookings/lib/handleConfirmation.ts @@ -3,6 +3,7 @@ import { scheduleMandatoryReminder } from "@calcom/ee/workflows/lib/reminders/sc import { sendScheduledEmailsAndSMS } from "@calcom/emails/email-manager"; import type { EventManagerUser } from "@calcom/features/bookings/lib/EventManager"; import EventManager, { placeholderCreatedEvent } from "@calcom/features/bookings/lib/EventManager"; +import { CreditService } from "@calcom/features/ee/billing/credit-service"; import { getBookerBaseUrl } from "@calcom/features/ee/organizations/lib/getBookerUrlServer"; import { allowDisablingAttendeeConfirmationEmails, @@ -360,6 +361,8 @@ export async function handleConfirmation(args: { }); } + const creditService = new CreditService(); + await WorkflowService.scheduleWorkflowsForNewBooking({ workflows, smsReminderNumber: updatedBookings[index].smsReminderNumber, @@ -368,6 +371,7 @@ export async function handleConfirmation(args: { isConfirmedByDefault: true, isNormalBookingOrFirstRecurringSlot: isFirstBooking, isRescheduleEvent: false, + creditCheckFn: creditService.hasAvailableCredits.bind(creditService), }); } } catch (error) { @@ -571,12 +575,15 @@ export async function handleConfirmation(args: { metadata: { videoCallUrl: meetingUrl }, }; + const creditService = new CreditService(); + await WorkflowService.scheduleWorkflowsFilteredByTriggerEvent({ workflows, smsReminderNumber: booking.smsReminderNumber, calendarEvent: calendarEventForWorkflow, hideBranding: !!updatedBookings[0].eventType?.owner?.hideBranding, triggers: [WorkflowTriggerEvents.BOOKING_PAID], + creditCheckFn: creditService.hasAvailableCredits.bind(creditService), }); } catch (error) { log.error("Error while scheduling workflow reminders for booking paid", safeStringify(error)); diff --git a/packages/features/bookings/lib/handleSeats/handleSeats.ts b/packages/features/bookings/lib/handleSeats/handleSeats.ts index f424fb560aaed0..ac64a723308e3d 100644 --- a/packages/features/bookings/lib/handleSeats/handleSeats.ts +++ b/packages/features/bookings/lib/handleSeats/handleSeats.ts @@ -1,6 +1,6 @@ - import dayjs from "@calcom/dayjs"; import { handleWebhookTrigger } from "@calcom/features/bookings/lib/handleWebhookTrigger"; +import { CreditService } from "@calcom/features/ee/billing/credit-service"; import { WorkflowService } from "@calcom/features/ee/workflows/lib/service/WorkflowService"; import type { EventPayloadType } from "@calcom/features/webhooks/lib/sendPayload"; import { ErrorCode } from "@calcom/lib/errorCodes"; @@ -108,6 +108,8 @@ const handleSeats = async (newSeatedBookingObject: NewSeatedBookingObject) => { ...reqBodyMetadata, }; try { + const creditService = new CreditService(); + await WorkflowService.scheduleWorkflowsForNewBooking({ workflows: workflows, smsReminderNumber: smsReminderNumber || null, @@ -130,6 +132,7 @@ const handleSeats = async (newSeatedBookingObject: NewSeatedBookingObject) => { isConfirmedByDefault: !evt.requiresConfirmation, isRescheduleEvent: !!rescheduleUid, isNormalBookingOrFirstRecurringSlot: true, + creditCheckFn: creditService.hasAvailableCredits.bind(creditService), }); } catch (error) { loggerWithEventDetails.error("Error while scheduling workflow reminders", JSON.stringify({ error })); diff --git a/packages/features/bookings/lib/service/RegularBookingService.ts b/packages/features/bookings/lib/service/RegularBookingService.ts index 8ec6de706485d9..840a0ea08450a8 100644 --- a/packages/features/bookings/lib/service/RegularBookingService.ts +++ b/packages/features/bookings/lib/service/RegularBookingService.ts @@ -35,6 +35,7 @@ import { handleWebhookTrigger } from "@calcom/features/bookings/lib/handleWebhoo import { isEventTypeLoggingEnabled } from "@calcom/features/bookings/lib/isEventTypeLoggingEnabled"; import { BookingEventHandlerService } from "@calcom/features/bookings/lib/onBookingEvents/BookingEventHandlerService"; import { getSpamCheckService } from "@calcom/features/di/watchlist/containers/SpamCheckService.container"; +import { CreditService } from "@calcom/features/ee/billing/credit-service"; import { getBookerBaseUrl } from "@calcom/features/ee/organizations/lib/getBookerUrlServer"; import AssignmentReasonRecorder from "@calcom/features/ee/round-robin/assignmentReason/AssignmentReasonRecorder"; import { WorkflowService } from "@calcom/features/ee/workflows/lib/service/WorkflowService"; @@ -1261,9 +1262,9 @@ async function handler( // This ensures that createMeeting isn't called for static video apps as bookingLocation becomes just a regular value for them. const { bookingLocation, conferenceCredentialId } = organizerOrFirstDynamicGroupMemberDefaultLocationUrl ? { - bookingLocation: organizerOrFirstDynamicGroupMemberDefaultLocationUrl, - conferenceCredentialId: undefined, - } + bookingLocation: organizerOrFirstDynamicGroupMemberDefaultLocationUrl, + conferenceCredentialId: undefined, + } : getLocationValueForDB(locationBodyString, eventType.locations); tracingLogger.info("locationBodyString", locationBodyString); @@ -1309,8 +1310,8 @@ async function handler( const destinationCalendar = eventType.destinationCalendar ? [eventType.destinationCalendar] : organizerUser.destinationCalendar - ? [organizerUser.destinationCalendar] - : null; + ? [organizerUser.destinationCalendar] + : null; let organizerEmail = organizerUser.email || "Email-less"; if (eventType.useEventTypeDestinationCalendarEmail && destinationCalendar?.[0]?.primaryEmail) { @@ -1931,14 +1932,14 @@ async function handler( } const updateManager = !skipCalendarSyncTaskCreation ? await eventManager.reschedule( - evt, - originalRescheduledBooking.uid, - undefined, - changedOrganizer, - previousHostDestinationCalendar, - isBookingRequestedReschedule, - skipDeleteEventsAndMeetings - ) + evt, + originalRescheduledBooking.uid, + undefined, + changedOrganizer, + previousHostDestinationCalendar, + isBookingRequestedReschedule, + skipDeleteEventsAndMeetings + ) : placeholderCreatedEvent; // This gets overridden when updating the event - to check if notes have been hidden or not. We just reset this back // to the default description when we are sending the emails. @@ -2237,8 +2238,8 @@ async function handler( const metadata = videoCallUrl ? { - videoCallUrl: getVideoCallUrlFromCalEvent(evt) || videoCallUrl, - } + videoCallUrl: getVideoCallUrlFromCalEvent(evt) || videoCallUrl, + } : undefined; const bookingFlowConfig = { @@ -2331,9 +2332,9 @@ async function handler( ...eventType, metadata: eventType.metadata ? { - ...eventType.metadata, - apps: eventType.metadata?.apps as Prisma.JsonValue, - } + ...eventType.metadata, + apps: eventType.metadata?.apps as Prisma.JsonValue, + } : {}, }, paymentAppCredentials: eventTypePaymentAppCredential as IEventTypePaymentCredentialType, @@ -2377,6 +2378,8 @@ async function handler( }; if (isNormalBookingOrFirstRecurringSlot) { + const creditService = new CreditService(); + await WorkflowService.scheduleWorkflowsFilteredByTriggerEvent({ workflows, smsReminderNumber: smsReminderNumber || null, @@ -2385,6 +2388,7 @@ async function handler( seatReferenceUid: evt.attendeeSeatId, isDryRun, triggers: [WorkflowTriggerEvents.BOOKING_PAYMENT_INITIATED], + creditCheckFn: creditService.hasAvailableCredits.bind(creditService), }); } } catch (error) { @@ -2553,6 +2557,8 @@ async function handler( } try { + const creditService = new CreditService(); + await WorkflowService.scheduleWorkflowsForNewBooking({ workflows, smsReminderNumber: smsReminderNumber || null, @@ -2563,6 +2569,7 @@ async function handler( isConfirmedByDefault, isNormalBookingOrFirstRecurringSlot, isRescheduleEvent: !!rescheduleUid, + creditCheckFn: creditService.hasAvailableCredits.bind(creditService), }); } catch (error) { tracingLogger.error("Error while scheduling workflow reminders", JSON.stringify({ error })); @@ -2630,7 +2637,7 @@ async function handler( * We are open to renaming it to something more descriptive. */ export class RegularBookingService implements IBookingService { - constructor(private readonly deps: IBookingServiceDependencies) { } + constructor(private readonly deps: IBookingServiceDependencies) {} async createBooking(input: { bookingData: CreateRegularBookingData; bookingMeta?: CreateBookingMeta }) { return handler({ bookingData: input.bookingData, ...input.bookingMeta }, this.deps); diff --git a/packages/features/ee/billing/credit-service.ts b/packages/features/ee/billing/credit-service.ts index 8b8b78819f5a6e..40c8760412b443 100644 --- a/packages/features/ee/billing/credit-service.ts +++ b/packages/features/ee/billing/credit-service.ts @@ -48,6 +48,8 @@ type LowCreditBalanceWarningResult = LowCreditBalanceResultBase & { type LowCreditBalanceResult = LowCreditBalanceLimitReachedResult | LowCreditBalanceWarningResult | null; +export type CreditCheckFn = CreditService["hasAvailableCredits"]; + export class CreditService { async chargeCredits({ userId, @@ -158,7 +160,13 @@ export class CreditService { /* also returns true if team has no available credits but limitReachedAt is not yet set */ - async hasAvailableCredits({ userId, teamId }: { userId?: number | null; teamId?: number | null }) { + async hasAvailableCredits({ + userId, + teamId, + }: { + userId?: number | null; + teamId?: number | null; + }): Promise { return await prisma.$transaction(async (tx) => { if (!IS_SMS_CREDITS_ENABLED) return true; diff --git a/packages/features/ee/round-robin/roundRobinManualReassignment.ts b/packages/features/ee/round-robin/roundRobinManualReassignment.ts index e7f9d3f74ef957..43646cfda1d450 100644 --- a/packages/features/ee/round-robin/roundRobinManualReassignment.ts +++ b/packages/features/ee/round-robin/roundRobinManualReassignment.ts @@ -13,6 +13,7 @@ import { getAllCredentialsIncludeServiceAccountKey } from "@calcom/features/book import { getBookingResponsesPartialSchema } from "@calcom/features/bookings/lib/getBookingResponsesSchema"; import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses"; import { getEventTypesFromDB } from "@calcom/features/bookings/lib/handleNewBooking/getEventTypesFromDB"; +import { CreditService } from "@calcom/features/ee/billing/credit-service"; import { getBookerBaseUrl } from "@calcom/features/ee/organizations/lib/getBookerUrlServer"; import AssignmentReasonRecorder, { RRReassignmentType, @@ -602,6 +603,8 @@ export async function handleWorkflowsUpdate({ }, }); + const creditService = new CreditService(); + await scheduleWorkflowReminders({ workflows: newEventWorkflows, smsReminderNumber: null, @@ -612,6 +615,7 @@ export async function handleWorkflowsUpdate({ bookerUrl, }, hideBranding: !!eventType?.owner?.hideBranding, + creditCheckFn: creditService.hasAvailableCredits.bind(creditService), }); } diff --git a/packages/features/ee/workflows/api/scheduleSMSReminders.ts b/packages/features/ee/workflows/api/scheduleSMSReminders.ts index f724516ebc70c0..6958a9c17a82ec 100644 --- a/packages/features/ee/workflows/api/scheduleSMSReminders.ts +++ b/packages/features/ee/workflows/api/scheduleSMSReminders.ts @@ -5,6 +5,7 @@ import { NextResponse } from "next/server"; import dayjs from "@calcom/dayjs"; import { bulkShortenLinks } from "@calcom/ee/workflows/lib/reminders/utils"; import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses"; +import { CreditService } from "@calcom/features/ee/billing/credit-service"; import { getBookerBaseUrl } from "@calcom/features/ee/organizations/lib/getBookerUrlServer"; import { isAttendeeAction } from "@calcom/features/ee/workflows/lib/actionHelperFunctions"; import { scheduleSmsOrFallbackEmail } from "@calcom/features/ee/workflows/lib/reminders/messageDispatcher"; @@ -173,6 +174,8 @@ export async function handler(req: NextRequest) { if (message?.length && message?.length > 0 && sendTo) { const smsMessageWithoutOptOut = await WorkflowOptOutService.addOptOutMessage(message, locale || "en"); + const creditService = new CreditService(); + const scheduledNotification = await scheduleSmsOrFallbackEmail({ twilioData: { phoneNumber: sendTo, @@ -193,6 +196,7 @@ export async function handler(req: NextRequest) { workflowStepId: reminder.workflowStep.id, } : undefined, + creditCheckFn: creditService.hasAvailableCredits.bind(creditService), }); if (scheduledNotification) { diff --git a/packages/features/ee/workflows/api/scheduleWhatsappReminders.ts b/packages/features/ee/workflows/api/scheduleWhatsappReminders.ts index f3083e20a0a26b..6570a518114626 100644 --- a/packages/features/ee/workflows/api/scheduleWhatsappReminders.ts +++ b/packages/features/ee/workflows/api/scheduleWhatsappReminders.ts @@ -3,6 +3,7 @@ import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; import dayjs from "@calcom/dayjs"; +import { CreditService } from "@calcom/features/ee/billing/credit-service"; import { getTranslation } from "@calcom/lib/server/i18n"; import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat"; import prisma from "@calcom/prisma"; @@ -102,6 +103,8 @@ export async function handler(req: NextRequest) { ); if (message?.length && message?.length > 0 && sendTo) { + const creditService = new CreditService(); + const scheduledNotification = await scheduleSmsOrFallbackEmail({ twilioData: { phoneNumber: sendTo, @@ -124,6 +127,7 @@ export async function handler(req: NextRequest) { workflowStepId: reminder.workflowStep.id, } : undefined, + creditCheckFn: creditService.hasAvailableCredits.bind(creditService), }); if (scheduledNotification) { diff --git a/packages/features/ee/workflows/lib/reminders/messageDispatcher.ts b/packages/features/ee/workflows/lib/reminders/messageDispatcher.ts index 086d8d92b717aa..2479d064eaf6f9 100644 --- a/packages/features/ee/workflows/lib/reminders/messageDispatcher.ts +++ b/packages/features/ee/workflows/lib/reminders/messageDispatcher.ts @@ -1,5 +1,6 @@ import type { TFunction } from "i18next"; +import type { CreditCheckFn } from "@calcom/features/ee/billing/credit-service"; import { sendOrScheduleWorkflowEmails } from "@calcom/features/ee/workflows/lib/reminders/providers/emailProvider"; import logger from "@calcom/lib/logger"; import prisma from "@calcom/prisma"; @@ -27,13 +28,11 @@ export async function sendSmsOrFallbackEmail(props: { t: TFunction; replyTo: string; }; + creditCheckFn: CreditCheckFn; }) { const { userId, teamId } = props.twilioData; - const { CreditService } = await import("@calcom/features/ee/billing/credit-service"); - const creditService = new CreditService(); - - const hasCredits = await creditService.hasAvailableCredits({ userId, teamId }); + const hasCredits = await props.creditCheckFn({ userId, teamId }); if (!hasCredits) { const { fallbackData, twilioData } = props; @@ -75,12 +74,11 @@ export async function scheduleSmsOrFallbackEmail(props: { replyTo: string; workflowStepId?: number; }; + creditCheckFn: CreditCheckFn; }) { const { userId, teamId } = props.twilioData; - const { CreditService } = await import("@calcom/features/ee/billing/credit-service"); - const creditService = new CreditService(); - const hasCredits = await creditService.hasAvailableCredits({ userId, teamId }); + const hasCredits = await props.creditCheckFn({ userId, teamId }); if (!hasCredits) { const { fallbackData, twilioData } = props; diff --git a/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts b/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts index 63c47a4971861b..d5fe5a63e2aa03 100644 --- a/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts +++ b/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts @@ -1,4 +1,5 @@ import type { FORM_SUBMITTED_WEBHOOK_RESPONSES } from "@calcom/app-store/routing-forms/lib/formSubmissionUtils"; +import type { CreditCheckFn } from "@calcom/features/ee/billing/credit-service"; import { isAttendeeAction, isSMSAction, @@ -72,6 +73,7 @@ type ProcessWorkflowStepParams = ( export type ScheduleWorkflowRemindersArgs = ProcessWorkflowStepParams & { workflows: Workflow[]; isDryRun?: boolean; + creditCheckFn: CreditCheckFn; }; const processWorkflowStep = async ( @@ -84,7 +86,8 @@ const processWorkflowStep = async ( hideBranding, seatReferenceUid, formData, - }: ProcessWorkflowStepParams + }: ProcessWorkflowStepParams, + creditCheckFn: CreditCheckFn ) => { if (!step?.verifiedAt) return; @@ -114,6 +117,7 @@ const processWorkflowStep = async ( teamId: workflow.teamId, seatReferenceUid, verifiedAt: step.verifiedAt, + creditCheckFn, }; if (isSMSAction(step.action)) { @@ -243,6 +247,7 @@ const _scheduleWorkflowReminders = async (args: ScheduleWorkflowRemindersArgs) = seatReferenceUid, isDryRun = false, formData, + creditCheckFn, } = args; if (isDryRun || !workflows.length) return; @@ -250,13 +255,18 @@ const _scheduleWorkflowReminders = async (args: ScheduleWorkflowRemindersArgs) = if (workflow.steps.length === 0) continue; for (const step of workflow.steps) { - await processWorkflowStep(workflow, step, { - emailAttendeeSendToOverride, - smsReminderNumber, - hideBranding, - seatReferenceUid, - ...(evt ? { calendarEvent: evt } : { formData }), - }); + await processWorkflowStep( + workflow, + step, + { + emailAttendeeSendToOverride, + smsReminderNumber, + hideBranding, + seatReferenceUid, + ...(evt ? { calendarEvent: evt } : { formData }), + }, + creditCheckFn + ); } } }; @@ -266,10 +276,11 @@ export interface SendCancelledRemindersArgs { smsReminderNumber: string | null; evt: ExtendedCalendarEvent; hideBranding?: boolean; + creditCheckFn: CreditCheckFn; } const _sendCancelledReminders = async (args: SendCancelledRemindersArgs) => { - const { smsReminderNumber, evt, workflows, hideBranding } = args; + const { smsReminderNumber, evt, workflows, hideBranding, creditCheckFn } = args; if (!workflows.length) return; @@ -277,11 +288,16 @@ const _sendCancelledReminders = async (args: SendCancelledRemindersArgs) => { if (workflow.trigger !== WorkflowTriggerEvents.EVENT_CANCELLED) continue; for (const step of workflow.steps) { - await processWorkflowStep(workflow, step, { - smsReminderNumber, - hideBranding, - calendarEvent: evt, - }); + await processWorkflowStep( + workflow, + step, + { + smsReminderNumber, + hideBranding, + calendarEvent: evt, + }, + creditCheckFn + ); } } }; diff --git a/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts b/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts index 1b06e6e4de9151..4af48c5a741a96 100644 --- a/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts +++ b/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts @@ -4,6 +4,7 @@ import { getSMSMessageWithVariables, shouldUseTwilio, } from "@calcom/ee/workflows/lib/reminders/utils"; +import type { CreditCheckFn } from "@calcom/features/ee/billing/credit-service"; import { getSubmitterEmail } from "@calcom/features/tasker/tasks/triggerFormSubmittedNoEvent/formSubmissionValidation"; import { SENDER_ID } from "@calcom/lib/constants"; import logger from "@calcom/lib/logger"; @@ -89,6 +90,7 @@ export type ScheduleTextReminderArgs = ScheduleReminderArgs & { isVerificationPending?: boolean; prisma?: PrismaClient; verifiedAt: Date | null; + creditCheckFn: CreditCheckFn; }; export type ScheduleTextReminderArgsWithRequiredFields = Omit< @@ -173,6 +175,7 @@ const scheduleSMSReminderForEvt = async ( userId, teamId, seatReferenceUid, + creditCheckFn, } = args; const { startTime, endTime } = evt; @@ -243,6 +246,7 @@ const scheduleSMSReminderForEvt = async ( replyTo: evt.organizer.email, } : undefined, + creditCheckFn, }); } catch (error) { log.error(`Error sending SMS with error ${error}`); @@ -274,6 +278,7 @@ const scheduleSMSReminderForEvt = async ( workflowStepId, } : undefined, + creditCheckFn, }); if (scheduledNotification?.sid) { @@ -318,7 +323,8 @@ const scheduleSMSReminderForForm = async ( formData: FormSubmissionData; } ) => { - const { message, triggerEvent, reminderPhone, sender, userId, teamId, action, formData } = args; + const { message, triggerEvent, reminderPhone, sender, userId, teamId, action, formData, creditCheckFn } = + args; let smsMessage = message; @@ -362,6 +368,7 @@ const scheduleSMSReminderForForm = async ( replyTo: formData.user.email, } : undefined, + creditCheckFn, }); } catch (error) { log.error(`Error sending SMS with error ${error}`); diff --git a/packages/features/ee/workflows/lib/reminders/whatsappReminderManager.ts b/packages/features/ee/workflows/lib/reminders/whatsappReminderManager.ts index 24b6b550d83b58..9f0990522762be 100644 --- a/packages/features/ee/workflows/lib/reminders/whatsappReminderManager.ts +++ b/packages/features/ee/workflows/lib/reminders/whatsappReminderManager.ts @@ -41,6 +41,7 @@ export const scheduleWhatsappReminder = async (args: ScheduleTextReminderArgs & isVerificationPending = false, seatReferenceUid, verifiedAt, + creditCheckFn, } = args; if (!verifiedAt) { @@ -194,11 +195,12 @@ export const scheduleWhatsappReminder = async (args: ScheduleTextReminderArgs & replyTo: evt.organizer.email, } : undefined, + creditCheckFn, }); } catch (error) { console.log(`Error sending WHATSAPP with error ${error}`); } - } else if ( + }else if ( (triggerEvent === WorkflowTriggerEvents.BEFORE_EVENT || triggerEvent === WorkflowTriggerEvents.AFTER_EVENT) && scheduledDate @@ -230,6 +232,7 @@ export const scheduleWhatsappReminder = async (args: ScheduleTextReminderArgs & workflowStepId, } : undefined, + creditCheckFn, }); if (scheduledNotification?.sid) { diff --git a/packages/features/ee/workflows/lib/service/WorkflowService.test.ts b/packages/features/ee/workflows/lib/service/WorkflowService.test.ts index 04c6a03bd68bdb..61f25dd25da323 100644 --- a/packages/features/ee/workflows/lib/service/WorkflowService.test.ts +++ b/packages/features/ee/workflows/lib/service/WorkflowService.test.ts @@ -76,10 +76,15 @@ describe("WorkflowService.scheduleFormWorkflows", () => { }, ]; + const mockCreditCheckFn = vi.fn().mockResolvedValue(true); + await WorkflowService.scheduleFormWorkflows({ workflows, responses: mockResponses, form: mockForm, + responseId: 123, + routedEventTypeId: null, + creditCheckFn: mockCreditCheckFn, }); expect(mockScheduleWorkflowReminders).toHaveBeenCalledWith({ @@ -87,9 +92,11 @@ describe("WorkflowService.scheduleFormWorkflows", () => { formData: { responses: mockResponses, user: { email: "formowner@example.com", timeFormat: 12, locale: "en" }, + routedEventTypeId: null, }, hideBranding: false, workflows: [workflows[0]], + creditCheckFn: mockCreditCheckFn, }); }); @@ -123,11 +130,15 @@ describe("WorkflowService.scheduleFormWorkflows", () => { mockTasker.create.mockResolvedValue({ id: "task-123" }); + const mockCreditCheckFn = vi.fn().mockResolvedValue(true); + await WorkflowService.scheduleFormWorkflows({ workflows, responses: mockResponses, responseId: 123, form: mockForm, + routedEventTypeId: null, + creditCheckFn: mockCreditCheckFn, }); expect(mockTasker.create).toHaveBeenCalledWith( @@ -137,6 +148,7 @@ describe("WorkflowService.scheduleFormWorkflows", () => { responses: mockResponses, smsReminderNumber: "+1234567890", hideBranding: false, + routedEventTypeId: null, submittedAt: expect.any(Date), form: { id: "form-123", @@ -190,10 +202,15 @@ describe("WorkflowService.scheduleFormWorkflows", () => { }, ]; + const mockCreditCheckFn = vi.fn().mockResolvedValue(true); + await WorkflowService.scheduleFormWorkflows({ workflows, responses: mockResponses, form: formWithoutPhone, + responseId: 123, + routedEventTypeId: null, + creditCheckFn: mockCreditCheckFn, }); expect(mockScheduleWorkflowReminders).toHaveBeenCalledWith({ @@ -201,9 +218,11 @@ describe("WorkflowService.scheduleFormWorkflows", () => { formData: { responses: mockResponses, user: { email: "formowner@example.com", timeFormat: 12, locale: "en" }, + routedEventTypeId: null, }, hideBranding: false, workflows: [workflows[0]], + creditCheckFn: mockCreditCheckFn, }); }); }); diff --git a/packages/features/ee/workflows/lib/service/WorkflowService.ts b/packages/features/ee/workflows/lib/service/WorkflowService.ts index f8550a7241a214..7093440613c4b3 100644 --- a/packages/features/ee/workflows/lib/service/WorkflowService.ts +++ b/packages/features/ee/workflows/lib/service/WorkflowService.ts @@ -4,6 +4,7 @@ import type { ScheduleWorkflowRemindersArgs } from "@calcom/ee/workflows/lib/rem import { scheduleWorkflowReminders } from "@calcom/ee/workflows/lib/reminders/reminderScheduler"; import type { timeUnitLowerCase } from "@calcom/ee/workflows/lib/reminders/smsReminderManager"; import type { Workflow } from "@calcom/ee/workflows/lib/types"; +import type { CreditCheckFn } from "@calcom/features/ee/billing/credit-service"; import { TeamRepository } from "@calcom/features/ee/teams/repositories/TeamRepository"; import { WorkflowRepository } from "@calcom/features/ee/workflows/repositories/WorkflowRepository"; import { getHideBranding } from "@calcom/features/profile/lib/hideBranding"; @@ -87,11 +88,13 @@ export class WorkflowService { form, responseId, routedEventTypeId, + creditCheckFn, }: { responseId: number; workflows: Workflow[]; responses: FORM_SUBMITTED_WEBHOOK_RESPONSES; routedEventTypeId: number | null; + creditCheckFn: CreditCheckFn; form: { id: string; userId: number; @@ -137,6 +140,7 @@ export class WorkflowService { }, hideBranding, workflows: workflowsToTrigger, + creditCheckFn, }); const workflowsToSchedule: Workflow[] = []; diff --git a/packages/features/handleMarkNoShow.ts b/packages/features/handleMarkNoShow.ts index 177ffdceb8abc3..4ed61fe5c43c18 100644 --- a/packages/features/handleMarkNoShow.ts +++ b/packages/features/handleMarkNoShow.ts @@ -1,6 +1,7 @@ import { type TFunction } from "i18next"; import { BookingRepository } from "@calcom/features/bookings/repositories/BookingRepository"; +import { CreditService } from "@calcom/features/ee/billing/credit-service"; import { getBookerBaseUrl } from "@calcom/features/ee/organizations/lib/getBookerUrlServer"; import { workflowSelect } from "@calcom/features/ee/workflows/lib/getAllWorkflows"; import type { ExtendedCalendarEvent } from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler"; @@ -293,12 +294,15 @@ const handleMarkNoShow = async ({ team, }; + const creditService = new CreditService(); + await WorkflowService.scheduleWorkflowsFilteredByTriggerEvent({ workflows, smsReminderNumber: booking.smsReminderNumber, hideBranding: booking.eventType.owner?.hideBranding, calendarEvent, triggers: [WorkflowTriggerEvents.BOOKING_NO_SHOW_UPDATED], + creditCheckFn: creditService.hasAvailableCredits.bind(creditService), }); } catch (error) { logger.error("Error while scheduling workflow reminders for booking no-show updated", error); diff --git a/packages/features/tasker/tasks/triggerFormSubmittedNoEvent/triggerFormSubmittedNoEventWorkflow.ts b/packages/features/tasker/tasks/triggerFormSubmittedNoEvent/triggerFormSubmittedNoEventWorkflow.ts index 7bdf5b5ad63f81..01b79d9f3c7810 100644 --- a/packages/features/tasker/tasks/triggerFormSubmittedNoEvent/triggerFormSubmittedNoEventWorkflow.ts +++ b/packages/features/tasker/tasks/triggerFormSubmittedNoEvent/triggerFormSubmittedNoEventWorkflow.ts @@ -3,6 +3,7 @@ import { z } from "zod"; import type { FORM_SUBMITTED_WEBHOOK_RESPONSES } from "@calcom/app-store/routing-forms/lib/formSubmissionUtils"; import { scheduleWorkflowReminders } from "@calcom/ee/workflows/lib/reminders/reminderScheduler"; import type { Workflow } from "@calcom/ee/workflows/lib/types"; +import { CreditService } from "@calcom/features/ee/billing/credit-service"; import logger from "@calcom/lib/logger"; import { ZWorkflow } from "@calcom/trpc/server/routers/viewer/workflows/getAllActiveWorkflows.schema"; @@ -52,6 +53,8 @@ export async function triggerFormSubmittedNoEventWorkflow(payload: string): Prom if (!shouldTrigger) return; + const creditService = new CreditService(); + try { await scheduleWorkflowReminders({ smsReminderNumber, @@ -62,6 +65,7 @@ export async function triggerFormSubmittedNoEventWorkflow(payload: string): Prom }, hideBranding, workflows: [workflow as Workflow], + creditCheckFn: creditService.hasAvailableCredits.bind(creditService), }); } catch (error) { log.error("Error while triggering form submitted no event workflows", JSON.stringify({ error })); diff --git a/packages/sms/sms-manager.ts b/packages/sms/sms-manager.ts index aefeda704d0576..aafe6a5a421a06 100644 --- a/packages/sms/sms-manager.ts +++ b/packages/sms/sms-manager.ts @@ -1,10 +1,11 @@ import dayjs from "@calcom/dayjs"; +import { CreditService } from "@calcom/features/ee/billing/credit-service"; import { getSenderId } from "@calcom/features/ee/workflows/lib/alphanumericSenderIdSupport"; import { sendSmsOrFallbackEmail } from "@calcom/features/ee/workflows/lib/reminders/messageDispatcher"; -import { checkSMSRateLimit } from "@calcom/lib/smsLockState"; import { SENDER_ID } from "@calcom/lib/constants"; import isSmsCalEmail from "@calcom/lib/isSmsCalEmail"; import { piiHasher } from "@calcom/lib/server/PiiHasher"; +import { checkSMSRateLimit } from "@calcom/lib/smsLockState"; import { TimeFormat } from "@calcom/lib/timeFormat"; import prisma from "@calcom/prisma"; import type { CalendarEvent, Person } from "@calcom/types/Calendar"; @@ -37,6 +38,8 @@ const handleSendingSMS = async ({ rateLimitingType: "sms", }); + const creditService = new CreditService(); + const smsOrFallbackEmail = await sendSmsOrFallbackEmail({ twilioData: { phoneNumber: reminderPhone, @@ -45,6 +48,7 @@ const handleSendingSMS = async ({ ...(teamId ? { teamId } : { userId: organizerUserId }), bookingUid, }, + creditCheckFn: creditService.hasAvailableCredits.bind(creditService), }); return smsOrFallbackEmail; diff --git a/packages/sms/test/sms-manager.test.ts b/packages/sms/test/sms-manager.test.ts index 00f6ba9d62a8d9..06237ce89e8c79 100644 --- a/packages/sms/test/sms-manager.test.ts +++ b/packages/sms/test/sms-manager.test.ts @@ -141,16 +141,18 @@ describe("SMSManager", () => { rateLimitingType: "sms", }); - expect(sendSmsOrFallbackEmail).toHaveBeenCalledWith({ - twilioData: { - phoneNumber: mockCalEvent.attendees[0].phoneNumber, - body: expect.stringContaining(mockCalEvent.attendees[0].name), - sender: expect.any(String), - teamId: undefined, - userId: 1, - bookingUid: "test-booking-uid", - }, - }); + expect(sendSmsOrFallbackEmail).toHaveBeenCalledWith( + expect.objectContaining({ + twilioData: expect.objectContaining({ + phoneNumber: mockCalEvent.attendees[0].phoneNumber, + body: expect.stringContaining(mockCalEvent.attendees[0].name), + sender: expect.any(String), + userId: 1, + bookingUid: "test-booking-uid", + }), + creditCheckFn: expect.any(Function), + }) + ); expect(result).toEqual(mockSmsResponse); }); diff --git a/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts b/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts index 558d98d9960e99..27bea34206eea5 100644 --- a/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts @@ -7,6 +7,7 @@ import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventR import { handleConfirmation } from "@calcom/features/bookings/lib/handleConfirmation"; import { handleWebhookTrigger } from "@calcom/features/bookings/lib/handleWebhookTrigger"; import { processPaymentRefund } from "@calcom/features/bookings/lib/payment/processPaymentRefund"; +import { CreditService } from "@calcom/features/ee/billing/credit-service"; import { getBookerBaseUrl } from "@calcom/features/ee/organizations/lib/getBookerUrlServer"; import { workflowSelect } from "@calcom/features/ee/workflows/lib/getAllWorkflows"; import { WorkflowService } from "@calcom/features/ee/workflows/lib/service/WorkflowService"; @@ -390,6 +391,8 @@ export const confirmHandler = async ({ ctx, input }: ConfirmOptions) => { const workflows = await getAllWorkflowsFromEventType(booking.eventType, user.id); try { + const creditService = new CreditService(); + await WorkflowService.scheduleWorkflowsFilteredByTriggerEvent({ workflows, smsReminderNumber: booking.smsReminderNumber, @@ -403,6 +406,7 @@ export const confirmHandler = async ({ ctx, input }: ConfirmOptions) => { }, hideBranding: !!booking.eventType?.owner?.hideBranding, triggers: [WorkflowTriggerEvents.BOOKING_REJECTED], + creditCheckFn: creditService.hasAvailableCredits.bind(creditService), }); } catch (error) { // Silently fail diff --git a/packages/trpc/server/routers/viewer/workflows/activateEventType.handler.ts b/packages/trpc/server/routers/viewer/workflows/activateEventType.handler.ts index ac0b2f3cabc3d8..8c8b303f0111f1 100644 --- a/packages/trpc/server/routers/viewer/workflows/activateEventType.handler.ts +++ b/packages/trpc/server/routers/viewer/workflows/activateEventType.handler.ts @@ -1,3 +1,4 @@ +import { CreditService } from "@calcom/features/ee/billing/credit-service"; import { getBookerBaseUrl } from "@calcom/features/ee/organizations/lib/getBookerUrlServer"; import { scheduleEmailReminder } from "@calcom/features/ee/workflows/lib/reminders/emailReminderManager"; import { scheduleSMSReminder } from "@calcom/features/ee/workflows/lib/reminders/smsReminderManager"; @@ -314,6 +315,8 @@ export const activateEventTypeHandler = async ({ ctx, input }: ActivateEventType const bookerUrl = await getBookerBaseUrl(ctx.user.organizationId ?? null); + const creditService = new CreditService(); + for (const booking of bookingsForReminders) { // eventTypeId is technically nullable but we know it will be there const bookingEventType = activeOnEventTypes.get(booking.eventTypeId!); @@ -419,6 +422,7 @@ export const activateEventTypeHandler = async ({ ctx, input }: ActivateEventType userId: booking.userId, teamId: eventTypeWorkflow.teamId, verifiedAt: step.verifiedAt, + creditCheckFn: creditService.hasAvailableCredits.bind(creditService), }); } else if (step.action === WorkflowActions.WHATSAPP_NUMBER && step.sendTo) { await scheduleWhatsappReminder({ @@ -436,6 +440,7 @@ export const activateEventTypeHandler = async ({ ctx, input }: ActivateEventType userId: booking.userId, teamId: eventTypeWorkflow.teamId, verifiedAt: step.verifiedAt, + creditCheckFn: creditService.hasAvailableCredits.bind(creditService), }); } else if (booking.smsReminderNumber) { if (step.action === WorkflowActions.SMS_ATTENDEE) { @@ -455,6 +460,7 @@ export const activateEventTypeHandler = async ({ ctx, input }: ActivateEventType userId: booking.userId, teamId: eventTypeWorkflow.teamId, verifiedAt: step.verifiedAt, + creditCheckFn: creditService.hasAvailableCredits.bind(creditService), }); } else if (step.action === WorkflowActions.WHATSAPP_ATTENDEE) { await scheduleWhatsappReminder({ @@ -473,6 +479,7 @@ export const activateEventTypeHandler = async ({ ctx, input }: ActivateEventType userId: booking.userId, teamId: eventTypeWorkflow.teamId, verifiedAt: step.verifiedAt, + creditCheckFn: creditService.hasAvailableCredits.bind(creditService), }); } } diff --git a/packages/trpc/server/routers/viewer/workflows/util.ts b/packages/trpc/server/routers/viewer/workflows/util.ts index 0cd2459e7bc65c..99d5c0d02a30ea 100644 --- a/packages/trpc/server/routers/viewer/workflows/util.ts +++ b/packages/trpc/server/routers/viewer/workflows/util.ts @@ -15,6 +15,7 @@ import { getAIAgentCallPhoneNumberField, getAIAgentCallPhoneNumberSource, } from "@calcom/features/bookings/lib/getBookingFields"; +import { CreditService } from "@calcom/features/ee/billing/credit-service"; import { getBookerBaseUrl } from "@calcom/features/ee/organizations/lib/getBookerUrlServer"; import { WorkflowRepository } from "@calcom/features/ee/workflows/repositories/WorkflowRepository"; import { removeBookingField, upsertBookingField } from "@calcom/features/eventtypes/lib/bookingFieldsManager"; @@ -738,6 +739,8 @@ export async function scheduleBookingReminders( const bookerUrl = await getBookerBaseUrl(isOrg ? teamId : null); + const creditService = new CreditService(); + //create reminders for all bookings for each workflow step const promiseSteps = workflowSteps.map(async (step) => { const promiseScheduleReminders = bookings.map(async (booking) => { @@ -841,6 +844,7 @@ export async function scheduleBookingReminders( userId: userId, teamId: teamId, verifiedAt: step?.verifiedAt ?? null, + creditCheckFn: creditService.hasAvailableCredits.bind(creditService), }); } else if (step.action === WorkflowActions.WHATSAPP_NUMBER && step.sendTo) { await scheduleWhatsappReminder({ @@ -858,6 +862,7 @@ export async function scheduleBookingReminders( userId: userId, teamId: teamId, verifiedAt: step?.verifiedAt ?? null, + creditCheckFn: creditService.hasAvailableCredits.bind(creditService), }); } else if (booking.smsReminderNumber) { if (step.action === WorkflowActions.SMS_ATTENDEE) { @@ -877,6 +882,7 @@ export async function scheduleBookingReminders( userId: userId, teamId: teamId, verifiedAt: step?.verifiedAt ?? null, + creditCheckFn: creditService.hasAvailableCredits.bind(creditService), }); } else if (step.action === WorkflowActions.WHATSAPP_ATTENDEE) { await scheduleWhatsappReminder({ @@ -895,6 +901,7 @@ export async function scheduleBookingReminders( userId: userId, teamId: teamId, verifiedAt: step?.verifiedAt ?? null, + creditCheckFn: creditService.hasAvailableCredits.bind(creditService), }); } } else if (step.action === WorkflowActions.CAL_AI_PHONE_CALL) {