fix: break circular dependency between reminderScheduler and credit-service (#25312)
* fix: break circular dependency between reminderScheduler and credit-service - Created WorkflowReminderRepository to handle workflow reminder queries - Refactored cancelScheduledMessagesAndScheduleEmails to accept userIdsWithoutCredits parameter - Moved credit-checking logic from workflows to CreditService - Changed dynamic import to static import in CreditService - Updated tests to pass userIdsWithoutCredits parameter - Removed unused imports (prisma, WorkflowMethods) This creates a one-way dependency (billing -> workflows) and follows the repository pattern by removing direct Prisma usage from reminderScheduler. Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix: resolve type check errors in circular dependency fix - Use MembershipRepository.listAcceptedTeamMemberIds instead of findAllAcceptedPublishedTeamMemberships - Re-add prisma import to reminderScheduler.ts for UserRepository - Update WorkflowReminderRepository to use WorkflowActions enum instead of string Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * refactor * refactor * wip * wip --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
@@ -605,11 +605,15 @@ export class CreditService {
|
||||
"@calcom/features/ee/workflows/lib/reminders/reminderScheduler"
|
||||
);
|
||||
promises.push(
|
||||
cancelScheduledMessagesAndScheduleEmails({ teamId: result.teamId, userId: result.userId }).catch(
|
||||
(error) => {
|
||||
log.error("Failed to cancel scheduled messages", error, { result });
|
||||
}
|
||||
)
|
||||
cancelScheduledMessagesAndScheduleEmails({
|
||||
teamId: result.teamId,
|
||||
userIdsWithNoCredits: await this._getUserIdsWithoutCredits({
|
||||
teamId: result.teamId ?? null,
|
||||
userId: result.userId ?? null,
|
||||
}),
|
||||
}).catch((error) => {
|
||||
log.error("Failed to cancel scheduled messages", error, { result });
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -826,4 +830,34 @@ export class CreditService {
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private async _getUserIdsWithoutCredits({
|
||||
teamId,
|
||||
userId,
|
||||
}: {
|
||||
teamId: number | null;
|
||||
userId: number | null;
|
||||
}) {
|
||||
let userIdsWithNoCredits: number[] = userId ? [userId] : [];
|
||||
if (teamId) {
|
||||
const teamMembers = await prisma.membership.findMany({
|
||||
where: {
|
||||
teamId,
|
||||
accepted: true,
|
||||
},
|
||||
});
|
||||
|
||||
userIdsWithNoCredits = (
|
||||
await Promise.all(
|
||||
teamMembers.map(async (member) => {
|
||||
const hasCredits = await this.hasAvailableCredits({ userId: member.userId });
|
||||
return { userId: member.userId, hasCredits };
|
||||
})
|
||||
)
|
||||
)
|
||||
.filter(({ hasCredits }) => !hasCredits)
|
||||
.map(({ userId }) => userId);
|
||||
}
|
||||
return userIdsWithNoCredits;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,8 +34,6 @@ describe("reminderScheduler", () => {
|
||||
|
||||
describe("cancelScheduledMessagesAndScheduleEmails", () => {
|
||||
it("should cancel SMS messages and schedule emails for team", async () => {
|
||||
prismaMock.membership.findMany.mockResolvedValue([]);
|
||||
|
||||
const mockScheduledMessages = [
|
||||
{
|
||||
id: 1,
|
||||
@@ -63,7 +61,7 @@ describe("reminderScheduler", () => {
|
||||
|
||||
prismaMock.workflowReminder.updateMany.mockResolvedValue({ count: 1 });
|
||||
|
||||
await cancelScheduledMessagesAndScheduleEmails({ teamId: 10 });
|
||||
await cancelScheduledMessagesAndScheduleEmails({ teamId: 10, userIdsWithNoCredits: [1, 2, 3] });
|
||||
|
||||
expect(twilioProvider.cancelSMS).toHaveBeenCalledWith("sms-123");
|
||||
|
||||
@@ -76,12 +74,13 @@ describe("reminderScheduler", () => {
|
||||
);
|
||||
|
||||
const callArgs = prismaMock.workflowReminder.findMany.mock.calls[0][0];
|
||||
expect(callArgs.where.workflowStep.workflow.OR).toEqual([{ userId: { in: [] } }, { teamId: 10 }]);
|
||||
expect(callArgs.where.workflowStep.workflow.OR).toEqual([
|
||||
{ userId: { in: [1, 2, 3] } },
|
||||
{ teamId: 10 },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should cancel SMS messages and schedule emails for user", async () => {
|
||||
prismaMock.membership.findMany.mockResolvedValue([]);
|
||||
|
||||
const mockScheduledMessages = [
|
||||
{
|
||||
id: 1,
|
||||
@@ -109,7 +108,7 @@ describe("reminderScheduler", () => {
|
||||
|
||||
prismaMock.workflowReminder.updateMany.mockResolvedValue({ count: 1 });
|
||||
|
||||
await cancelScheduledMessagesAndScheduleEmails({ userId: 11 });
|
||||
await cancelScheduledMessagesAndScheduleEmails({ userIdsWithNoCredits: [11] });
|
||||
|
||||
const callArgs = prismaMock.workflowReminder.findMany.mock.calls[0][0];
|
||||
expect(callArgs.where.workflowStep.workflow.OR).toEqual([{ userId: { in: [11] } }]);
|
||||
|
||||
@@ -11,14 +11,14 @@ import * as twilio from "@calcom/features/ee/workflows/lib/reminders/providers/t
|
||||
import type { Workflow, WorkflowStep } from "@calcom/features/ee/workflows/lib/types";
|
||||
import { getSubmitterEmail } from "@calcom/features/tasker/tasks/triggerFormSubmittedNoEvent/formSubmissionValidation";
|
||||
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
|
||||
import { checkSMSRateLimit } from "@calcom/lib/smsLockState";
|
||||
import { SENDER_NAME } from "@calcom/lib/constants";
|
||||
import { formatCalEventExtended } from "@calcom/lib/formatCalendarEvent";
|
||||
import { withReporting } from "@calcom/lib/sentryWrapper";
|
||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
import { checkSMSRateLimit } from "@calcom/lib/smsLockState";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
import { WorkflowActions, WorkflowMethods, WorkflowTriggerEvents } from "@calcom/prisma/enums";
|
||||
import { WorkflowActions, WorkflowTriggerEvents } from "@calcom/prisma/enums";
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
|
||||
import { scheduleAIPhoneCall } from "./aiPhoneCallManager";
|
||||
@@ -288,86 +288,18 @@ const _sendCancelledReminders = async (args: SendCancelledRemindersArgs) => {
|
||||
|
||||
const _cancelScheduledMessagesAndScheduleEmails = async ({
|
||||
teamId,
|
||||
userId,
|
||||
userIdsWithNoCredits,
|
||||
}: {
|
||||
teamId?: number | null;
|
||||
userId?: number | null;
|
||||
userIdsWithNoCredits: number[];
|
||||
}) => {
|
||||
const { CreditService } = await import("@calcom/features/ee/billing/credit-service");
|
||||
const { WorkflowReminderRepository } = await import(
|
||||
"@calcom/features/ee/workflows/repositories/WorkflowReminderRepository"
|
||||
);
|
||||
|
||||
let userIdsWithNoCredits: number[] = userId ? [userId] : [];
|
||||
|
||||
if (teamId) {
|
||||
const teamMembers = await prisma.membership.findMany({
|
||||
where: {
|
||||
teamId,
|
||||
accepted: true,
|
||||
},
|
||||
});
|
||||
|
||||
const creditService = new CreditService();
|
||||
|
||||
userIdsWithNoCredits = (
|
||||
await Promise.all(
|
||||
teamMembers.map(async (member) => {
|
||||
const hasCredits = await creditService.hasAvailableCredits({ userId: member.userId });
|
||||
return { userId: member.userId, hasCredits };
|
||||
})
|
||||
)
|
||||
)
|
||||
.filter(({ hasCredits }) => !hasCredits)
|
||||
.map(({ userId }) => userId);
|
||||
}
|
||||
|
||||
const scheduledMessages = await prisma.workflowReminder.findMany({
|
||||
where: {
|
||||
workflowStep: {
|
||||
workflow: {
|
||||
OR: [
|
||||
{
|
||||
userId: {
|
||||
in: userIdsWithNoCredits,
|
||||
},
|
||||
},
|
||||
...(teamId ? [{ teamId }] : []),
|
||||
],
|
||||
},
|
||||
},
|
||||
scheduled: true,
|
||||
OR: [{ cancelled: false }, { cancelled: null }],
|
||||
referenceId: {
|
||||
not: null,
|
||||
},
|
||||
method: {
|
||||
in: [WorkflowMethods.SMS, WorkflowMethods.WHATSAPP],
|
||||
},
|
||||
},
|
||||
select: {
|
||||
referenceId: true,
|
||||
workflowStep: {
|
||||
select: {
|
||||
action: true,
|
||||
},
|
||||
},
|
||||
scheduledDate: true,
|
||||
uuid: true,
|
||||
id: true,
|
||||
booking: {
|
||||
select: {
|
||||
attendees: {
|
||||
select: {
|
||||
email: true,
|
||||
locale: true,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
select: {
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
const scheduledMessages = await WorkflowReminderRepository.findScheduledMessagesToCancel({
|
||||
teamId,
|
||||
userIdsWithNoCredits,
|
||||
});
|
||||
|
||||
await Promise.allSettled(scheduledMessages.map((msg) => twilio.cancelSMS(msg.referenceId ?? "")));
|
||||
@@ -393,16 +325,8 @@ const _cancelScheduledMessagesAndScheduleEmails = async ({
|
||||
})
|
||||
);
|
||||
|
||||
await prisma.workflowReminder.updateMany({
|
||||
where: {
|
||||
id: {
|
||||
in: scheduledMessages.map((msg) => msg.id),
|
||||
},
|
||||
},
|
||||
data: {
|
||||
method: WorkflowMethods.EMAIL,
|
||||
referenceId: null,
|
||||
},
|
||||
await WorkflowReminderRepository.updateRemindersToEmail({
|
||||
reminderIds: scheduledMessages.map((msg) => msg.id),
|
||||
});
|
||||
};
|
||||
// Export functions wrapped with withReporting
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { WorkflowMethods } from "@calcom/prisma/enums";
|
||||
|
||||
export class WorkflowReminderRepository {
|
||||
static async findScheduledMessagesToCancel({
|
||||
teamId,
|
||||
userIdsWithNoCredits,
|
||||
}: {
|
||||
teamId?: number | null;
|
||||
userIdsWithNoCredits: number[];
|
||||
}) {
|
||||
return await prisma.workflowReminder.findMany({
|
||||
where: {
|
||||
workflowStep: {
|
||||
workflow: {
|
||||
OR: [
|
||||
{
|
||||
userId: {
|
||||
in: userIdsWithNoCredits,
|
||||
},
|
||||
},
|
||||
...(teamId ? [{ teamId }] : []),
|
||||
],
|
||||
},
|
||||
},
|
||||
scheduled: true,
|
||||
OR: [{ cancelled: false }, { cancelled: null }],
|
||||
referenceId: {
|
||||
not: null,
|
||||
},
|
||||
method: {
|
||||
in: [WorkflowMethods.SMS, WorkflowMethods.WHATSAPP],
|
||||
},
|
||||
},
|
||||
select: {
|
||||
referenceId: true,
|
||||
workflowStep: {
|
||||
select: {
|
||||
action: true,
|
||||
},
|
||||
},
|
||||
scheduledDate: true,
|
||||
uuid: true,
|
||||
id: true,
|
||||
booking: {
|
||||
select: {
|
||||
attendees: {
|
||||
select: {
|
||||
email: true,
|
||||
locale: true,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
select: {
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
static async updateRemindersToEmail({ reminderIds }: { reminderIds: number[] }): Promise<void> {
|
||||
await prisma.workflowReminder.updateMany({
|
||||
where: {
|
||||
id: {
|
||||
in: reminderIds,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
method: WorkflowMethods.EMAIL,
|
||||
referenceId: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user