* refactor: migrate workflows utilities from trpc to features layer Move workflow utility functions from packages/trpc/server/routers/viewer/workflows/util.ts to packages/features/ee/workflows/lib/workflowUtils.ts to break circular dependencies. Functions migrated: - isAuthorized - getAllWorkflowsFromEventType - scheduleWorkflowNotifications - scheduleBookingReminders Changes: - Created new workflowUtils.ts in features layer with migrated functions - Added getBookingsForWorkflowReminders to WorkflowRepository (no prisma outside repositories) - Updated all imports in features layer to use new location - Updated trpc util.ts to re-export from features for backward compatibility - Fixed pre-existing lint warning (any -> unknown) in handleMarkNoShow.ts This is part of the effort to remove circular dependencies between packages/features and packages/trpc. Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * refactor: remove re-exports from trpc util.ts, update imports to use features layer Per user request, removed the backward compatibility re-exports from packages/trpc/server/routers/viewer/workflows/util.ts and updated all imports in the trpc package to import directly from the features layer. Files updated to import from @calcom/features/ee/workflows/lib/workflowUtils: - confirm.handler.ts (getAllWorkflowsFromEventType) - delete.handler.ts (isAuthorized) - update.handler.ts (isAuthorized, scheduleWorkflowNotifications) - getAllActiveWorkflows.handler.ts (getAllWorkflowsFromEventType) - get.handler.ts (isAuthorized) - util.test.ts (isAuthorized) - delete.handler.test.ts (isAuthorized) Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix: update test imports to use features layer for workflow utilities Updated test files to import scheduleBookingReminders and scheduleWorkflowNotifications from @calcom/features/ee/workflows/lib/workflowUtils instead of @calcom/trpc/server/routers/viewer/workflows/util. Files updated: - packages/features/ee/workflows/lib/test/workflows.test.ts - packages/features/tasker/tasks/scanWorkflowBody.test.ts Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * refactor: split workflowUtils.ts into individual files Split the monolithic workflowUtils.ts into separate files for each function: - isAuthorized.ts - Authorization check for workflow access - getAllWorkflowsFromEventType.ts - Get workflows for an event type - scheduleWorkflowNotifications.ts - Schedule workflow notifications - scheduleBookingReminders.ts - Schedule booking reminders The workflowUtils.ts now re-exports from these individual files for backward compatibility. Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix import * fix more * wip * wip * remove workflowUtils * wip * refactor deleteRemindersOfActiveOnIds * fix --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
282 lines
8.5 KiB
TypeScript
282 lines
8.5 KiB
TypeScript
import { prisma } from "@calcom/prisma/__mocks__/prisma";
|
|
|
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
|
|
import { createDefaultAIPhoneServiceProvider } from "@calcom/features/calAIPhone";
|
|
import { isAuthorized } from "@calcom/features/ee/workflows/lib/isAuthorized";
|
|
import { WorkflowRepository } from "@calcom/features/ee/workflows/repositories/WorkflowRepository";
|
|
import { WorkflowActions } from "@calcom/prisma/enums";
|
|
|
|
import { TRPCError } from "@trpc/server";
|
|
|
|
import { deleteHandler } from "./delete.handler";
|
|
import { removeSmsReminderFieldForEventTypes, removeAIAgentCallPhoneNumberFieldForEventTypes } from "./util";
|
|
|
|
vi.mock("@calcom/prisma", () => ({
|
|
prisma,
|
|
}));
|
|
|
|
vi.mock("@calcom/features/calAIPhone", () => ({
|
|
createDefaultAIPhoneServiceProvider: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("@calcom/features/ee/workflows/repositories/WorkflowRepository", () => ({
|
|
WorkflowRepository: {
|
|
deleteAllWorkflowReminders: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
vi.mock("@calcom/features/ee/workflows/lib/isAuthorized", () => ({
|
|
isAuthorized: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("./util", () => ({
|
|
removeSmsReminderFieldForEventTypes: vi.fn(),
|
|
removeAIAgentCallPhoneNumberFieldForEventTypes: vi.fn(),
|
|
}));
|
|
|
|
describe("deleteHandler", () => {
|
|
const mockCreateDefaultAIPhoneServiceProvider = vi.mocked(createDefaultAIPhoneServiceProvider);
|
|
const mockIsAuthorized = vi.mocked(isAuthorized);
|
|
const mockRemoveSmsReminderFieldForEventTypes = vi.mocked(removeSmsReminderFieldForEventTypes);
|
|
const mockRemoveAIAgentCallPhoneNumberFieldForEventTypes = vi.mocked(
|
|
removeAIAgentCallPhoneNumberFieldForEventTypes
|
|
);
|
|
const mockDeleteAllWorkflowReminders = vi.mocked(WorkflowRepository.deleteAllWorkflowReminders);
|
|
|
|
const mockUser = {
|
|
id: 123,
|
|
name: "Test User",
|
|
email: "test@example.com",
|
|
};
|
|
|
|
const mockCtx = {
|
|
user: mockUser,
|
|
};
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
describe("Authorization", () => {
|
|
it("should throw UNAUTHORIZED when user is not authorized or workflow not found", async () => {
|
|
const workflowId = 1;
|
|
|
|
prisma.workflow.findUnique.mockResolvedValue({
|
|
id: workflowId,
|
|
teamId: 456,
|
|
userId: 789,
|
|
activeOn: [],
|
|
activeOnTeams: [],
|
|
steps: [],
|
|
team: null,
|
|
});
|
|
mockIsAuthorized.mockResolvedValue(false);
|
|
|
|
await expect(deleteHandler({ ctx: mockCtx, input: { id: workflowId } })).rejects.toThrow(
|
|
new TRPCError({ code: "UNAUTHORIZED" })
|
|
);
|
|
|
|
prisma.workflow.findUnique.mockResolvedValue(null);
|
|
|
|
await expect(deleteHandler({ ctx: mockCtx, input: { id: workflowId } })).rejects.toThrow(
|
|
new TRPCError({ code: "UNAUTHORIZED" })
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("Booking field cleanup", () => {
|
|
it("should remove both SMS reminder and AI agent phone number fields", async () => {
|
|
const workflowId = 1;
|
|
const eventTypeIds = [10, 20];
|
|
const mockWorkflow = {
|
|
id: workflowId,
|
|
teamId: null,
|
|
userId: mockUser.id,
|
|
activeOn: eventTypeIds.map((id) => ({ eventTypeId: id })),
|
|
activeOnTeams: [],
|
|
steps: [],
|
|
team: null,
|
|
};
|
|
|
|
prisma.workflow.findUnique.mockResolvedValue(mockWorkflow);
|
|
mockIsAuthorized.mockResolvedValue(true);
|
|
prisma.workflowReminder.findMany.mockResolvedValue([]);
|
|
prisma.workflow.deleteMany.mockResolvedValue({ count: 1 });
|
|
|
|
await deleteHandler({ ctx: mockCtx, input: { id: workflowId } });
|
|
|
|
expect(mockRemoveSmsReminderFieldForEventTypes).toHaveBeenCalledWith({
|
|
activeOnToRemove: eventTypeIds,
|
|
workflowId: workflowId,
|
|
isOrg: false,
|
|
});
|
|
|
|
expect(mockRemoveAIAgentCallPhoneNumberFieldForEventTypes).toHaveBeenCalledWith({
|
|
activeOnToRemove: eventTypeIds,
|
|
workflowId: workflowId,
|
|
isOrg: false,
|
|
});
|
|
});
|
|
|
|
it("should handle organization workflows correctly", async () => {
|
|
const workflowId = 1;
|
|
const teamIds = [100, 200];
|
|
const mockWorkflow = {
|
|
id: workflowId,
|
|
teamId: 456,
|
|
userId: mockUser.id,
|
|
activeOn: [],
|
|
activeOnTeams: teamIds.map((id) => ({ teamId: id })),
|
|
steps: [],
|
|
team: {
|
|
isOrganization: true,
|
|
},
|
|
};
|
|
|
|
prisma.workflow.findUnique.mockResolvedValue(mockWorkflow);
|
|
mockIsAuthorized.mockResolvedValue(true);
|
|
prisma.workflowReminder.findMany.mockResolvedValue([]);
|
|
prisma.workflow.deleteMany.mockResolvedValue({ count: 1 });
|
|
|
|
await deleteHandler({ ctx: mockCtx, input: { id: workflowId } });
|
|
|
|
expect(mockRemoveSmsReminderFieldForEventTypes).toHaveBeenCalledWith({
|
|
activeOnToRemove: teamIds,
|
|
workflowId: workflowId,
|
|
isOrg: true,
|
|
});
|
|
|
|
expect(mockRemoveAIAgentCallPhoneNumberFieldForEventTypes).toHaveBeenCalledWith({
|
|
activeOnToRemove: teamIds,
|
|
workflowId: workflowId,
|
|
isOrg: true,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("CAL AI phone call cleanup", () => {
|
|
let mockAIPhoneService: {
|
|
cancelPhoneNumberSubscription: ReturnType<typeof vi.fn>;
|
|
deletePhoneNumber: ReturnType<typeof vi.fn>;
|
|
deleteAgent: ReturnType<typeof vi.fn>;
|
|
};
|
|
|
|
beforeEach(() => {
|
|
mockAIPhoneService = {
|
|
cancelPhoneNumberSubscription: vi.fn(),
|
|
deletePhoneNumber: vi.fn(),
|
|
deleteAgent: vi.fn(),
|
|
};
|
|
mockCreateDefaultAIPhoneServiceProvider.mockReturnValue(mockAIPhoneService);
|
|
});
|
|
|
|
it("should cleanup AI phone resources based on subscription status", async () => {
|
|
const workflowId = 1;
|
|
const mockWorkflow = {
|
|
id: workflowId,
|
|
teamId: null,
|
|
userId: mockUser.id,
|
|
activeOn: [],
|
|
activeOnTeams: [],
|
|
steps: [
|
|
{
|
|
action: WorkflowActions.CAL_AI_PHONE_CALL,
|
|
agent: {
|
|
id: "agent-1",
|
|
outboundPhoneNumbers: [
|
|
{
|
|
id: "phone-active",
|
|
phoneNumber: "+1111111111",
|
|
subscriptionStatus: "ACTIVE",
|
|
},
|
|
{
|
|
id: "phone-null",
|
|
phoneNumber: "+2222222222",
|
|
subscriptionStatus: null,
|
|
},
|
|
],
|
|
},
|
|
},
|
|
],
|
|
team: null,
|
|
};
|
|
|
|
prisma.workflow.findUnique.mockResolvedValue(mockWorkflow);
|
|
mockIsAuthorized.mockResolvedValue(true);
|
|
prisma.workflowReminder.findMany.mockResolvedValue([]);
|
|
prisma.workflow.deleteMany.mockResolvedValue({ count: 1 });
|
|
|
|
await deleteHandler({ ctx: mockCtx, input: { id: workflowId } });
|
|
|
|
expect(mockAIPhoneService.cancelPhoneNumberSubscription).toHaveBeenCalledWith({
|
|
phoneNumberId: "phone-active",
|
|
userId: mockUser.id,
|
|
});
|
|
|
|
expect(mockAIPhoneService.deletePhoneNumber).toHaveBeenCalledWith({
|
|
phoneNumber: "+2222222222",
|
|
userId: mockUser.id,
|
|
deleteFromDB: true,
|
|
});
|
|
|
|
expect(mockAIPhoneService.deleteAgent).toHaveBeenCalledWith({
|
|
id: "agent-1",
|
|
userId: mockUser.id,
|
|
teamId: undefined,
|
|
});
|
|
|
|
expect(mockRemoveAIAgentCallPhoneNumberFieldForEventTypes).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("Workflow deletion flow", () => {
|
|
it("should complete full deletion flow successfully", async () => {
|
|
const workflowId = 1;
|
|
const mockReminders = [
|
|
{
|
|
id: 1,
|
|
workflowStepId: 1,
|
|
},
|
|
];
|
|
const mockWorkflow = {
|
|
id: workflowId,
|
|
teamId: null,
|
|
userId: mockUser.id,
|
|
activeOn: [{ eventTypeId: 10 }],
|
|
activeOnTeams: [],
|
|
steps: [],
|
|
team: null,
|
|
};
|
|
|
|
prisma.workflow.findUnique.mockResolvedValue(mockWorkflow);
|
|
mockIsAuthorized.mockResolvedValue(true);
|
|
prisma.workflowReminder.findMany.mockResolvedValue(mockReminders);
|
|
prisma.workflow.deleteMany.mockResolvedValue({ count: 1 });
|
|
|
|
const result = await deleteHandler({ ctx: mockCtx, input: { id: workflowId } });
|
|
|
|
expect(prisma.workflowReminder.findMany).toHaveBeenCalledWith({
|
|
where: {
|
|
workflowStep: {
|
|
workflowId: workflowId,
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(mockDeleteAllWorkflowReminders).toHaveBeenCalledWith(mockReminders);
|
|
|
|
expect(mockRemoveSmsReminderFieldForEventTypes).toHaveBeenCalled();
|
|
expect(mockRemoveAIAgentCallPhoneNumberFieldForEventTypes).toHaveBeenCalled();
|
|
|
|
expect(prisma.workflow.deleteMany).toHaveBeenCalledWith({
|
|
where: {
|
|
id: workflowId,
|
|
},
|
|
});
|
|
|
|
expect(result).toEqual({ id: workflowId });
|
|
});
|
|
});
|
|
});
|