mirror of
https://github.com/hanzoai/calendar.git
synced 2026-10-30 14:46:43 +00:00
* feat: Add routing trace presenters Add domain-specific presenters (SalesforceRoutingTracePresenter, RoutingFormTracePresenter) that format trace steps into human-readable strings, and a core RoutingTracePresenter that delegates to them based on step domain. Includes unit tests for all presenters. Co-Authored-By: Claude Opus 4.5 <[email protected]> * feat: Add findByBookingUid to RoutingTraceRepository Add method to look up a routing trace by booking UID, needed by the routing trace presenter tRPC endpoint. Co-Authored-By: Claude Opus 4.5 <[email protected]> * feat: Add getRoutingTrace tRPC endpoint Expose routing trace data for a booking via viewer.bookings.getRoutingTrace. Tries the permanent RoutingTrace first (round robin bookings), then falls back to PendingRoutingTrace via the booking's form response relation. Co-Authored-By: Claude Opus 4.5 <[email protected]> * feat: Add routing trace side sheet to booking list item Add a route icon button on booking list items that came from routing forms. Clicking it opens a side sheet displaying the full routing trace as human-readable steps. Adds RoutingTraceSheet component, store state, and translation key. Co-Authored-By: Claude Opus 4.5 <[email protected]> * refactor: Move routing trace action to dropdown menu Move the routing trace button from a standalone icon on the booking list item into the actions dropdown menu alongside "Report wrong assignment". The RoutingTraceSheet is now rendered from BookingActionsDropdown. Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix: Improve routing trace UI and handle unnamed routes - Redesign RoutingTraceSheet with vertical timeline layout, domain badges, skeleton loading state, and millisecond timestamps - Use Salesforce app-store icon for Salesforce steps - Fall back to "Unnamed route" when route name is missing or matches route ID - Fix dropdown menu icon to git-merge (valid icon, unique in menu) Co-Authored-By: Claude Opus 4.5 <[email protected]> * test: Add tests for getRoutingTrace handler and findByBookingUid repository method Co-Authored-By: [email protected] <[email protected]> * Abstract functions * Fix build error * Add permission check * fix: Update getRoutingTrace tests to include ctx and mock BookingAccessService Co-Authored-By: [email protected] <[email protected]> --------- Co-authored-by: Claude Opus 4.5 <[email protected]> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
347 lines
9.8 KiB
TypeScript
347 lines
9.8 KiB
TypeScript
import type { PrismaClient } from "@calcom/prisma";
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { PrismaRoutingTraceRepository } from "./PrismaRoutingTraceRepository";
|
|
|
|
describe("PrismaRoutingTraceRepository", () => {
|
|
let repository: PrismaRoutingTraceRepository;
|
|
|
|
const mockPrisma = {
|
|
routingTrace: {
|
|
create: vi.fn(),
|
|
findFirst: vi.fn(),
|
|
},
|
|
} as unknown as PrismaClient;
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
repository = new PrismaRoutingTraceRepository(mockPrisma);
|
|
});
|
|
|
|
describe("create", () => {
|
|
it("should create a routing trace with all fields", async () => {
|
|
const trace = [
|
|
{
|
|
domain: "routing_form",
|
|
step: "attribute-logic-evaluated",
|
|
timestamp: Date.now(),
|
|
data: { routeIsFallback: false },
|
|
},
|
|
];
|
|
|
|
const mockResult = {
|
|
id: "trace-1",
|
|
createdAt: new Date("2025-01-01T10:00:00Z"),
|
|
trace,
|
|
formResponseId: 123,
|
|
queuedFormResponseId: null,
|
|
bookingUid: "booking-uid-123",
|
|
assignmentReasonId: 42,
|
|
};
|
|
|
|
vi.mocked(mockPrisma.routingTrace.create).mockResolvedValue(mockResult);
|
|
|
|
const result = await repository.create({
|
|
trace,
|
|
formResponseId: 123,
|
|
bookingUid: "booking-uid-123",
|
|
assignmentReasonId: 42,
|
|
});
|
|
|
|
expect(mockPrisma.routingTrace.create).toHaveBeenCalledWith({
|
|
data: {
|
|
createdAt: expect.any(Date),
|
|
trace,
|
|
formResponseId: 123,
|
|
queuedFormResponseId: undefined,
|
|
bookingUid: "booking-uid-123",
|
|
assignmentReasonId: 42,
|
|
},
|
|
});
|
|
expect(result).toEqual({
|
|
id: "trace-1",
|
|
createdAt: new Date("2025-01-01T10:00:00Z"),
|
|
trace,
|
|
formResponseId: 123,
|
|
queuedFormResponseId: null,
|
|
bookingUid: "booking-uid-123",
|
|
assignmentReasonId: 42,
|
|
});
|
|
});
|
|
|
|
it("should create a routing trace with queuedFormResponseId", async () => {
|
|
const trace = [
|
|
{
|
|
domain: "salesforce",
|
|
step: "salesforce_assignment",
|
|
timestamp: Date.now(),
|
|
data: { email: "[email protected]" },
|
|
},
|
|
];
|
|
|
|
const mockResult = {
|
|
id: "trace-2",
|
|
createdAt: new Date("2025-01-02T10:00:00Z"),
|
|
trace,
|
|
formResponseId: null,
|
|
queuedFormResponseId: "queued-456",
|
|
bookingUid: "booking-uid-456",
|
|
assignmentReasonId: 100,
|
|
};
|
|
|
|
vi.mocked(mockPrisma.routingTrace.create).mockResolvedValue(mockResult);
|
|
|
|
const result = await repository.create({
|
|
trace,
|
|
queuedFormResponseId: "queued-456",
|
|
bookingUid: "booking-uid-456",
|
|
assignmentReasonId: 100,
|
|
});
|
|
|
|
expect(mockPrisma.routingTrace.create).toHaveBeenCalledWith({
|
|
data: {
|
|
createdAt: expect.any(Date),
|
|
trace,
|
|
formResponseId: undefined,
|
|
queuedFormResponseId: "queued-456",
|
|
bookingUid: "booking-uid-456",
|
|
assignmentReasonId: 100,
|
|
},
|
|
});
|
|
expect(result).toEqual({
|
|
id: "trace-2",
|
|
createdAt: new Date("2025-01-02T10:00:00Z"),
|
|
trace,
|
|
formResponseId: null,
|
|
queuedFormResponseId: "queued-456",
|
|
bookingUid: "booking-uid-456",
|
|
assignmentReasonId: 100,
|
|
});
|
|
});
|
|
|
|
it("should create a routing trace without assignmentReasonId", async () => {
|
|
const trace = [
|
|
{
|
|
domain: "other_domain",
|
|
step: "some_step",
|
|
timestamp: Date.now(),
|
|
data: {},
|
|
},
|
|
];
|
|
|
|
const mockResult = {
|
|
id: "trace-3",
|
|
createdAt: new Date("2025-01-03T10:00:00Z"),
|
|
trace,
|
|
formResponseId: 789,
|
|
queuedFormResponseId: null,
|
|
bookingUid: "booking-uid-789",
|
|
assignmentReasonId: null,
|
|
};
|
|
|
|
vi.mocked(mockPrisma.routingTrace.create).mockResolvedValue(mockResult);
|
|
|
|
const result = await repository.create({
|
|
trace,
|
|
formResponseId: 789,
|
|
bookingUid: "booking-uid-789",
|
|
});
|
|
|
|
expect(mockPrisma.routingTrace.create).toHaveBeenCalledWith({
|
|
data: {
|
|
createdAt: expect.any(Date),
|
|
trace,
|
|
formResponseId: 789,
|
|
queuedFormResponseId: undefined,
|
|
bookingUid: "booking-uid-789",
|
|
assignmentReasonId: undefined,
|
|
},
|
|
});
|
|
expect(result).toEqual({
|
|
id: "trace-3",
|
|
createdAt: new Date("2025-01-03T10:00:00Z"),
|
|
trace,
|
|
formResponseId: 789,
|
|
queuedFormResponseId: null,
|
|
bookingUid: "booking-uid-789",
|
|
assignmentReasonId: null,
|
|
});
|
|
});
|
|
|
|
it("should create a routing trace with multiple trace steps", async () => {
|
|
const trace = [
|
|
{
|
|
domain: "salesforce",
|
|
step: "salesforce_assignment",
|
|
timestamp: Date.now() - 1000,
|
|
data: { email: "[email protected]", recordType: "Contact", recordId: "003ABC" },
|
|
},
|
|
{
|
|
domain: "routing_form",
|
|
step: "attribute-logic-evaluated",
|
|
timestamp: Date.now(),
|
|
data: { routeIsFallback: false },
|
|
},
|
|
];
|
|
|
|
const mockResult = {
|
|
id: "trace-4",
|
|
createdAt: new Date("2025-01-04T10:00:00Z"),
|
|
trace,
|
|
formResponseId: 111,
|
|
queuedFormResponseId: null,
|
|
bookingUid: "booking-uid-111",
|
|
assignmentReasonId: 50,
|
|
};
|
|
|
|
vi.mocked(mockPrisma.routingTrace.create).mockResolvedValue(mockResult);
|
|
|
|
const result = await repository.create({
|
|
trace,
|
|
formResponseId: 111,
|
|
bookingUid: "booking-uid-111",
|
|
assignmentReasonId: 50,
|
|
});
|
|
|
|
expect(mockPrisma.routingTrace.create).toHaveBeenCalledWith({
|
|
data: {
|
|
createdAt: expect.any(Date),
|
|
trace,
|
|
formResponseId: 111,
|
|
queuedFormResponseId: undefined,
|
|
bookingUid: "booking-uid-111",
|
|
assignmentReasonId: 50,
|
|
},
|
|
});
|
|
expect(result.trace).toHaveLength(2);
|
|
});
|
|
|
|
it("should create a routing trace with empty trace array", async () => {
|
|
const trace: never[] = [];
|
|
|
|
const mockResult = {
|
|
id: "trace-5",
|
|
createdAt: new Date("2025-01-05T10:00:00Z"),
|
|
trace,
|
|
formResponseId: 222,
|
|
queuedFormResponseId: null,
|
|
bookingUid: "booking-uid-222",
|
|
assignmentReasonId: null,
|
|
};
|
|
|
|
vi.mocked(mockPrisma.routingTrace.create).mockResolvedValue(mockResult);
|
|
|
|
const result = await repository.create({
|
|
trace,
|
|
formResponseId: 222,
|
|
bookingUid: "booking-uid-222",
|
|
});
|
|
|
|
expect(mockPrisma.routingTrace.create).toHaveBeenCalledWith({
|
|
data: {
|
|
createdAt: expect.any(Date),
|
|
trace,
|
|
formResponseId: 222,
|
|
queuedFormResponseId: undefined,
|
|
bookingUid: "booking-uid-222",
|
|
assignmentReasonId: undefined,
|
|
},
|
|
});
|
|
expect(result.trace).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("findByBookingUid", () => {
|
|
it("should return routing trace when found", async () => {
|
|
const trace = [
|
|
{
|
|
domain: "routing_form",
|
|
step: "route_matched",
|
|
timestamp: Date.now(),
|
|
data: { routeId: "route-1", routeName: "Enterprise" },
|
|
},
|
|
];
|
|
|
|
const mockResult = {
|
|
id: "trace-1",
|
|
createdAt: new Date("2025-01-01T10:00:00Z"),
|
|
trace,
|
|
formResponseId: 123,
|
|
queuedFormResponseId: null,
|
|
bookingUid: "booking-uid-123",
|
|
assignmentReasonId: 42,
|
|
};
|
|
|
|
vi.mocked(mockPrisma.routingTrace.findFirst).mockResolvedValue(mockResult);
|
|
|
|
const result = await repository.findByBookingUid("booking-uid-123");
|
|
|
|
expect(mockPrisma.routingTrace.findFirst).toHaveBeenCalledWith({
|
|
where: { bookingUid: "booking-uid-123" },
|
|
});
|
|
expect(result).toEqual({
|
|
id: "trace-1",
|
|
createdAt: new Date("2025-01-01T10:00:00Z"),
|
|
trace,
|
|
formResponseId: 123,
|
|
queuedFormResponseId: null,
|
|
bookingUid: "booking-uid-123",
|
|
assignmentReasonId: 42,
|
|
});
|
|
});
|
|
|
|
it("should return null when routing trace not found", async () => {
|
|
vi.mocked(mockPrisma.routingTrace.findFirst).mockResolvedValue(null);
|
|
|
|
const result = await repository.findByBookingUid("non-existent-uid");
|
|
|
|
expect(mockPrisma.routingTrace.findFirst).toHaveBeenCalledWith({
|
|
where: { bookingUid: "non-existent-uid" },
|
|
});
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it("should return routing trace with multiple steps", async () => {
|
|
const trace = [
|
|
{
|
|
domain: "routing_form",
|
|
step: "route_matched",
|
|
timestamp: 1000,
|
|
data: { routeId: "route-1", routeName: "Sales" },
|
|
},
|
|
{
|
|
domain: "salesforce",
|
|
step: "graphql_query_initiated",
|
|
timestamp: 2000,
|
|
data: { email: "[email protected]", emailDomain: "acme.com" },
|
|
},
|
|
{
|
|
domain: "salesforce",
|
|
step: "salesforce_assignment",
|
|
timestamp: 3000,
|
|
data: { email: "[email protected]", recordType: "Contact", recordId: "003ABC" },
|
|
},
|
|
];
|
|
|
|
const mockResult = {
|
|
id: "trace-2",
|
|
createdAt: new Date("2025-01-02T10:00:00Z"),
|
|
trace,
|
|
formResponseId: 456,
|
|
queuedFormResponseId: null,
|
|
bookingUid: "booking-uid-456",
|
|
assignmentReasonId: 100,
|
|
};
|
|
|
|
vi.mocked(mockPrisma.routingTrace.findFirst).mockResolvedValue(mockResult);
|
|
|
|
const result = await repository.findByBookingUid("booking-uid-456");
|
|
|
|
expect(result).not.toBeNull();
|
|
expect(result?.trace).toHaveLength(3);
|
|
expect(result?.trace[0].domain).toBe("routing_form");
|
|
expect(result?.trace[1].domain).toBe("salesforce");
|
|
expect(result?.trace[2].domain).toBe("salesforce");
|
|
});
|
|
});
|
|
});
|