* Add DB schema * Init zod schema * Init RoutingTrace and PendingRoutingTrace repository interfaces * Create PrismaPendingRoutingTraceRepository * Init RoutingTraceService * Create RoutingTraceService container * User routing trace service in routing * Create RoutingTraceRepository and PrismaRoutingTraceRepoistory * Add findByFormResponseId and findByQueuedFormResponseId to PendingRoutingTraceRepository * Update DI containers * RoutingTraceService create process booking method * Use pending routing trace rather than URL params * Fix schema * Fix writing assignment reason for routed booking * Remove from service * Refactor RoutingTraceService to not rely on async local storage * Pass RoutingTraceService through routing call * Add attribute-logic-evaluated to routing trace step * Add routing trace to trpc endpoint * Add CRM routing trace step * Fix extracting routing trace to assignment reason * Add back CRM params to prevent refetching * test: Add unit tests for RoutingTraceService and repositories Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * test: Add missing mock for RoutingTraceService in getRoutedUrl tests Also fix pre-existing lint issues in the test file: - Add explicit types to mockForm and mockSerializableForm variables - Add explicit type to url parameter in mockContext - Replace 'as any' with 'as unknown as InstanceType<typeof UserRepository>' Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * Link pending form submission to routing trace * Clean up * Add lookup field assignment reasons * Rename to PendingRoutingTrace * Add migration file * fix: Update RoutingTraceService tests to use assignmentReasonRepository mock - Add getStepsCount() method back to RoutingTraceService - Add queuedFormResponseId support to processForBooking method - Update tests to use mockAssignmentReasonRepository instead of prisma mock - Remove test for missing routingTraceRepository (all deps now required) Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * fix: Remove PII (organizer email) from log payload in RoutingTraceService Addresses Cubic AI review feedback with confidence 9/10. Logging PII violates sensitive information logging rules. Co-Authored-By: unknown <> * Write field values at the time of routing * Write attributes used to route * fix: Add CHECK constraint to ensure at least one response ID is set in routing trace tables Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * fix: Address PR review comments for routing trace feature - Add checkedFallback to routing trace step data (Comment 11) - Extract hardcoded domain/step strings to constants (Comment 13) - Use @default(now()) for createdAt in PendingRoutingTrace and RoutingTrace (Comment 15) - Add DEFAULT CURRENT_TIMESTAMP to migration for createdAt fields Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * fix: Use UTC-safe timezone expression for createdAt defaults in routing trace tables Addresses Cubic AI review feedback (confidence 9/10) to prevent timezone issues by using dbgenerated("timezone('UTC', now())") instead of bare now() for the createdAt fields in PendingRoutingTrace and RoutingTrace models. Co-Authored-By: unknown <> * fix: Align migration SQL with Prisma's expected timezone syntax Use 'UTC'::text cast in timezone() function to match Prisma's generated SQL. Co-Authored-By: unknown <> * fix: Revert migration SQL to match Prisma schema timezone syntax Remove ::text cast from timezone() function to match what Prisma generates from the schema definition. Co-Authored-By: unknown <> * fix: Use explicit ::text cast in timezone() for PostgreSQL compatibility PostgreSQL normalizes timezone('UTC', now()) to timezone('UTC'::text, now()) internally. Update both schema and migration to use the explicit cast to ensure they match and pass the migration check. Co-Authored-By: unknown <> * feat: Add RoutingFormTraceService domain class for type-safe routing traces - Create RoutingFormTraceService class with routeMatched, fallbackRouteUsed, attributeLogicEvaluated, and attributeFallbackUsed methods - Add ROUTING_FORM_STEPS constants - Add unit tests Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: Integrate RoutingFormTraceService for routing form tracing - Add routingFormTraceService parameter to findMatchingRoute - Record route_matched or fallback_route_used step when route is selected - Use "Default Route" as name for form-level fallback routes - Refactor findTeamMembersMatchingAttributeLogic to use RoutingFormTraceService - Update handleResponse to accept and pass routingFormTraceService - Create RoutingFormTraceService in getRoutedUrl and pass through the flow Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * test: Add test coverage for RoutingFormTraceService integration Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * feat: Integrate RoutingFormTraceService for routing form tracing - Add routingFormTraceService parameter to findMatchingRoute - Record route_matched or fallback_route_used step when route is selected - Use "Default Route" as name for form-level fallback routes - Refactor findTeamMembersMatchingAttributeLogic to use RoutingFormTraceService - Update handleResponse to accept and pass routingFormTraceService - Create RoutingFormTraceService in getRoutedUrl and pass through the flow Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: Restore schema and migration from main branch Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * fix: Use correct parameter name routingFormTraceService in tests Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * fix: Use 'default_route' instead of 'Default Route' for fallback route name Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
89 lines
2.7 KiB
TypeScript
89 lines
2.7 KiB
TypeScript
"use client";
|
|
|
|
import type { RoutingFormTraceService } from "@calcom/features/routing-trace/domains/RoutingFormTraceService";
|
|
import { evaluateRaqbLogic, RaqbLogicResult } from "@calcom/lib/raqb/evaluateRaqbLogic";
|
|
import type { App_RoutingForms_Form } from "@calcom/prisma/client";
|
|
import type { JsonTree } from "react-awesome-query-builder";
|
|
import type { z } from "zod";
|
|
import type { FormResponse, Route, SerializableForm } from "../types/types";
|
|
import type { zodNonRouterRoute } from "../zod";
|
|
import { getQueryBuilderConfigForFormFields } from "./getQueryBuilderConfig";
|
|
import { isFallbackRoute } from "./isFallbackRoute";
|
|
import isRouter from "./isRouter";
|
|
|
|
export function findMatchingRoute({
|
|
form,
|
|
response,
|
|
routingFormTraceService,
|
|
}: {
|
|
form: Pick<SerializableForm<App_RoutingForms_Form>, "routes" | "fields">;
|
|
response: Record<string, Pick<FormResponse[string], "value">>;
|
|
routingFormTraceService?: RoutingFormTraceService;
|
|
}) {
|
|
const queryBuilderConfig = getQueryBuilderConfigForFormFields(form);
|
|
|
|
const routes = form.routes || [];
|
|
|
|
let chosenRoute: Route | null = null;
|
|
|
|
const fallbackRoute = routes.find(isFallbackRoute);
|
|
|
|
if (!fallbackRoute) {
|
|
throw new Error("Fallback route is missing");
|
|
}
|
|
|
|
const routesWithFallbackInEnd = routes
|
|
.flatMap((r) => {
|
|
// For a router, use it's routes instead.
|
|
if (isRouter(r)) return r.routes;
|
|
return r;
|
|
})
|
|
// Use only non fallback routes
|
|
.filter((route) => route && !isFallbackRoute(route))
|
|
// After above flat map, all routes are non router routes.
|
|
.concat([fallbackRoute]) as z.infer<typeof zodNonRouterRoute>[];
|
|
|
|
for (const route of routesWithFallbackInEnd) {
|
|
if (!route) {
|
|
continue;
|
|
}
|
|
|
|
const responseValues: Record<string, FormResponse[string]["value"]> = Object.fromEntries(
|
|
Object.entries(response).map(([uuid, { value }]) => [uuid, value])
|
|
);
|
|
|
|
const result = evaluateRaqbLogic({
|
|
queryValue: route.queryValue as JsonTree,
|
|
queryBuilderConfig,
|
|
data: responseValues,
|
|
});
|
|
|
|
if (result === RaqbLogicResult.MATCH || result === RaqbLogicResult.LOGIC_NOT_FOUND_SO_MATCHED) {
|
|
chosenRoute = route;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!chosenRoute) {
|
|
return null;
|
|
}
|
|
|
|
if (routingFormTraceService) {
|
|
let routeName: string;
|
|
if ("name" in chosenRoute && chosenRoute.name) {
|
|
routeName = chosenRoute.name;
|
|
} else if (isFallbackRoute(chosenRoute)) {
|
|
routeName = "default_route";
|
|
} else {
|
|
routeName = chosenRoute.id;
|
|
}
|
|
if (isFallbackRoute(chosenRoute)) {
|
|
routingFormTraceService.fallbackRouteUsed({ routeId: chosenRoute.id, routeName });
|
|
} else {
|
|
routingFormTraceService.routeMatched({ routeId: chosenRoute.id, routeName });
|
|
}
|
|
}
|
|
|
|
return chosenRoute;
|
|
}
|