fix: break circular dependency by passing creditCheckFn in messageDispatcher (#25343)
* fix: break circular dependency in messageDispatcher via dependency injection Break the 4-file circular dependency chain: credit-service → reminderScheduler → smsReminderManager → messageDispatcher → credit-service Solution: - Add optional creditCheckFn parameter to messageDispatcher functions - Thread creditCheckFn through the call chain: scheduleWorkflowReminders → scheduleSMSReminder/scheduleWhatsappReminder → messageDispatcher - When creditCheckFn is provided, use it; otherwise fall back to dynamic CreditService import for backward compatibility - This breaks the workflows → billing import while preserving immediate fallback behavior Changes: - messageDispatcher: Accept optional creditCheckFn parameter, use it if provided - smsReminderManager: Thread creditCheckFn through scheduleSMSReminder - whatsappReminderManager: Thread creditCheckFn through scheduleWhatsappReminder - reminderScheduler: Add creditCheckFn to ScheduleWorkflowRemindersArgs and pass through processWorkflowStep All type checks, lint checks, and unit tests pass. Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * feat: wire creditCheckFn from all callers to complete circular dependency fix - Add creditCheckFn parameter to WorkflowService.scheduleFormWorkflows - Wire creditCheckFn from all 10 entry points that call workflow scheduling: * formSubmissionUtils.ts (form submissions) * roundRobinManualReassignment.ts (round-robin reassignment) * triggerFormSubmittedNoEventWorkflow.ts (form workflow trigger) * handleBookingRequested.ts (booking requests) * RegularBookingService.ts (2 calls - payment initiated & new bookings) * handleSeats.ts (seated bookings) * handleConfirmation.ts (2 calls - confirmation & payment) * handleMarkNoShow.ts (no-show updates) * confirm.handler.ts (booking rejection) - Update test expectations to use expect.objectContaining() - Fix pre-existing lint warning in handleMarkNoShow.ts (any type) - This completes the messageDispatcher circular dependency fix by ensuring creditCheckFn is actually passed through the call chain, breaking the 4-file circular dependency at runtime Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix: use generic type with type guard in logFailedResults to fix type check error - Replace constrained type with generic type parameter - Add proper type guard for rejected promises - Fixes CI type check failure in handleMarkNoShow.ts:385 - Avoids 'any' type while accepting any fulfilled value shape Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * wip * wip * wip * revert * revert * feat: wire creditCheckFn from all remaining callers to eliminate fallbacks - Wire creditCheckFn in packages/sms/sms-manager.ts (can safely import CreditService) - Create makeHandler factory pattern for CRON endpoints (scheduleSMSReminders.ts, scheduleWhatsappReminders.ts) - Wire creditCheckFn from apps/web CRON routes to factories - Add warning log in messageDispatcher when fallback is used - Complete creditCheckFn wiring from all direct callers (activateEventType.handler.ts, util.ts) This eliminates all fallbacks to dynamic import except as a safety net for unforeseen call sites. The circular dependency (workflows ↔ billing) remains acceptable as discussed with user (Option C). Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * test: update formSubmissionUtils tests to expect creditCheckFn parameter The scheduleFormWorkflows function now receives creditCheckFn as a parameter. Updated test assertions to use expect.objectContaining() with creditCheckFn: expect.any(Function) to account for the new dependency injection parameter. Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * test: update sms-manager test to expect creditCheckFn parameter The sendSmsOrFallbackEmail function now receives creditCheckFn as a parameter. Updated test assertion to use expect.objectContaining() with creditCheckFn: expect.any(Function) to account for the new dependency injection parameter. Also removed teamId: undefined assertion as the key may be omitted entirely from the actual call. Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * feat: make creditCheckFn required to fully break circular dependency This commit completes the circular dependency fix by making creditCheckFn required throughout the call chain, eliminating the dynamic import fallback entirely. Changes: - Make creditCheckFn required in messageDispatcher functions (sendSmsOrFallbackEmail, scheduleSmsOrFallbackEmail) - Remove dynamic import fallback and warning log from messageDispatcher - Make creditCheckFn required in ScheduleTextReminderArgs (smsReminderManager) - Make creditCheckFn required in processWorkflowStep and ScheduleWorkflowRemindersArgs (reminderScheduler) - Add creditCheckFn to SendCancelledRemindersArgs and wire from handleCancelBooking The circular dependency is now fully broken - no more dynamic imports of CreditService from within the workflows package. All callers must explicitly provide creditCheckFn via dependency injection. Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix: make creditCheckFn required in WorkflowService.scheduleFormWorkflows This commit fixes the CI type check error by making creditCheckFn required in WorkflowService.scheduleFormWorkflows. Previously, creditCheckFn was optional in scheduleFormWorkflows but required in scheduleWorkflowReminders, causing a type mismatch. Changes: - Make creditCheckFn required in scheduleFormWorkflows signature - Update WorkflowService.test.ts to pass mock creditCheckFn in all test cases - Add responseId and routedEventTypeId to test calls for completeness All callers of scheduleFormWorkflows already pass creditCheckFn, so this change is safe and completes the circular dependency fix. Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * remove * fix * refactor * refactor * refactor * wip * fix * fix * rm --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
@@ -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),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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 }));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<boolean> {
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
if (!IS_SMS_CREDITS_ENABLED) return true;
|
||||
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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[] = [];
|
||||
|
||||
@@ -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);
|
||||
|
||||
+4
@@ -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 }));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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";
|
||||
@@ -394,6 +395,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,
|
||||
@@ -407,6 +410,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
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user