diff --git a/packages/features/insights/server/__tests__/buildBaseWhereCondition.test.ts b/packages/features/insights/server/__tests__/buildBaseWhereCondition.test.ts new file mode 100644 index 0000000000..99af95a48c --- /dev/null +++ b/packages/features/insights/server/__tests__/buildBaseWhereCondition.test.ts @@ -0,0 +1,297 @@ +import { describe, expect, it, vi, beforeEach } from "vitest"; + +import type { readonlyPrisma } from "@calcom/prisma"; + +import { buildBaseWhereCondition } from "../trpc-router"; + +const mockTeamFindMany = vi.fn(); +const mockMembershipFindMany = vi.fn(); + +const mockInsightsDb = { + team: { + findMany: mockTeamFindMany, + }, + membership: { + findMany: mockMembershipFindMany, + }, +} as unknown as typeof readonlyPrisma; + +const createMockContext = (overrides = {}) => ({ + userIsOwnerAdminOfParentTeam: false, + userOrganizationId: null, + insightsDb: mockInsightsDb, + ...overrides, +}); + +describe("buildBaseWhereCondition", () => { + beforeEach(() => { + vi.resetAllMocks(); + }); + + describe("Basic filtering", () => { + it("should set eventTypeId condition when eventTypeId is provided", async () => { + const ctx = createMockContext(); + const result = await buildBaseWhereCondition({ + eventTypeId: 123, + ctx, + }); + + expect(result.whereCondition).toEqual({ + OR: [{ eventTypeId: 123 }, { eventParentId: 123 }], + }); + }); + + it("should set userId condition when memberUserId is provided", async () => { + const ctx = createMockContext(); + const result = await buildBaseWhereCondition({ + memberUserId: 456, + ctx, + }); + + expect(result.whereCondition).toEqual({ + userId: 456, + }); + }); + + it("should set userId and teamId conditions when userId is provided", async () => { + const ctx = createMockContext(); + const result = await buildBaseWhereCondition({ + userId: 789, + ctx, + }); + + expect(result.whereCondition).toEqual({ + userId: 789, + teamId: null, + }); + }); + }); + + describe("Organization-wide queries", () => { + it("should return appropriate where condition when no teams found in organization", async () => { + mockTeamFindMany.mockResolvedValue([]); + + const ctx = createMockContext({ + userIsOwnerAdminOfParentTeam: true, + userOrganizationId: 100, + }); + + const result = await buildBaseWhereCondition({ + isAll: true, + ctx, + }); + + expect(result.whereCondition).toEqual({ + OR: [ + { + teamId: { in: [100] }, + isTeamBooking: true, + }, + ], + }); + }); + + it("should build complex where condition for organization-wide query", async () => { + mockTeamFindMany.mockResolvedValue([{ id: 101 }, { id: 102 }]); + mockMembershipFindMany.mockResolvedValue([{ userId: 201 }, { userId: 202 }]); + + const ctx = createMockContext({ + userIsOwnerAdminOfParentTeam: true, + userOrganizationId: 100, + }); + + const result = await buildBaseWhereCondition({ + isAll: true, + ctx, + }); + + expect(result.whereCondition).toEqual({ + OR: [ + { + teamId: { + in: [100, 101, 102], + }, + isTeamBooking: true, + }, + { + userId: { + in: [201, 202], + }, + isTeamBooking: false, + }, + ], + }); + }); + }); + + describe("Team-specific queries", () => { + it("should build where condition for team-specific query", async () => { + mockMembershipFindMany.mockResolvedValue([{ userId: 301 }, { userId: 302 }]); + + const ctx = createMockContext(); + + const result = await buildBaseWhereCondition({ + teamId: 200, + isAll: false, + ctx, + }); + + expect(result.whereCondition).toEqual({ + OR: [ + { + teamId: 200, + isTeamBooking: true, + }, + { + userId: { + in: [301, 302], + }, + isTeamBooking: false, + }, + ], + }); + }); + + it("should apply both team and eventTypeId conditions when both are provided", async () => { + mockMembershipFindMany.mockResolvedValue([{ userId: 301 }, { userId: 302 }]); + + const ctx = createMockContext(); + + const result = await buildBaseWhereCondition({ + teamId: 200, + eventTypeId: 500, + isAll: false, + ctx, + }); + + expect(result.whereCondition).toEqual({ + AND: [ + { + OR: [{ eventTypeId: 500 }, { eventParentId: 500 }], + }, + { + OR: [ + { + teamId: 200, + isTeamBooking: true, + }, + { + userId: { + in: [301, 302], + }, + isTeamBooking: false, + }, + ], + }, + ], + }); + }); + }); + + describe("Combined filtering", () => { + it("should combine eventTypeId and memberUserId conditions", async () => { + const ctx = createMockContext(); + + const result = await buildBaseWhereCondition({ + eventTypeId: 123, + memberUserId: 456, + ctx, + }); + + expect(result.whereCondition).toEqual({ + AND: [ + { + OR: [{ eventTypeId: 123 }, { eventParentId: 123 }], + }, + { + userId: 456, + }, + ], + }); + }); + }); + + describe("Invalid parameters", () => { + it("should handle missing parameters and return restrictive where condition", async () => { + const ctx = createMockContext(); + + const result = await buildBaseWhereCondition({ + ctx, + }); + + expect(result.whereCondition).toEqual({ id: -1 }); + }); + + it("should handle null teamId with restrictive where condition", async () => { + const ctx = createMockContext(); + + const result = await buildBaseWhereCondition({ + teamId: null, + ctx, + }); + + expect(result.whereCondition).toEqual({ id: -1 }); + }); + + it("should handle empty team members with proper team condition", async () => { + mockMembershipFindMany.mockResolvedValue([]); + + const ctx = createMockContext(); + + const result = await buildBaseWhereCondition({ + teamId: 200, + isAll: false, + ctx, + }); + + expect(result.whereCondition).toEqual({ + OR: [ + { + teamId: 200, + isTeamBooking: true, + }, + { + userId: { + in: [], + }, + isTeamBooking: false, + }, + ], + }); + }); + + it("should handle rejected team membership differently from empty team", async () => { + mockMembershipFindMany.mockImplementation((params) => { + if (params?.where?.accepted === true) { + // No accepted members + return Promise.resolve([]); + } + return Promise.resolve([{ userId: 601 }, { userId: 602 }]); + }); + + const ctx = createMockContext(); + + const result = await buildBaseWhereCondition({ + teamId: 200, + isAll: false, + ctx, + }); + + // Should only include team bookings since there are no accepted members + expect(result.whereCondition).toEqual({ + OR: [ + { + teamId: 200, + isTeamBooking: true, + }, + { + userId: { + in: [], // Empty because no accepted members + }, + isTeamBooking: false, + }, + ], + }); + }); + }); +}); diff --git a/packages/features/insights/server/trpc-router.ts b/packages/features/insights/server/trpc-router.ts index 58a687fcb2..65ede552fa 100644 --- a/packages/features/insights/server/trpc-router.ts +++ b/packages/features/insights/server/trpc-router.ts @@ -41,7 +41,7 @@ interface BuildBaseWhereConditionType { const bookingStatusSchema = z.enum(["NO_BOOKING", ...Object.values(BookingStatus)]).optional(); -const buildBaseWhereCondition = async ({ +export const buildBaseWhereCondition = async ({ teamId, eventTypeId, memberUserId, @@ -50,17 +50,28 @@ const buildBaseWhereCondition = async ({ ctx, }: BuildBaseWhereConditionType): Promise<{ whereCondition: Prisma.BookingTimeStatusWhereInput; - isEmptyResponse?: boolean; }> => { - let whereCondition: Prisma.BookingTimeStatusWhereInput = {}; + const conditions: Prisma.BookingTimeStatusWhereInput[] = []; + // EventType Filter - if (eventTypeId) whereCondition.OR = [{ eventTypeId }, { eventParentId: eventTypeId }]; - // User/Member filter - if (memberUserId) whereCondition.userId = memberUserId; - if (userId) { - whereCondition.teamId = null; - whereCondition.userId = userId; + if (eventTypeId) { + conditions.push({ + OR: [{ eventTypeId }, { eventParentId: eventTypeId }], + }); } + + // User/Member filter + if (memberUserId) { + conditions.push({ userId: memberUserId }); + } + + if (userId) { + conditions.push({ + teamId: null, + userId: userId, + }); + } + // organization-wide queries condition if (isAll && ctx.userIsOwnerAdminOfParentTeam && ctx.userOrganizationId) { const teamsFromOrg = await ctx.insightsDb.team.findMany({ @@ -72,57 +83,48 @@ const buildBaseWhereCondition = async ({ }, }); - if (teamsFromOrg.length === 0) { - return { - whereCondition: { - ...whereCondition, - OR: [ - ...(whereCondition.OR ?? []), - { - teamId: ctx.userOrganizationId, - isTeamBooking: true, + const teamIds = [ctx.userOrganizationId, ...teamsFromOrg.map((t) => t.id)]; + const usersFromOrg = + teamsFromOrg.length > 0 + ? await ctx.insightsDb.membership.findMany({ + where: { + team: { + id: { + in: teamIds, + }, + }, + accepted: true, }, - ], - }, - isEmptyResponse: true, - }; - } + select: { + userId: true, + }, + }) + : []; - const teamConditional = { - id: { - in: [ctx.userOrganizationId, ...teamsFromOrg.map((t) => t.id)], - }, - }; - const usersFromOrg = await ctx.insightsDb.membership.findMany({ - where: { - team: teamConditional, - accepted: true, - }, - select: { - userId: true, - }, - }); - const userIdsFromOrg = usersFromOrg.map((u) => u.userId); - whereCondition = { - ...whereCondition, + conditions.push({ OR: [ { teamId: { - in: [ctx.userOrganizationId, ...teamsFromOrg.map((t) => t.id)], + in: teamIds, }, isTeamBooking: true, }, - { - userId: { - in: userIdsFromOrg, - }, - isTeamBooking: false, - }, + ...(usersFromOrg.length > 0 + ? [ + { + userId: { + in: usersFromOrg.map((u) => u.userId), + }, + isTeamBooking: false, + }, + ] + : []), ], - }; + }); } - if (teamId && !isAll && !eventTypeId) { + // Team-specific queries condition + if (!isAll && teamId) { const usersFromTeam = await ctx.insightsDb.membership.findMany({ where: { teamId: teamId, @@ -133,8 +135,8 @@ const buildBaseWhereCondition = async ({ }, }); const userIdsFromTeam = usersFromTeam.map((u) => u.userId); - whereCondition = { - ...whereCondition, + + conditions.push({ OR: [ { teamId, @@ -147,9 +149,23 @@ const buildBaseWhereCondition = async ({ isTeamBooking: false, }, ], - }; + }); } - return { whereCondition }; + + let whereCondition: Prisma.BookingTimeStatusWhereInput = {}; + + if (conditions.length === 1) { + whereCondition = conditions[0]; + } else if (conditions.length > 1) { + whereCondition = { AND: conditions }; + } + + return { + whereCondition: + Object.keys(whereCondition).length === 0 + ? { id: -1 } // Ensure no data is returned for invalid parameters + : whereCondition, + }; }; const buildHashMapForUsers = < @@ -1200,8 +1216,6 @@ export const insightsRouter = router({ bookingWhere = { ...bookingWhere, - teamId, - eventTypeId, createdAt: { gte: dayjs(startDate).startOf("day").toDate(), lte: dayjs(endDate).endOf("day").toDate(), @@ -1318,8 +1332,6 @@ export const insightsRouter = router({ bookingWhere = { ...bookingWhere, - teamId, - eventTypeId, createdAt: { gte: dayjs(startDate).startOf("day").toDate(), lte: dayjs(endDate).endOf("day").toDate(), @@ -1397,8 +1409,7 @@ export const insightsRouter = router({ let { whereCondition: bookingWhere } = r; bookingWhere = { - teamId, - eventTypeId, + ...bookingWhere, createdAt: { gte: dayjs(startDate).startOf("day").toDate(), lte: dayjs(endDate).endOf("day").toDate(), @@ -1476,8 +1487,7 @@ export const insightsRouter = router({ let { whereCondition: bookingWhere } = r; bookingWhere = { - teamId, - eventTypeId, + ...bookingWhere, createdAt: { gte: dayjs(startDate).startOf("day").toDate(), lte: dayjs(endDate).endOf("day").toDate(),