-
Notifications
You must be signed in to change notification settings - Fork 13.3k
feat: take into account guest availability when host reschedules #28636
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 17 commits
d4de6e2
bd9929d
4a761e6
4d728e1
0d29c21
d558910
4475b4f
a2b98d7
b545426
78269db
fa147ce
50c4055
f8174f6
13edf26
5e327e8
69733d3
fb8da3c
989b419
23b3609
5ca694d
efb6cbc
c050464
0a83d1a
e8a7275
e5ee2dc
45899ba
37f58e2
7cec9fb
577cb17
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,6 +20,7 @@ export type UseScheduleWithCacheArgs = { | |
| duration?: number | null; | ||
| dayCount?: number | null; | ||
| rescheduleUid?: string | null; | ||
| rescheduledBy?: string | null; | ||
| isTeamEvent?: boolean; | ||
| orgSlug?: string; | ||
| teamMemberEmail?: string | null; | ||
|
|
@@ -58,6 +59,7 @@ export const useSchedule = ({ | |
| duration, | ||
| dayCount, | ||
| rescheduleUid, | ||
| rescheduledBy, | ||
| isTeamEvent, | ||
| orgSlug, | ||
| teamMemberEmail, | ||
|
|
@@ -102,6 +104,7 @@ export const useSchedule = ({ | |
| timeZone: timezone ?? "PLACEHOLDER_TIMEZONE", | ||
| duration: duration ? `${duration}` : undefined, | ||
| rescheduleUid, | ||
| rescheduledBy, | ||
|
Comment on lines
106
to
+107
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sanitize cache/log handling before adding email-valued Line 107 forwards 🤖 Prompt for AI Agents |
||
| orgSlug, | ||
| teamMemberEmail, | ||
| routedTeamMemberIds, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,18 +1,27 @@ | ||
| import type { PrismaClient } from "@calcom/prisma"; | ||
| import { BookingStatus } from "@calcom/prisma/enums"; | ||
| import { beforeEach, describe, expect, it, vi } from "vitest"; | ||
| import { BookingRepository } from "./BookingRepository"; | ||
|
|
||
| describe("BookingRepository", () => { | ||
| let repository: BookingRepository; | ||
| let mockPrismaClient: { | ||
| $queryRaw: ReturnType<typeof vi.fn>; | ||
| booking: { | ||
| findUnique: ReturnType<typeof vi.fn>; | ||
| findMany: ReturnType<typeof vi.fn>; | ||
| }; | ||
| }; | ||
|
|
||
| beforeEach(() => { | ||
| vi.clearAllMocks(); | ||
|
|
||
| mockPrismaClient = { | ||
| $queryRaw: vi.fn(), | ||
| booking: { | ||
| findUnique: vi.fn(), | ||
| findMany: vi.fn(), | ||
| }, | ||
| }; | ||
|
|
||
| repository = new BookingRepository(mockPrismaClient as unknown as PrismaClient); | ||
|
|
@@ -58,4 +67,177 @@ describe("BookingRepository", () => { | |
| expect(mockPrismaClient.$queryRaw).toHaveBeenCalledTimes(1); | ||
| }); | ||
| }); | ||
|
|
||
| describe("findByUidIncludeAttendeeEmails", () => { | ||
| it("should query booking by uid with attendee emails and host user email", async () => { | ||
| const mockBooking = { | ||
| id: 1, | ||
| uid: "test-uid", | ||
| attendees: [{ email: "guest@example.com" }], | ||
| user: { email: "host@example.com" }, | ||
| }; | ||
| mockPrismaClient.booking.findUnique.mockResolvedValue(mockBooking); | ||
|
|
||
| const result = await repository.findByUidIncludeAttendeeEmails({ uid: "test-uid" }); | ||
|
|
||
| expect(result).toEqual(mockBooking); | ||
| expect(mockPrismaClient.booking.findUnique).toHaveBeenCalledWith({ | ||
| where: { uid: "test-uid" }, | ||
| select: { | ||
| id: true, | ||
| uid: true, | ||
| attendees: { select: { email: true } }, | ||
| user: { select: { email: true } }, | ||
| }, | ||
| }); | ||
| }); | ||
|
|
||
| it("should return null when booking does not exist", async () => { | ||
| mockPrismaClient.booking.findUnique.mockResolvedValue(null); | ||
|
|
||
| const result = await repository.findByUidIncludeAttendeeEmails({ uid: "nonexistent" }); | ||
|
|
||
| expect(result).toBeNull(); | ||
| }); | ||
| }); | ||
|
|
||
| describe("findByUserIdsAndDateRange", () => { | ||
| const dateFrom = new Date("2026-04-01T00:00:00Z"); | ||
| const dateTo = new Date("2026-04-30T23:59:59Z"); | ||
|
|
||
| it("should return empty array when both userIds and userEmails are empty", async () => { | ||
| const result = await repository.findByUserIdsAndDateRange({ | ||
| userIds: [], | ||
| userEmails: [], | ||
| dateFrom, | ||
| dateTo, | ||
| }); | ||
|
|
||
| expect(result).toEqual([]); | ||
| expect(mockPrismaClient.booking.findMany).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it("should query bookings by userId when userIds are provided", async () => { | ||
| const mockBookings = [ | ||
| { | ||
| uid: "booking-1", | ||
| startTime: new Date("2026-04-10T09:00:00Z"), | ||
| endTime: new Date("2026-04-10T10:00:00Z"), | ||
| title: "Meeting", | ||
| userId: 10, | ||
| status: BookingStatus.ACCEPTED, | ||
| }, | ||
| ]; | ||
| mockPrismaClient.booking.findMany.mockResolvedValue(mockBookings); | ||
|
|
||
| const result = await repository.findByUserIdsAndDateRange({ | ||
| userIds: [10], | ||
| userEmails: [], | ||
| dateFrom, | ||
| dateTo, | ||
| }); | ||
|
|
||
| expect(result).toEqual(mockBookings); | ||
| expect(mockPrismaClient.booking.findMany).toHaveBeenCalledWith( | ||
| expect.objectContaining({ | ||
| where: expect.objectContaining({ | ||
| status: { in: [BookingStatus.ACCEPTED, BookingStatus.PENDING] }, | ||
| AND: [{ startTime: { lt: dateTo } }, { endTime: { gt: dateFrom } }], | ||
| }), | ||
| }) | ||
| ); | ||
| }); | ||
|
Comment on lines
+120
to
+149
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Assert the This test currently verifies status/date overlap but would still pass if the repository forgot to filter by Suggested assertion expect(mockPrismaClient.booking.findMany).toHaveBeenCalledWith(
expect.objectContaining({
where: expect.objectContaining({
status: { in: [BookingStatus.ACCEPTED, BookingStatus.PENDING] },
AND: [{ startTime: { lt: dateTo } }, { endTime: { gt: dateFrom } }],
+ OR: expect.arrayContaining([{ userId: { in: [10] } }]),
}),
})
);🤖 Prompt for AI Agents |
||
|
|
||
| it("should query bookings by email when userEmails are provided", async () => { | ||
| mockPrismaClient.booking.findMany.mockResolvedValue([]); | ||
|
|
||
| await repository.findByUserIdsAndDateRange({ | ||
| userIds: [], | ||
| userEmails: ["guest@example.com"], | ||
| dateFrom, | ||
| dateTo, | ||
| }); | ||
|
|
||
| expect(mockPrismaClient.booking.findMany).toHaveBeenCalledWith( | ||
| expect.objectContaining({ | ||
| where: expect.objectContaining({ | ||
| OR: expect.arrayContaining([ | ||
| { | ||
| attendees: { | ||
| some: { email: { in: ["guest@example.com"], mode: "insensitive" } }, | ||
| }, | ||
| }, | ||
| ]), | ||
| }), | ||
| }) | ||
| ); | ||
| }); | ||
|
|
||
| it("should combine userId and email conditions in OR clause", async () => { | ||
| mockPrismaClient.booking.findMany.mockResolvedValue([]); | ||
|
|
||
| await repository.findByUserIdsAndDateRange({ | ||
| userIds: [10, 20], | ||
| userEmails: ["guest@example.com"], | ||
| dateFrom, | ||
| dateTo, | ||
| }); | ||
|
|
||
| const callArgs = mockPrismaClient.booking.findMany.mock.calls[0][0]; | ||
| expect(callArgs.where.OR).toHaveLength(2); | ||
| expect(callArgs.where.OR).toEqual( | ||
| expect.arrayContaining([ | ||
| { userId: { in: [10, 20] } }, | ||
| { | ||
| attendees: { | ||
| some: { email: { in: ["guest@example.com"], mode: "insensitive" } }, | ||
| }, | ||
| }, | ||
| ]) | ||
| ); | ||
| }); | ||
|
|
||
| it("should select the correct fields", async () => { | ||
| mockPrismaClient.booking.findMany.mockResolvedValue([]); | ||
|
|
||
| await repository.findByUserIdsAndDateRange({ | ||
| userIds: [10], | ||
| userEmails: [], | ||
| dateFrom, | ||
| dateTo, | ||
| }); | ||
|
|
||
| expect(mockPrismaClient.booking.findMany).toHaveBeenCalledWith( | ||
| expect.objectContaining({ | ||
| select: { | ||
| uid: true, | ||
| startTime: true, | ||
| endTime: true, | ||
| title: true, | ||
| userId: true, | ||
| status: true, | ||
| }, | ||
| }) | ||
| ); | ||
| }); | ||
|
|
||
| it("should include excludeUid in query when provided", async () => { | ||
| const repo = new BookingRepository(mockPrismaClient as unknown as PrismaClient); | ||
| await repo.findByUserIdsAndDateRange({ | ||
| userIds: [1], | ||
| userEmails: [], | ||
| dateFrom, | ||
| dateTo, | ||
| excludeUid: "booking-to-exclude", | ||
| }); | ||
|
|
||
| expect(mockPrismaClient.booking.findMany).toHaveBeenCalledWith( | ||
| expect.objectContaining({ | ||
| where: expect.objectContaining({ | ||
| uid: { not: "booking-to-exclude" }, | ||
| }), | ||
| }) | ||
| ); | ||
| }); | ||
| }); | ||
| }); | ||
Uh oh!
There was an error while loading. Please reload this page.