fix: remove booking question after workflow deletion (#23901)

* fix: remove booking question after workflow deletion

* fix
This commit is contained in:
Udit Takkar
2025-09-23 15:58:56 +00:00
committed by GitHub
parent 224e606439
commit 7e882eedf8
2 changed files with 297 additions and 1 deletions
@@ -0,0 +1,287 @@
import { prisma } from "@calcom/prisma/__mocks__/prisma";
import { describe, it, expect, vi, beforeEach } from "vitest";
import { createDefaultAIPhoneServiceProvider } from "@calcom/features/calAIPhone";
import { WorkflowRepository } from "@calcom/lib/server/repository/workflow";
import { WorkflowActions } from "@calcom/prisma/enums";
import { TRPCError } from "@trpc/server";
import { deleteHandler } from "./delete.handler";
import {
isAuthorized,
removeSmsReminderFieldForEventTypes,
removeAIAgentCallPhoneNumberFieldForEventTypes,
} from "./util";
vi.mock("@calcom/prisma", () => ({
prisma,
}));
vi.mock("@calcom/features/calAIPhone", () => ({
createDefaultAIPhoneServiceProvider: vi.fn(),
}));
vi.mock("@calcom/lib/server/repository/workflow", () => ({
WorkflowRepository: {
deleteAllWorkflowReminders: vi.fn(),
},
}));
vi.mock("./util", () => ({
isAuthorized: vi.fn(),
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,
scheduled: true,
referenceId: "ref-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,
},
scheduled: true,
NOT: {
referenceId: null,
},
},
});
expect(mockDeleteAllWorkflowReminders).toHaveBeenCalledWith(mockReminders);
expect(mockRemoveSmsReminderFieldForEventTypes).toHaveBeenCalled();
expect(mockRemoveAIAgentCallPhoneNumberFieldForEventTypes).toHaveBeenCalled();
expect(prisma.workflow.deleteMany).toHaveBeenCalledWith({
where: {
id: workflowId,
},
});
expect(result).toEqual({ id: workflowId });
});
});
});
@@ -7,7 +7,11 @@ import type { TrpcSessionUser } from "@calcom/trpc/server/types";
import { TRPCError } from "@trpc/server";
import type { TDeleteInputSchema } from "./delete.schema";
import { isAuthorized, removeSmsReminderFieldForEventTypes } from "./util";
import {
isAuthorized,
removeSmsReminderFieldForEventTypes,
removeAIAgentCallPhoneNumberFieldForEventTypes,
} from "./util";
type DeleteOptions = {
ctx: {
@@ -138,6 +142,11 @@ export const deleteHandler = async ({ ctx, input }: DeleteOptions) => {
: workflowToDelete.activeOn.map((activeOn) => activeOn.eventTypeId);
await removeSmsReminderFieldForEventTypes({ activeOnToRemove, workflowId: workflowToDelete.id, isOrg });
await removeAIAgentCallPhoneNumberFieldForEventTypes({
activeOnToRemove,
workflowId: workflowToDelete.id,
isOrg,
});
// automatically deletes all steps and reminders connected to this workflow
await prisma.workflow.deleteMany({