Files
calendar/packages/features/routing-trace/services/RoutingTraceService.test.ts
T
98b6d63164 refactor: apply biome formatting to packages/features (#27844)
* refactor: apply biome formatting to packages/features (batch 1 - small subdirs)

Format small subdirectories in packages/features: di, flags, holidays, oauth,
settings, users, assignment-reason, selectedCalendar, hashedLink, host, form,
form-builder, availability, data-table, pbac, schedules, troubleshooter,
eventtypes, calendar-subscription, and root-level files.

Also includes straggler apps/web BookEventForm.tsx.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: apply biome formatting to packages/features (batch 2 - medium subdirs)

Format medium subdirectories in packages/features: auth, credentials,
calendars, routing-forms, routing-trace, attributes, watchlist, calAIPhone,
tasker, and webhooks.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: apply biome formatting to packages/features (batch 3 - bookings + insights)

Format bookings and insights subdirectories in packages/features.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: apply biome formatting to packages/features (batch 4 - ee)

Format packages/features/ee subdirectory covering billing, workflows,
organizations, teams, managed-event-types, round-robin, dsync,
integration-attribute-sync, and payments.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: apply biome formatting to packages/features (batch 5 - booking-audit part 1)

Format booking-audit di, actions, common, dto, repository, and types
subdirectories in packages/features/booking-audit.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: apply biome formatting to packages/features (batch 6 - booking-audit part 2)

Format booking-audit service subdirectory in packages/features/booking-audit.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 15:47:14 +01:00

667 lines
22 KiB
TypeScript

import type { AssignmentReasonRepository } from "@calcom/features/assignment-reason/repositories/AssignmentReasonRepository";
import { AssignmentReasonEnum } from "@calcom/prisma/enums";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type {
IPendingRoutingTraceRepository,
PendingRoutingTraceRecord,
} from "../repositories/PendingRoutingTraceRepository.interface";
import type { IRoutingTraceRepository } from "../repositories/RoutingTraceRepository.interface";
import { RoutingTraceService } from "./RoutingTraceService";
describe("RoutingTraceService", () => {
let service: RoutingTraceService;
let mockPendingRoutingTraceRepository: IPendingRoutingTraceRepository;
let mockRoutingTraceRepository: IRoutingTraceRepository;
let mockAssignmentReasonRepository: Pick<AssignmentReasonRepository, "createAssignmentReason">;
function createMockPendingRoutingTraceRepository(): IPendingRoutingTraceRepository {
return {
create: vi.fn(),
findByFormResponseId: vi.fn(),
findByQueuedFormResponseId: vi.fn(),
};
}
function createMockRoutingTraceRepository(): IRoutingTraceRepository {
return {
create: vi.fn(),
};
}
function createMockAssignmentReasonRepository(): Pick<
AssignmentReasonRepository,
"createAssignmentReason"
> {
return {
createAssignmentReason: vi.fn(),
};
}
beforeEach(() => {
vi.clearAllMocks();
mockPendingRoutingTraceRepository = createMockPendingRoutingTraceRepository();
mockRoutingTraceRepository = createMockRoutingTraceRepository();
mockAssignmentReasonRepository = createMockAssignmentReasonRepository();
service = new RoutingTraceService({
pendingRoutingTraceRepository: mockPendingRoutingTraceRepository,
routingTraceRepository: mockRoutingTraceRepository,
assignmentReasonRepository: mockAssignmentReasonRepository as AssignmentReasonRepository,
});
});
describe("addStep", () => {
it("should add a step to the routing trace", () => {
service.addStep({
domain: "routing_form",
step: "attribute-logic-evaluated",
data: { routeIsFallback: false },
});
expect(service.getStepsCount()).toBe(1);
});
it("should add multiple steps to the routing trace", () => {
service.addStep({
domain: "routing_form",
step: "attribute-logic-evaluated",
data: { routeIsFallback: false },
});
service.addStep({
domain: "salesforce",
step: "salesforce_assignment",
data: { email: "owner@example.com" },
});
expect(service.getStepsCount()).toBe(2);
});
it("should add step with empty data by default", () => {
service.addStep({
domain: "test_domain",
step: "test_step",
});
expect(service.getStepsCount()).toBe(1);
});
});
describe("getStepsCount", () => {
it("should return 0 when no steps have been added", () => {
expect(service.getStepsCount()).toBe(0);
});
it("should return correct count after adding steps", () => {
service.addStep({ domain: "test", step: "step1" });
service.addStep({ domain: "test", step: "step2" });
service.addStep({ domain: "test", step: "step3" });
expect(service.getStepsCount()).toBe(3);
});
});
describe("savePendingRoutingTrace", () => {
it("should save pending trace with formResponseId", async () => {
service.addStep({
domain: "routing_form",
step: "attribute-logic-evaluated",
data: { routeIsFallback: false },
});
await service.savePendingRoutingTrace({ formResponseId: 123 });
expect(mockPendingRoutingTraceRepository.create).toHaveBeenCalledWith({
trace: expect.arrayContaining([
expect.objectContaining({
domain: "routing_form",
step: "attribute-logic-evaluated",
data: { routeIsFallback: false },
}),
]),
formResponseId: 123,
});
});
it("should save pending trace with queuedFormResponseId", async () => {
service.addStep({
domain: "routing_form",
step: "attribute-logic-evaluated",
data: { routeIsFallback: true },
});
await service.savePendingRoutingTrace({ queuedFormResponseId: "queued-123" });
expect(mockPendingRoutingTraceRepository.create).toHaveBeenCalledWith({
trace: expect.arrayContaining([
expect.objectContaining({
domain: "routing_form",
step: "attribute-logic-evaluated",
data: { routeIsFallback: true },
}),
]),
queuedFormResponseId: "queued-123",
});
});
});
describe("processForBooking", () => {
const baseProcessArgs = {
bookingId: 1,
bookingUid: "booking-uid-123",
organizerEmail: "organizer@example.com",
isRerouting: false,
};
it("should return null if no pending trace is found", async () => {
vi.mocked(mockPendingRoutingTraceRepository.findByFormResponseId).mockResolvedValue(null);
const result = await service.processForBooking({
...baseProcessArgs,
formResponseId: 123,
});
expect(result).toBeNull();
});
it("should return null if no pending trace is found for queuedFormResponseId", async () => {
vi.mocked(mockPendingRoutingTraceRepository.findByQueuedFormResponseId).mockResolvedValue(null);
const result = await service.processForBooking({
...baseProcessArgs,
queuedFormResponseId: "queued-123",
});
expect(result).toBeNull();
});
describe("routing form assignment", () => {
it("should extract ROUTING_FORM_ROUTING assignment reason", async () => {
const pendingTrace: PendingRoutingTraceRecord = {
id: "pending-1",
createdAt: new Date(),
trace: [
{
domain: "routing_form",
step: "attribute-logic-evaluated",
timestamp: Date.now(),
data: { routeIsFallback: false },
},
],
formResponseId: 123,
queuedFormResponseId: null,
};
vi.mocked(mockPendingRoutingTraceRepository.findByFormResponseId).mockResolvedValue(pendingTrace);
vi.mocked(mockAssignmentReasonRepository.createAssignmentReason).mockResolvedValue({
id: 1,
bookingId: 1,
reasonEnum: AssignmentReasonEnum.ROUTING_FORM_ROUTING,
reasonString: "",
});
vi.mocked(mockRoutingTraceRepository.create).mockResolvedValue({
id: "trace-1",
createdAt: new Date(),
trace: pendingTrace.trace,
formResponseId: 123,
queuedFormResponseId: null,
bookingUid: "booking-uid-123",
assignmentReasonId: 1,
});
const result = await service.processForBooking({
...baseProcessArgs,
formResponseId: 123,
});
expect(result).toEqual({
assignmentReason: {
reasonEnum: AssignmentReasonEnum.ROUTING_FORM_ROUTING,
reasonString: "",
},
});
expect(mockAssignmentReasonRepository.createAssignmentReason).toHaveBeenCalledWith({
bookingId: 1,
reasonEnum: AssignmentReasonEnum.ROUTING_FORM_ROUTING,
reasonString: "",
});
});
it("should extract ROUTING_FORM_ROUTING_FALLBACK assignment reason when routeIsFallback is true", async () => {
const pendingTrace: PendingRoutingTraceRecord = {
id: "pending-1",
createdAt: new Date(),
trace: [
{
domain: "routing_form",
step: "attribute-logic-evaluated",
timestamp: Date.now(),
data: { routeIsFallback: true },
},
],
formResponseId: 123,
queuedFormResponseId: null,
};
vi.mocked(mockPendingRoutingTraceRepository.findByFormResponseId).mockResolvedValue(pendingTrace);
vi.mocked(mockAssignmentReasonRepository.createAssignmentReason).mockResolvedValue({
id: 1,
bookingId: 1,
reasonEnum: AssignmentReasonEnum.ROUTING_FORM_ROUTING_FALLBACK,
reasonString: "",
});
vi.mocked(mockRoutingTraceRepository.create).mockResolvedValue({
id: "trace-1",
createdAt: new Date(),
trace: pendingTrace.trace,
formResponseId: 123,
queuedFormResponseId: null,
bookingUid: "booking-uid-123",
assignmentReasonId: 1,
});
const result = await service.processForBooking({
...baseProcessArgs,
formResponseId: 123,
});
expect(result).toEqual({
assignmentReason: {
reasonEnum: AssignmentReasonEnum.ROUTING_FORM_ROUTING_FALLBACK,
reasonString: "",
},
});
});
it("should extract REROUTED assignment reason when isRerouting is true", async () => {
const pendingTrace: PendingRoutingTraceRecord = {
id: "pending-1",
createdAt: new Date(),
trace: [
{
domain: "routing_form",
step: "attribute-logic-evaluated",
timestamp: Date.now(),
data: { routeIsFallback: false },
},
],
formResponseId: 123,
queuedFormResponseId: null,
};
vi.mocked(mockPendingRoutingTraceRepository.findByFormResponseId).mockResolvedValue(pendingTrace);
vi.mocked(mockAssignmentReasonRepository.createAssignmentReason).mockResolvedValue({
id: 1,
bookingId: 1,
reasonEnum: AssignmentReasonEnum.REROUTED,
reasonString: "Rerouted by admin@example.com",
});
vi.mocked(mockRoutingTraceRepository.create).mockResolvedValue({
id: "trace-1",
createdAt: new Date(),
trace: pendingTrace.trace,
formResponseId: 123,
queuedFormResponseId: null,
bookingUid: "booking-uid-123",
assignmentReasonId: 1,
});
const result = await service.processForBooking({
...baseProcessArgs,
formResponseId: 123,
isRerouting: true,
reroutedByEmail: "admin@example.com",
});
expect(result).toEqual({
assignmentReason: {
reasonEnum: AssignmentReasonEnum.REROUTED,
reasonString: "Rerouted by admin@example.com",
},
});
});
});
describe("salesforce assignment", () => {
it("should extract SALESFORCE_ASSIGNMENT when organizer matches CRM contact owner", async () => {
const pendingTrace: PendingRoutingTraceRecord = {
id: "pending-1",
createdAt: new Date(),
trace: [
{
domain: "salesforce",
step: "salesforce_assignment",
timestamp: Date.now(),
data: {
email: "organizer@example.com",
recordType: "Contact",
recordId: "003ABC123",
},
},
],
formResponseId: 123,
queuedFormResponseId: null,
};
vi.mocked(mockPendingRoutingTraceRepository.findByFormResponseId).mockResolvedValue(pendingTrace);
vi.mocked(mockAssignmentReasonRepository.createAssignmentReason).mockResolvedValue({
id: 1,
bookingId: 1,
reasonEnum: AssignmentReasonEnum.SALESFORCE_ASSIGNMENT,
reasonString: "Salesforce contact owner: organizer@example.com (Contact ID: 003ABC123)",
});
vi.mocked(mockRoutingTraceRepository.create).mockResolvedValue({
id: "trace-1",
createdAt: new Date(),
trace: pendingTrace.trace,
formResponseId: 123,
queuedFormResponseId: null,
bookingUid: "booking-uid-123",
assignmentReasonId: 1,
});
const result = await service.processForBooking({
...baseProcessArgs,
formResponseId: 123,
});
expect(result).toEqual({
assignmentReason: {
reasonEnum: AssignmentReasonEnum.SALESFORCE_ASSIGNMENT,
reasonString: "Salesforce contact owner: organizer@example.com (Contact ID: 003ABC123)",
},
});
});
it("should use account lookup field reason string when rrSkipToAccountLookupField is true", async () => {
const pendingTrace: PendingRoutingTraceRecord = {
id: "pending-1",
createdAt: new Date(),
trace: [
{
domain: "salesforce",
step: "salesforce_assignment",
timestamp: Date.now(),
data: {
email: "organizer@example.com",
recordType: "Account",
recordId: "001ABC123",
rrSkipToAccountLookupField: true,
rrSKipToAccountLookupFieldName: "Account_Owner__c",
},
},
],
formResponseId: 123,
queuedFormResponseId: null,
};
vi.mocked(mockPendingRoutingTraceRepository.findByFormResponseId).mockResolvedValue(pendingTrace);
vi.mocked(mockAssignmentReasonRepository.createAssignmentReason).mockResolvedValue({
id: 1,
bookingId: 1,
reasonEnum: AssignmentReasonEnum.SALESFORCE_ASSIGNMENT,
reasonString:
"Salesforce account lookup field: Account_Owner__c - organizer@example.com (Account ID: 001ABC123)",
});
vi.mocked(mockRoutingTraceRepository.create).mockResolvedValue({
id: "trace-1",
createdAt: new Date(),
trace: pendingTrace.trace,
formResponseId: 123,
queuedFormResponseId: null,
bookingUid: "booking-uid-123",
assignmentReasonId: 1,
});
const result = await service.processForBooking({
...baseProcessArgs,
formResponseId: 123,
});
expect(result).toEqual({
assignmentReason: {
reasonEnum: AssignmentReasonEnum.SALESFORCE_ASSIGNMENT,
reasonString:
"Salesforce account lookup field: Account_Owner__c - organizer@example.com (Account ID: 001ABC123)",
},
});
});
it("should fall back to routing form assignment when organizer does not match CRM contact owner", async () => {
const pendingTrace: PendingRoutingTraceRecord = {
id: "pending-1",
createdAt: new Date(),
trace: [
{
domain: "salesforce",
step: "salesforce_assignment",
timestamp: Date.now(),
data: {
email: "different-owner@example.com",
recordType: "Contact",
recordId: "003ABC123",
},
},
{
domain: "routing_form",
step: "attribute-logic-evaluated",
timestamp: Date.now(),
data: { routeIsFallback: false },
},
],
formResponseId: 123,
queuedFormResponseId: null,
};
vi.mocked(mockPendingRoutingTraceRepository.findByFormResponseId).mockResolvedValue(pendingTrace);
vi.mocked(mockAssignmentReasonRepository.createAssignmentReason).mockResolvedValue({
id: 1,
bookingId: 1,
reasonEnum: AssignmentReasonEnum.ROUTING_FORM_ROUTING,
reasonString: "",
});
vi.mocked(mockRoutingTraceRepository.create).mockResolvedValue({
id: "trace-1",
createdAt: new Date(),
trace: pendingTrace.trace,
formResponseId: 123,
queuedFormResponseId: null,
bookingUid: "booking-uid-123",
assignmentReasonId: 1,
});
const result = await service.processForBooking({
...baseProcessArgs,
formResponseId: 123,
});
expect(result).toEqual({
assignmentReason: {
reasonEnum: AssignmentReasonEnum.ROUTING_FORM_ROUTING,
reasonString: "",
},
});
});
it("should handle case-insensitive email matching for CRM assignment", async () => {
const pendingTrace: PendingRoutingTraceRecord = {
id: "pending-1",
createdAt: new Date(),
trace: [
{
domain: "salesforce",
step: "salesforce_assignment",
timestamp: Date.now(),
data: {
email: "ORGANIZER@EXAMPLE.COM",
recordType: "Contact",
recordId: "003ABC123",
},
},
],
formResponseId: 123,
queuedFormResponseId: null,
};
vi.mocked(mockPendingRoutingTraceRepository.findByFormResponseId).mockResolvedValue(pendingTrace);
vi.mocked(mockAssignmentReasonRepository.createAssignmentReason).mockResolvedValue({
id: 1,
bookingId: 1,
reasonEnum: AssignmentReasonEnum.SALESFORCE_ASSIGNMENT,
reasonString: "Salesforce contact owner: ORGANIZER@EXAMPLE.COM (Contact ID: 003ABC123)",
});
vi.mocked(mockRoutingTraceRepository.create).mockResolvedValue({
id: "trace-1",
createdAt: new Date(),
trace: pendingTrace.trace,
formResponseId: 123,
queuedFormResponseId: null,
bookingUid: "booking-uid-123",
assignmentReasonId: 1,
});
const result = await service.processForBooking({
...baseProcessArgs,
formResponseId: 123,
});
expect(result?.assignmentReason?.reasonEnum).toBe(AssignmentReasonEnum.SALESFORCE_ASSIGNMENT);
});
});
it("should create permanent routing trace with assignment reason", async () => {
const pendingTrace: PendingRoutingTraceRecord = {
id: "pending-1",
createdAt: new Date(),
trace: [
{
domain: "routing_form",
step: "attribute-logic-evaluated",
timestamp: Date.now(),
data: { routeIsFallback: false },
},
],
formResponseId: 123,
queuedFormResponseId: null,
};
vi.mocked(mockPendingRoutingTraceRepository.findByFormResponseId).mockResolvedValue(pendingTrace);
vi.mocked(mockAssignmentReasonRepository.createAssignmentReason).mockResolvedValue({
id: 42,
bookingId: 1,
reasonEnum: AssignmentReasonEnum.ROUTING_FORM_ROUTING,
reasonString: "",
});
vi.mocked(mockRoutingTraceRepository.create).mockResolvedValue({
id: "trace-1",
createdAt: new Date(),
trace: pendingTrace.trace,
formResponseId: 123,
queuedFormResponseId: null,
bookingUid: "booking-uid-123",
assignmentReasonId: 42,
});
await service.processForBooking({
...baseProcessArgs,
formResponseId: 123,
});
expect(mockRoutingTraceRepository.create).toHaveBeenCalledWith({
trace: pendingTrace.trace,
formResponseId: 123,
queuedFormResponseId: undefined,
bookingUid: "booking-uid-123",
assignmentReasonId: 42,
});
});
it("should handle pending trace without assignment reason data", async () => {
const pendingTrace: PendingRoutingTraceRecord = {
id: "pending-1",
createdAt: new Date(),
trace: [
{
domain: "other_domain",
step: "some_step",
timestamp: Date.now(),
data: {},
},
],
formResponseId: 123,
queuedFormResponseId: null,
};
vi.mocked(mockPendingRoutingTraceRepository.findByFormResponseId).mockResolvedValue(pendingTrace);
vi.mocked(mockRoutingTraceRepository.create).mockResolvedValue({
id: "trace-1",
createdAt: new Date(),
trace: pendingTrace.trace,
formResponseId: 123,
queuedFormResponseId: null,
bookingUid: "booking-uid-123",
assignmentReasonId: null,
});
const result = await service.processForBooking({
...baseProcessArgs,
formResponseId: 123,
});
expect(result).toEqual({
assignmentReason: undefined,
});
expect(mockAssignmentReasonRepository.createAssignmentReason).not.toHaveBeenCalled();
expect(mockRoutingTraceRepository.create).toHaveBeenCalledWith({
trace: pendingTrace.trace,
formResponseId: 123,
queuedFormResponseId: undefined,
bookingUid: "booking-uid-123",
assignmentReasonId: undefined,
});
});
it("should process pending trace found by queuedFormResponseId", async () => {
const pendingTrace: PendingRoutingTraceRecord = {
id: "pending-1",
createdAt: new Date(),
trace: [
{
domain: "routing_form",
step: "attribute-logic-evaluated",
timestamp: Date.now(),
data: { routeIsFallback: false },
},
],
formResponseId: null,
queuedFormResponseId: "queued-123",
};
vi.mocked(mockPendingRoutingTraceRepository.findByQueuedFormResponseId).mockResolvedValue(pendingTrace);
vi.mocked(mockAssignmentReasonRepository.createAssignmentReason).mockResolvedValue({
id: 1,
bookingId: 1,
reasonEnum: AssignmentReasonEnum.ROUTING_FORM_ROUTING,
reasonString: "",
});
vi.mocked(mockRoutingTraceRepository.create).mockResolvedValue({
id: "trace-1",
createdAt: new Date(),
trace: pendingTrace.trace,
formResponseId: null,
queuedFormResponseId: "queued-123",
bookingUid: "booking-uid-123",
assignmentReasonId: 1,
});
const result = await service.processForBooking({
...baseProcessArgs,
queuedFormResponseId: "queued-123",
});
expect(mockPendingRoutingTraceRepository.findByQueuedFormResponseId).toHaveBeenCalledWith("queued-123");
expect(result?.assignmentReason?.reasonEnum).toBe(AssignmentReasonEnum.ROUTING_FORM_ROUTING);
});
});
});