Skip to content

Commit 6e69de3

Browse files
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: [email protected] <[email protected]>
1 parent 976f891 commit 6e69de3

File tree

4 files changed

+59
-20
lines changed

4 files changed

+59
-20
lines changed

packages/features/ee/workflows/lib/reminders/messageDispatcher.ts

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ import * as twilio from "./providers/twilioProvider";
99

1010
const log = logger.getSubLogger({ prefix: ["[reminderScheduler]"] });
1111

12+
export type CreditCheckFn = (params: {
13+
userId?: number | null;
14+
teamId?: number | null;
15+
}) => Promise<boolean>;
16+
1217
export async function sendSmsOrFallbackEmail(props: {
1318
twilioData: {
1419
phoneNumber: string;
@@ -27,13 +32,18 @@ export async function sendSmsOrFallbackEmail(props: {
2732
t: TFunction;
2833
replyTo: string;
2934
};
35+
creditCheckFn?: CreditCheckFn;
3036
}) {
3137
const { userId, teamId } = props.twilioData;
32-
const { CreditService } = await import("@calcom/features/ee/billing/credit-service");
33-
34-
const creditService = new CreditService();
35-
36-
const hasCredits = await creditService.hasAvailableCredits({ userId, teamId });
38+
39+
let hasCredits: boolean;
40+
if (props.creditCheckFn) {
41+
hasCredits = await props.creditCheckFn({ userId, teamId });
42+
} else {
43+
const { CreditService } = await import("@calcom/features/ee/billing/credit-service");
44+
const creditService = new CreditService();
45+
hasCredits = await creditService.hasAvailableCredits({ userId, teamId });
46+
}
3747

3848
if (!hasCredits) {
3949
const { fallbackData, twilioData } = props;
@@ -75,12 +85,18 @@ export async function scheduleSmsOrFallbackEmail(props: {
7585
replyTo: string;
7686
workflowStepId?: number;
7787
};
88+
creditCheckFn?: CreditCheckFn;
7889
}) {
7990
const { userId, teamId } = props.twilioData;
80-
const { CreditService } = await import("@calcom/features/ee/billing/credit-service");
81-
const creditService = new CreditService();
82-
83-
const hasCredits = await creditService.hasAvailableCredits({ userId, teamId });
91+
92+
let hasCredits: boolean;
93+
if (props.creditCheckFn) {
94+
hasCredits = await props.creditCheckFn({ userId, teamId });
95+
} else {
96+
const { CreditService } = await import("@calcom/features/ee/billing/credit-service");
97+
const creditService = new CreditService();
98+
hasCredits = await creditService.hasAvailableCredits({ userId, teamId });
99+
}
84100

85101
if (!hasCredits) {
86102
const { fallbackData, twilioData } = props;

packages/features/ee/workflows/lib/reminders/reminderScheduler.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import type { CalendarEvent } from "@calcom/types/Calendar";
2323

2424
import { scheduleAIPhoneCall } from "./aiPhoneCallManager";
2525
import { scheduleEmailReminder } from "./emailReminderManager";
26+
import type { CreditCheckFn } from "./messageDispatcher";
2627
import type { BookingInfo } from "./smsReminderManager";
2728
import { scheduleSMSReminder, type ScheduleTextReminderAction } from "./smsReminderManager";
2829
import { scheduleWhatsappReminder } from "./whatsappReminderManager";
@@ -72,6 +73,7 @@ type ProcessWorkflowStepParams = (
7273
export type ScheduleWorkflowRemindersArgs = ProcessWorkflowStepParams & {
7374
workflows: Workflow[];
7475
isDryRun?: boolean;
76+
creditCheckFn?: CreditCheckFn;
7577
};
7678

7779
const processWorkflowStep = async (
@@ -84,7 +86,8 @@ const processWorkflowStep = async (
8486
hideBranding,
8587
seatReferenceUid,
8688
formData,
87-
}: ProcessWorkflowStepParams
89+
}: ProcessWorkflowStepParams,
90+
creditCheckFn?: CreditCheckFn
8891
) => {
8992
if (!step?.verifiedAt) return;
9093

@@ -114,6 +117,7 @@ const processWorkflowStep = async (
114117
teamId: workflow.teamId,
115118
seatReferenceUid,
116119
verifiedAt: step.verifiedAt,
120+
creditCheckFn,
117121
};
118122

119123
if (isSMSAction(step.action)) {
@@ -243,20 +247,26 @@ const _scheduleWorkflowReminders = async (args: ScheduleWorkflowRemindersArgs) =
243247
seatReferenceUid,
244248
isDryRun = false,
245249
formData,
250+
creditCheckFn,
246251
} = args;
247252
if (isDryRun || !workflows.length) return;
248253

249254
for (const workflow of workflows) {
250255
if (workflow.steps.length === 0) continue;
251256

252257
for (const step of workflow.steps) {
253-
await processWorkflowStep(workflow, step, {
254-
emailAttendeeSendToOverride,
255-
smsReminderNumber,
256-
hideBranding,
257-
seatReferenceUid,
258-
...(evt ? { calendarEvent: evt } : { formData }),
259-
});
258+
await processWorkflowStep(
259+
workflow,
260+
step,
261+
{
262+
emailAttendeeSendToOverride,
263+
smsReminderNumber,
264+
hideBranding,
265+
seatReferenceUid,
266+
...(evt ? { calendarEvent: evt } : { formData }),
267+
},
268+
creditCheckFn
269+
);
260270
}
261271
}
262272
};

packages/features/ee/workflows/lib/reminders/smsReminderManager.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,11 @@ import { IMMEDIATE_WORKFLOW_TRIGGER_EVENTS } from "../constants";
2424
import { WorkflowOptOutContactRepository } from "../repository/workflowOptOutContact";
2525
import { WorkflowOptOutService } from "../service/workflowOptOutService";
2626
import type { ScheduleReminderArgs } from "./emailReminderManager";
27-
import { scheduleSmsOrFallbackEmail, sendSmsOrFallbackEmail } from "./messageDispatcher";
27+
import {
28+
scheduleSmsOrFallbackEmail,
29+
sendSmsOrFallbackEmail,
30+
type CreditCheckFn,
31+
} from "./messageDispatcher";
2832
import * as twilio from "./providers/twilioProvider";
2933
import type { FormSubmissionData } from "./reminderScheduler";
3034
import customTemplate, { transformRoutingFormResponsesToVariableFormat } from "./templates/customTemplate";
@@ -89,6 +93,7 @@ export type ScheduleTextReminderArgs = ScheduleReminderArgs & {
8993
isVerificationPending?: boolean;
9094
prisma?: PrismaClient;
9195
verifiedAt: Date | null;
96+
creditCheckFn?: CreditCheckFn;
9297
};
9398

9499
export type ScheduleTextReminderArgsWithRequiredFields = Omit<
@@ -173,6 +178,7 @@ const scheduleSMSReminderForEvt = async (
173178
userId,
174179
teamId,
175180
seatReferenceUid,
181+
creditCheckFn,
176182
} = args;
177183

178184
const { startTime, endTime } = evt;
@@ -243,6 +249,7 @@ const scheduleSMSReminderForEvt = async (
243249
replyTo: evt.organizer.email,
244250
}
245251
: undefined,
252+
creditCheckFn,
246253
});
247254
} catch (error) {
248255
log.error(`Error sending SMS with error ${error}`);
@@ -274,6 +281,7 @@ const scheduleSMSReminderForEvt = async (
274281
workflowStepId,
275282
}
276283
: undefined,
284+
creditCheckFn,
277285
});
278286

279287
if (scheduledNotification?.sid) {
@@ -318,7 +326,8 @@ const scheduleSMSReminderForForm = async (
318326
formData: FormSubmissionData;
319327
}
320328
) => {
321-
const { message, triggerEvent, reminderPhone, sender, userId, teamId, action, formData } = args;
329+
const { message, triggerEvent, reminderPhone, sender, userId, teamId, action, formData, creditCheckFn } =
330+
args;
322331

323332
let smsMessage = message;
324333

@@ -362,6 +371,7 @@ const scheduleSMSReminderForForm = async (
362371
replyTo: formData.user.email,
363372
}
364373
: undefined,
374+
creditCheckFn,
365375
});
366376
} catch (error) {
367377
log.error(`Error sending SMS with error ${error}`);

packages/features/ee/workflows/lib/reminders/whatsappReminderManager.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export const scheduleWhatsappReminder = async (args: ScheduleTextReminderArgs &
4141
isVerificationPending = false,
4242
seatReferenceUid,
4343
verifiedAt,
44+
creditCheckFn,
4445
} = args;
4546

4647
if (!verifiedAt) {
@@ -194,11 +195,12 @@ export const scheduleWhatsappReminder = async (args: ScheduleTextReminderArgs &
194195
replyTo: evt.organizer.email,
195196
}
196197
: undefined,
198+
creditCheckFn,
197199
});
198200
} catch (error) {
199201
console.log(`Error sending WHATSAPP with error ${error}`);
200202
}
201-
} else if (
203+
}else if (
202204
(triggerEvent === WorkflowTriggerEvents.BEFORE_EVENT ||
203205
triggerEvent === WorkflowTriggerEvents.AFTER_EVENT) &&
204206
scheduledDate
@@ -230,6 +232,7 @@ export const scheduleWhatsappReminder = async (args: ScheduleTextReminderArgs &
230232
workflowStepId,
231233
}
232234
: undefined,
235+
creditCheckFn,
233236
});
234237

235238
if (scheduledNotification?.sid) {

0 commit comments

Comments
 (0)