diff --git a/packages/features/insights/components/routing/FailedBookingsByField.tsx b/packages/features/insights/components/routing/FailedBookingsByField.tsx index c8dd1d8d398202..9ed58e9958c250 100644 --- a/packages/features/insights/components/routing/FailedBookingsByField.tsx +++ b/packages/features/insights/components/routing/FailedBookingsByField.tsx @@ -16,7 +16,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale"; import { trpc } from "@calcom/trpc"; import { ToggleGroup } from "@calcom/ui/components/form"; -import { useInsightsParameters } from "../../hooks/useInsightsParameters"; +import { useInsightsRoutingParameters } from "../../hooks/useInsightsRoutingParameters"; import { ChartCard } from "../ChartCard"; // Custom Tooltip component @@ -131,13 +131,8 @@ function FormCard({ formName, fields }: FormCardProps) { export function FailedBookingsByField() { const { t } = useLocale(); - const { userId, teamId, startDate, endDate, isAll, routingFormId } = useInsightsParameters(); - const { data } = trpc.viewer.insights.failedBookingsByField.useQuery({ - userId, - teamId, - isAll, - routingFormId, - }); + const insightsRoutingParams = useInsightsRoutingParameters(); + const { data } = trpc.viewer.insights.failedBookingsByField.useQuery(insightsRoutingParams); if (!data || Object.entries(data).length === 0) return null; diff --git a/packages/features/insights/server/routing-events.ts b/packages/features/insights/server/routing-events.ts index 4b00c39ab4229e..319b72abc05b31 100644 --- a/packages/features/insights/server/routing-events.ts +++ b/packages/features/insights/server/routing-events.ts @@ -215,143 +215,6 @@ class RoutingEventsInsights { return fields; } - static async getFailedBookingsByRoutingFormGroup({ - userId, - teamId, - isAll, - routingFormId, - organizationId, - }: RoutingFormInsightsTeamFilter) { - const formsWhereCondition = await this.getWhereForTeamOrAllTeams({ - userId, - teamId, - isAll, - organizationId, - routingFormId, - }); - - const teamConditions = []; - - // @ts-expect-error it doesn't exist but TS isn't smart enough when it's a number or int filter - if (formsWhereCondition.teamId?.in) { - // @ts-expect-error it doesn't exist but TS isn't smart enough when it's a number or int filter - teamConditions.push(`f."teamId" IN (${formsWhereCondition.teamId.in.join(",")})`); - } - // @ts-expect-error it doesn't exist but TS isn't smart enough when it's a number or int filter - if (!formsWhereCondition.teamId?.in && userId) { - teamConditions.push(`f."userId" = ${userId}`); - } - if (routingFormId) { - teamConditions.push(`f.id = '${routingFormId}'`); - } - - const whereClause = teamConditions.length - ? Prisma.sql`AND ${Prisma.raw(teamConditions.join(" AND "))}` - : Prisma.sql``; - - // If you're at this point wondering what this does. This groups the responses by form and field and counts the number of responses for each option that don't have a booking. - const result = await prisma.$queryRaw< - { - formId: string; - formName: string; - fieldId: string; - fieldLabel: string; - optionId: string; - optionLabel: string; - count: number; - }[] - >` - WITH form_fields AS ( - SELECT - f.id as form_id, - f.name as form_name, - field->>'id' as field_id, - field->>'label' as field_label, - opt->>'id' as option_id, - opt->>'label' as option_label - FROM "App_RoutingForms_Form" f, - LATERAL jsonb_array_elements(f.fields) as field - LEFT JOIN LATERAL jsonb_array_elements(field->'options') as opt ON true - WHERE true - ${whereClause} - ), - response_stats AS ( - SELECT - r."formId", - key as field_id, - CASE - WHEN jsonb_typeof(value->'value') = 'array' THEN - v.value_item - ELSE - value->>'value' - END as selected_option, - COUNT(DISTINCT r.id) as response_count - FROM "App_RoutingForms_FormResponse" r - CROSS JOIN jsonb_each(r.response::jsonb) as fields(key, value) - LEFT JOIN LATERAL jsonb_array_elements_text( - CASE - WHEN jsonb_typeof(value->'value') = 'array' - THEN value->'value' - ELSE NULL - END - ) as v(value_item) ON true - WHERE r."routedToBookingUid" IS NULL - GROUP BY r."formId", key, selected_option - ) - SELECT - ff.form_id as "formId", - ff.form_name as "formName", - ff.field_id as "fieldId", - ff.field_label as "fieldLabel", - ff.option_id as "optionId", - ff.option_label as "optionLabel", - COALESCE(rs.response_count, 0)::integer as count - FROM form_fields ff - LEFT JOIN response_stats rs ON - rs."formId" = ff.form_id AND - rs.field_id = ff.field_id AND - rs.selected_option = ff.option_id - WHERE ff.option_id IS NOT NULL - ORDER BY count DESC`; - - // First group by form and field - const groupedByFormAndField = result.reduce((acc, curr) => { - const formKey = curr.formName; - acc[formKey] = acc[formKey] || {}; - const labelKey = curr.fieldLabel; - acc[formKey][labelKey] = acc[formKey][labelKey] || []; - acc[formKey][labelKey].push({ - optionId: curr.optionId, - count: curr.count, - optionLabel: curr.optionLabel, - }); - return acc; - }, {} as Record>); - - // NOTE: totalCount represents the sum of all response counts across all fields and options for a form - // For example, if a form has 2 fields with 2 options each: - // Field1: Option1 (5 responses), Option2 (3 responses) - // Field2: Option1 (2 responses), Option2 (4 responses) - // Then totalCount = 5 + 3 + 2 + 4 = 14 total responses - const sortedEntries = Object.entries(groupedByFormAndField) - .map(([formName, fields]) => ({ - formName, - fields, - totalCount: Object.values(fields) - .flat() - .reduce((sum, item) => sum + item.count, 0), - })) - .sort((a, b) => b.totalCount - a.totalCount); - - // Convert back to original format - const sortedGroupedByFormAndField = sortedEntries.reduce((acc, { formName, fields }) => { - acc[formName] = fields; - return acc; - }, {} as Record>); - - return sortedGroupedByFormAndField; - } - static async getRoutingFormHeaders({ userId, teamId, diff --git a/packages/features/insights/server/trpc-router.ts b/packages/features/insights/server/trpc-router.ts index 7384ae7dc07d6b..6a7dbf62573550 100644 --- a/packages/features/insights/server/trpc-router.ts +++ b/packages/features/insights/server/trpc-router.ts @@ -885,20 +885,14 @@ export const insightsRouter = router({ return options; }), failedBookingsByField: userBelongsToTeamProcedure - .input( - z.object({ - userId: z.number().optional(), - teamId: z.number().optional(), - isAll: z.boolean(), - routingFormId: z.string().optional(), - }) - ) + .input(insightsRoutingServiceInputSchema) .query(async ({ ctx, input }) => { - return await RoutingEventsInsights.getFailedBookingsByRoutingFormGroup({ - ...input, - userId: ctx.user.id, - organizationId: ctx.user.organizationId ?? null, - }); + const insightsRoutingService = createInsightsRoutingService(ctx, input); + try { + return await insightsRoutingService.getFailedBookingsByFieldData(); + } catch (e) { + throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" }); + } }), routingFormResponsesHeaders: userBelongsToTeamProcedure .input( diff --git a/packages/lib/server/service/InsightsRoutingBaseService.ts b/packages/lib/server/service/InsightsRoutingBaseService.ts index 1a70dcabb8098f..8d3c6a74656c6a 100644 --- a/packages/lib/server/service/InsightsRoutingBaseService.ts +++ b/packages/lib/server/service/InsightsRoutingBaseService.ts @@ -433,7 +433,7 @@ export class InsightsRoutingBaseService { // Date range filtering if (this.filters.startDate && this.filters.endDate) { conditions.push( - Prisma.sql`"createdAt" >= ${this.filters.startDate}::timestamp AND "createdAt" <= ${this.filters.endDate}::timestamp` + Prisma.sql`rfrd."createdAt" >= ${this.filters.startDate}::timestamp AND rfrd."createdAt" <= ${this.filters.endDate}::timestamp` ); } @@ -450,7 +450,7 @@ export class InsightsRoutingBaseService { if (bookingStatusOrder && isMultiSelectFilterValue(bookingStatusOrder.value)) { const statusCondition = makeSqlCondition(bookingStatusOrder.value); if (statusCondition) { - conditions.push(Prisma.sql`"bookingStatusOrder" ${statusCondition}`); + conditions.push(Prisma.sql`rfrd."bookingStatusOrder" ${statusCondition}`); } } @@ -459,7 +459,7 @@ export class InsightsRoutingBaseService { if (bookingAssignmentReason && isTextFilterValue(bookingAssignmentReason.value)) { const reasonCondition = makeSqlCondition(bookingAssignmentReason.value); if (reasonCondition) { - conditions.push(Prisma.sql`"bookingAssignmentReason" ${reasonCondition}`); + conditions.push(Prisma.sql`rfrd."bookingAssignmentReason" ${reasonCondition}`); } } @@ -468,7 +468,7 @@ export class InsightsRoutingBaseService { if (bookingUid && isTextFilterValue(bookingUid.value)) { const uidCondition = makeSqlCondition(bookingUid.value); if (uidCondition) { - conditions.push(Prisma.sql`"bookingUid" ${uidCondition}`); + conditions.push(Prisma.sql`rfrd."bookingUid" ${uidCondition}`); } } @@ -502,7 +502,7 @@ export class InsightsRoutingBaseService { // Extract member user IDs filter (multi-select) const memberUserIds = filtersMap["bookingUserId"]; if (memberUserIds && isMultiSelectFilterValue(memberUserIds.value)) { - conditions.push(Prisma.sql`"bookingUserId" = ANY(${memberUserIds.value.data})`); + conditions.push(Prisma.sql`rfrd."bookingUserId" = ANY(${memberUserIds.value.data})`); } // Extract form ID filter (single-select) @@ -510,7 +510,7 @@ export class InsightsRoutingBaseService { if (formId && isSingleSelectFilterValue(formId.value)) { const formIdCondition = makeSqlCondition(formId.value); if (formIdCondition) { - conditions.push(Prisma.sql`"formId" ${formIdCondition}`); + conditions.push(Prisma.sql`rfrd."formId" ${formIdCondition}`); } } @@ -562,7 +562,7 @@ export class InsightsRoutingBaseService { } if (scope === "user") { - return Prisma.sql`"formUserId" = ${this.options.userId} AND "formTeamId" IS NULL`; + return Prisma.sql`rfrd."formUserId" = ${this.options.userId} AND rfrd."formTeamId" IS NULL`; } else if (scope === "org") { return await this.buildOrgAuthorizationCondition(this.options); } else if (scope === "team") { @@ -584,7 +584,7 @@ export class InsightsRoutingBaseService { const teamIds = [options.orgId, ...teamsFromOrg.map((t) => t.id)]; - return Prisma.sql`("formTeamId" = ANY(${teamIds})) OR ("formUserId" = ${options.userId} AND "formTeamId" IS NULL)`; + return Prisma.sql`(rfrd."formTeamId" = ANY(${teamIds})) OR (rfrd."formUserId" = ${options.userId} AND rfrd."formTeamId" IS NULL)`; } private async buildTeamAuthorizationCondition( @@ -600,7 +600,7 @@ export class InsightsRoutingBaseService { return NOTHING_CONDITION; } - return Prisma.sql`"formTeamId" = ${options.teamId}`; + return Prisma.sql`rfrd."formTeamId" = ${options.teamId}`; } private async isOwnerOrAdmin(userId: number, targetId: number): Promise { @@ -693,4 +693,105 @@ export class InsightsRoutingBaseService { AND ${columnCondition} )`; } + + async getFailedBookingsByFieldData(): Promise< + Record> + > { + const baseConditions = await this.getBaseConditions(); + + // Get failed bookings (responses without a successful booking) grouped by form, field, and option + const result = await this.prisma.$queryRaw< + { + formId: string; + formName: string; + fieldId: string; + fieldLabel: string; + optionId: string; + optionLabel: string; + count: number; + }[] + >` + WITH form_fields AS ( + SELECT DISTINCT + rfrd."formId" as form_id, + rfrd."formName" as form_name, + field->>'id' as field_id, + field->>'label' as field_label, + opt->>'id' as option_id, + opt->>'label' as option_label + FROM "RoutingFormResponseDenormalized" rfrd + JOIN "App_RoutingForms_Form" f ON rfrd."formId" = f.id, + LATERAL jsonb_array_elements(f.fields) as field + LEFT JOIN LATERAL jsonb_array_elements(field->'options') as opt ON true + WHERE + ${baseConditions} + ), + response_stats AS ( + SELECT + rfrd."formId", + f."fieldId" as field_id, + COALESCE(arr.value, f."valueString", f."valueNumber"::text) as selected_option, + COUNT(DISTINCT rfrd.id) as response_count + FROM "RoutingFormResponseDenormalized" rfrd + JOIN "RoutingFormResponseField" f ON rfrd.id = f."responseId" + LEFT JOIN LATERAL unnest(f."valueStringArray") as arr(value) ON f."valueStringArray" != '{}' + WHERE ${baseConditions} + AND rfrd."bookingUid" IS NULL + AND COALESCE(arr.value, f."valueString", f."valueNumber"::text) IS NOT NULL + GROUP BY rfrd."formId", f."fieldId", COALESCE(arr.value, f."valueString", f."valueNumber"::text) + ) + SELECT + ff.form_id as "formId", + ff.form_name as "formName", + ff.field_id as "fieldId", + ff.field_label as "fieldLabel", + ff.option_id as "optionId", + ff.option_label as "optionLabel", + COALESCE(rs.response_count, 0)::integer as count + FROM form_fields ff + LEFT JOIN response_stats rs ON + rs."formId" = ff.form_id AND + rs.field_id = ff.field_id AND + rs.selected_option = ff.option_id + WHERE ff.option_id IS NOT NULL + ORDER BY count DESC + `; + + // First group by form and field + const groupedByFormAndField = result.reduce((acc, curr) => { + const formKey = curr.formName; + acc[formKey] = acc[formKey] || {}; + const labelKey = curr.fieldLabel; + acc[formKey][labelKey] = acc[formKey][labelKey] || []; + acc[formKey][labelKey].push({ + optionId: curr.optionId, + count: curr.count, + optionLabel: curr.optionLabel, + }); + return acc; + }, {} as Record>); + + // NOTE: totalCount represents the sum of all response counts across all fields and options for a form + // For example, if a form has 2 fields with 2 options each: + // Field1: Option1 (5 responses), Option2 (3 responses) + // Field2: Option1 (2 responses), Option2 (4 responses) + // Then totalCount = 5 + 3 + 2 + 4 = 14 total responses + const sortedEntries = Object.entries(groupedByFormAndField) + .map(([formName, fields]) => ({ + formName, + fields, + totalCount: Object.values(fields) + .flat() + .reduce((sum, item) => sum + item.count, 0), + })) + .sort((a, b) => b.totalCount - a.totalCount); + + // Convert back to original format + const sortedGroupedByFormAndField = sortedEntries.reduce((acc, { formName, fields }) => { + acc[formName] = fields; + return acc; + }, {} as Record>); + + return sortedGroupedByFormAndField; + } } diff --git a/packages/lib/server/service/__tests__/InsightsRoutingService.integration-test.ts b/packages/lib/server/service/__tests__/InsightsRoutingService.integration-test.ts index 343f8df36d55a2..3c5651264bdfa1 100644 --- a/packages/lib/server/service/__tests__/InsightsRoutingService.integration-test.ts +++ b/packages/lib/server/service/__tests__/InsightsRoutingService.integration-test.ts @@ -323,17 +323,9 @@ describe("InsightsRoutingService Integration Tests", () => { }); const conditions = await service.getAuthorizationConditions(); - expect(conditions).toMatchInlineSnapshot(` - e { - "strings": [ - ""formUserId" = ", - " AND "formTeamId" IS NULL", - ], - "values": [ - ${testData.user.id}, - ], - } - `); + expect(conditions).toEqual( + Prisma.sql`rfrd."formUserId" = ${testData.user.id} AND rfrd."formTeamId" IS NULL` + ); await testData.cleanup(); }); @@ -356,7 +348,7 @@ describe("InsightsRoutingService Integration Tests", () => { }); const conditions = await service.getAuthorizationConditions(); - expect(conditions).toEqual(Prisma.sql`"formTeamId" = ${testData.team.id}`); + expect(conditions).toEqual(Prisma.sql`rfrd."formTeamId" = ${testData.team.id}`); // Clean up await testData.cleanup(); @@ -389,24 +381,11 @@ describe("InsightsRoutingService Integration Tests", () => { const conditions = await service.getAuthorizationConditions(); - expect(conditions).toMatchInlineSnapshot(` - e { - "strings": [ - "("formTeamId" = ANY(", - ")) OR ("formUserId" = ", - " AND "formTeamId" IS NULL)", - ], - "values": [ - [ - ${testData.org.id}, - ${testData.team.id}, - ${team2.id}, - ${team3.id}, - ], - ${testData.user.id}, - ], - } - `); + // Build expected team IDs array for org scope + const expectedTeamIds = [testData.org.id, testData.team.id, team2.id, team3.id]; + expect(conditions).toEqual( + Prisma.sql`(rfrd."formTeamId" = ANY(${expectedTeamIds})) OR (rfrd."formUserId" = ${testData.user.id} AND rfrd."formTeamId" IS NULL)` + ); await testData.cleanup(); }); @@ -469,7 +448,7 @@ describe("InsightsRoutingService Integration Tests", () => { const conditions = await service.getFilterConditions(); expect(conditions).toEqual( - Prisma.sql`"createdAt" >= ${"2024-01-01"}::timestamp AND "createdAt" <= ${"2024-12-31"}::timestamp` + Prisma.sql`rfrd."createdAt" >= ${"2024-01-01"}::timestamp AND rfrd."createdAt" <= ${"2024-12-31"}::timestamp` ); await testData.cleanup(); @@ -496,7 +475,9 @@ describe("InsightsRoutingService Integration Tests", () => { // First call should build conditions const conditions1 = await service.getAuthorizationConditions(); - expect(conditions1).toEqual(Prisma.sql`"formUserId" = ${testData.user.id} AND "formTeamId" IS NULL`); + expect(conditions1).toEqual( + Prisma.sql`rfrd."formUserId" = ${testData.user.id} AND rfrd."formTeamId" IS NULL` + ); // Second call should use cached conditions const conditions2 = await service.getAuthorizationConditions(); @@ -540,7 +521,7 @@ describe("InsightsRoutingService Integration Tests", () => { }); const filters = createDefaultFilters(); - const dateCondition = Prisma.sql`"createdAt" >= ${filters.startDate}::timestamp AND "createdAt" <= ${filters.endDate}::timestamp`; + const dateCondition = Prisma.sql`rfrd."createdAt" >= ${filters.startDate}::timestamp AND rfrd."createdAt" <= ${filters.endDate}::timestamp`; const service = new InsightsRoutingService({ prisma, @@ -555,7 +536,7 @@ describe("InsightsRoutingService Integration Tests", () => { const results = await service.getBaseConditions(); expect(results).toEqual( - Prisma.sql`(("formUserId" = ${testData.user.id} AND "formTeamId" IS NULL) AND (${dateCondition}))` + Prisma.sql`((rfrd."formUserId" = ${testData.user.id} AND rfrd."formTeamId" IS NULL) AND (${dateCondition}))` ); await testData.cleanup(); @@ -609,7 +590,7 @@ describe("InsightsRoutingService Integration Tests", () => { const baseConditions = await service.getBaseConditions(); const results = await prisma.$queryRaw>` - SELECT id FROM "RoutingFormResponseDenormalized" WHERE ${baseConditions} + SELECT id FROM "RoutingFormResponseDenormalized" rfrd WHERE ${baseConditions} `; // Should only return the authorized user's form response @@ -713,7 +694,7 @@ describe("InsightsRoutingService Integration Tests", () => { const baseConditions = await service.getBaseConditions(); const results = await prisma.$queryRaw>` - SELECT id FROM "RoutingFormResponseDenormalized" WHERE ${baseConditions} + SELECT id FROM "RoutingFormResponseDenormalized" rfrd WHERE ${baseConditions} `; // Should only return the authorized user's form response @@ -803,7 +784,7 @@ describe("InsightsRoutingService Integration Tests", () => { const baseConditions = await service.getBaseConditions(); const results = await prisma.$queryRaw>` - SELECT id FROM "RoutingFormResponseDenormalized" WHERE ${baseConditions} + SELECT id FROM "RoutingFormResponseDenormalized" rfrd WHERE ${baseConditions} `; // Should return both form responses (original user's and team member's) @@ -862,7 +843,7 @@ describe("InsightsRoutingService Integration Tests", () => { const filterConditions = await service.getFilterConditions(); expect(filterConditions).toEqual( - Prisma.sql`"createdAt" >= ${defaultFilters.startDate}::timestamp AND "createdAt" <= ${defaultFilters.endDate}::timestamp` + Prisma.sql`rfrd."createdAt" >= ${defaultFilters.startDate}::timestamp AND rfrd."createdAt" <= ${defaultFilters.endDate}::timestamp` ); await testData.cleanup(); @@ -891,7 +872,7 @@ describe("InsightsRoutingService Integration Tests", () => { const filterConditions = await service.getFilterConditions(); expect(filterConditions).toEqual( - Prisma.sql`"createdAt" >= ${defaultFilters.startDate}::timestamp AND "createdAt" <= ${defaultFilters.endDate}::timestamp` + Prisma.sql`rfrd."createdAt" >= ${defaultFilters.startDate}::timestamp AND rfrd."createdAt" <= ${defaultFilters.endDate}::timestamp` ); await testData.cleanup(); @@ -928,9 +909,9 @@ describe("InsightsRoutingService Integration Tests", () => { const filterConditions = await service.getFilterConditions(); expect(filterConditions).toEqual( - Prisma.sql`("createdAt" >= ${defaultFilters.startDate}::timestamp AND "createdAt" <= ${ + Prisma.sql`(rfrd."createdAt" >= ${defaultFilters.startDate}::timestamp AND rfrd."createdAt" <= ${ defaultFilters.endDate - }::timestamp) AND ("bookingStatusOrder" = ANY(${["pending", "accepted"]}))` + }::timestamp) AND (rfrd."bookingStatusOrder" = ANY(${["pending", "accepted"]}))` ); await testData.cleanup(); @@ -967,9 +948,9 @@ describe("InsightsRoutingService Integration Tests", () => { const filterConditions = await service.getFilterConditions(); expect(filterConditions).toEqual( - Prisma.sql`("createdAt" >= ${defaultFilters.startDate}::timestamp AND "createdAt" <= ${ + Prisma.sql`(rfrd."createdAt" >= ${defaultFilters.startDate}::timestamp AND rfrd."createdAt" <= ${ defaultFilters.endDate - }::timestamp) AND ("bookingAssignmentReason" ILIKE ${`%manual%`})` + }::timestamp) AND (rfrd."bookingAssignmentReason" ILIKE ${`%manual%`})` ); await testData.cleanup(); @@ -1006,9 +987,9 @@ describe("InsightsRoutingService Integration Tests", () => { const filterConditions = await service.getFilterConditions(); expect(filterConditions).toEqual( - Prisma.sql`("createdAt" >= ${defaultFilters.startDate}::timestamp AND "createdAt" <= ${ + Prisma.sql`(rfrd."createdAt" >= ${defaultFilters.startDate}::timestamp AND rfrd."createdAt" <= ${ defaultFilters.endDate - }::timestamp) AND ("bookingUid" = ${"test-booking-123"})` + }::timestamp) AND (rfrd."bookingUid" = ${"test-booking-123"})` ); await testData.cleanup(); @@ -1045,9 +1026,9 @@ describe("InsightsRoutingService Integration Tests", () => { const filterConditions = await service.getFilterConditions(); expect(filterConditions).toEqual( - Prisma.sql`("createdAt" >= ${defaultFilters.startDate}::timestamp AND "createdAt" <= ${ + Prisma.sql`(rfrd."createdAt" >= ${defaultFilters.startDate}::timestamp AND rfrd."createdAt" <= ${ defaultFilters.endDate - }::timestamp) AND ("bookingUserId" = ANY(${[testData.user.id, 999]}))` + }::timestamp) AND (rfrd."bookingUserId" = ANY(${[testData.user.id, 999]}))` ); await testData.cleanup(); @@ -1083,26 +1064,16 @@ describe("InsightsRoutingService Integration Tests", () => { }); const filterConditions = await service.getFilterConditions(); - expect(filterConditions).toMatchInlineSnapshot(` - e { - "strings": [ - "("createdAt" >= ", - "::timestamp AND "createdAt" <= ", - "::timestamp) AND (EXISTS ( - SELECT 1 FROM "Booking" b - INNER JOIN "Attendee" a ON a."bookingId" = b."id" - WHERE b."uid" = rfrd."bookingUid" - AND a.name ILIKE ", - " - ))", - ], - "values": [ - "2025-08-18T00:00:00.000Z", - "2025-08-20T00:00:00.000Z", - "%john%", - ], - } - `); + expect(filterConditions).toEqual( + Prisma.sql`(rfrd."createdAt" >= ${defaultFilters.startDate}::timestamp AND rfrd."createdAt" <= ${ + defaultFilters.endDate + }::timestamp) AND (EXISTS ( + SELECT 1 FROM "Booking" b + INNER JOIN "Attendee" a ON a."bookingId" = b."id" + WHERE b."uid" = rfrd."bookingUid" + AND a.name ILIKE ${"%john%"} + ))` + ); await testData.cleanup(); }); @@ -1137,26 +1108,16 @@ describe("InsightsRoutingService Integration Tests", () => { }); const filterConditions = await service.getFilterConditions(); - expect(filterConditions).toMatchInlineSnapshot(` - e { - "strings": [ - "("createdAt" >= ", - "::timestamp AND "createdAt" <= ", - "::timestamp) AND (EXISTS ( - SELECT 1 FROM "Booking" b - INNER JOIN "Attendee" a ON a."bookingId" = b."id" - WHERE b."uid" = rfrd."bookingUid" - AND a.email ILIKE ", - " - ))", - ], - "values": [ - "2025-08-18T00:00:00.000Z", - "2025-08-20T00:00:00.000Z", - "%@gmail.com", - ], - } - `); + expect(filterConditions).toEqual( + Prisma.sql`(rfrd."createdAt" >= ${defaultFilters.startDate}::timestamp AND rfrd."createdAt" <= ${ + defaultFilters.endDate + }::timestamp) AND (EXISTS ( + SELECT 1 FROM "Booking" b + INNER JOIN "Attendee" a ON a."bookingId" = b."id" + WHERE b."uid" = rfrd."bookingUid" + AND a.email ILIKE ${"%@gmail.com"} + ))` + ); await testData.cleanup(); }); @@ -1191,26 +1152,16 @@ describe("InsightsRoutingService Integration Tests", () => { }); const filterConditions = await service.getFilterConditions(); - expect(filterConditions).toMatchInlineSnapshot(` - e { - "strings": [ - "("createdAt" >= ", - "::timestamp AND "createdAt" <= ", - "::timestamp) AND (EXISTS ( - SELECT 1 FROM "Booking" b - INNER JOIN "Attendee" a ON a."bookingId" = b."id" - WHERE b."uid" = rfrd."bookingUid" - AND a."phoneNumber" ILIKE ", - " - ))", - ], - "values": [ - "2025-08-18T00:00:00.000Z", - "2025-08-20T00:00:00.000Z", - "%000%", - ], - } - `); + expect(filterConditions).toEqual( + Prisma.sql`(rfrd."createdAt" >= ${defaultFilters.startDate}::timestamp AND rfrd."createdAt" <= ${ + defaultFilters.endDate + }::timestamp) AND (EXISTS ( + SELECT 1 FROM "Booking" b + INNER JOIN "Attendee" a ON a."bookingId" = b."id" + WHERE b."uid" = rfrd."bookingUid" + AND a."phoneNumber" ILIKE ${"%000%"} + ))` + ); await testData.cleanup(); }); @@ -1248,7 +1199,7 @@ describe("InsightsRoutingService Integration Tests", () => { const filterConditions = await service.getFilterConditions(); expect(filterConditions).toEqual( - Prisma.sql`("createdAt" >= ${defaultFilters.startDate}::timestamp AND "createdAt" <= ${ + Prisma.sql`(rfrd."createdAt" >= ${defaultFilters.startDate}::timestamp AND rfrd."createdAt" <= ${ defaultFilters.endDate }::timestamp) AND (EXISTS ( SELECT 1 FROM "RoutingFormResponseField" rrf @@ -1293,7 +1244,7 @@ describe("InsightsRoutingService Integration Tests", () => { const filterConditions = await service.getFilterConditions(); expect(filterConditions).toEqual( - Prisma.sql`("createdAt" >= ${defaultFilters.startDate}::timestamp AND "createdAt" <= ${ + Prisma.sql`(rfrd."createdAt" >= ${defaultFilters.startDate}::timestamp AND rfrd."createdAt" <= ${ defaultFilters.endDate }::timestamp) AND (EXISTS ( SELECT 1 FROM "RoutingFormResponseField" rrf @@ -1352,11 +1303,11 @@ describe("InsightsRoutingService Integration Tests", () => { const filterConditions = await service.getFilterConditions(); expect(filterConditions).toEqual( - Prisma.sql`((("createdAt" >= ${defaultFilters.startDate}::timestamp AND "createdAt" <= ${ + Prisma.sql`(((rfrd."createdAt" >= ${defaultFilters.startDate}::timestamp AND rfrd."createdAt" <= ${ defaultFilters.endDate - }::timestamp) AND ("bookingStatusOrder" = ANY(${[ + }::timestamp) AND (rfrd."bookingStatusOrder" = ANY(${[ "pending", - ]}))) AND ("bookingAssignmentReason" ILIKE ${`%manual%`})) AND (EXISTS ( + ]}))) AND (rfrd."bookingAssignmentReason" ILIKE ${`%manual%`})) AND (EXISTS ( SELECT 1 FROM "RoutingFormResponseField" rrf WHERE rrf."responseId" = rfrd."id" AND rrf."fieldId" = ${customFieldId} @@ -1398,9 +1349,9 @@ describe("InsightsRoutingService Integration Tests", () => { const filterConditions = await service.getFilterConditions(); expect(filterConditions).toEqual( - Prisma.sql`("createdAt" >= ${defaultFilters.startDate}::timestamp AND "createdAt" <= ${ + Prisma.sql`(rfrd."createdAt" >= ${defaultFilters.startDate}::timestamp AND rfrd."createdAt" <= ${ defaultFilters.endDate - }::timestamp) AND ("formId" = ${"form-123"})` + }::timestamp) AND (rfrd."formId" = ${"form-123"})` ); await testData.cleanup(); @@ -1444,12 +1395,12 @@ describe("InsightsRoutingService Integration Tests", () => { const filterConditions = await service.getFilterConditions(); expect(filterConditions).toEqual( - Prisma.sql`(("createdAt" >= ${defaultFilters.startDate}::timestamp AND "createdAt" <= ${ + Prisma.sql`((rfrd."createdAt" >= ${defaultFilters.startDate}::timestamp AND rfrd."createdAt" <= ${ defaultFilters.endDate - }::timestamp) AND ("bookingStatusOrder" = ANY(${[ + }::timestamp) AND (rfrd."bookingStatusOrder" = ANY(${[ "pending", "accepted", - ]}))) AND ("formId" = ${"form-456"})` + ]}))) AND (rfrd."formId" = ${"form-456"})` ); await testData.cleanup(); @@ -1495,9 +1446,9 @@ describe("InsightsRoutingService Integration Tests", () => { const filterConditions = await service.getFilterConditions(); expect(filterConditions).toEqual( - Prisma.sql`(("createdAt" >= ${defaultFilters.startDate}::timestamp AND "createdAt" <= ${ + Prisma.sql`((rfrd."createdAt" >= ${defaultFilters.startDate}::timestamp AND rfrd."createdAt" <= ${ defaultFilters.endDate - }::timestamp) AND ("bookingStatusOrder" = ANY(${["pending"]}))) AND (EXISTS ( + }::timestamp) AND (rfrd."bookingStatusOrder" = ANY(${["pending"]}))) AND (EXISTS ( SELECT 1 FROM "RoutingFormResponseField" rrf WHERE rrf."responseId" = rfrd."id" AND rrf."fieldId" = ${customFieldId}