Files
calendar/packages/trpc/server/routers/viewer/workflows/delete.handler.test.ts
T
Benny JooGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
f98acb7301 refactor: migrate workflows utilities from trpc to features layer (#25534)
* 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>
2025-12-16 10:17:46 +00:00

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 });
});
});
});