-
Notifications
You must be signed in to change notification settings - Fork 13.4k
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
Open
bcornish1797
wants to merge
29
commits into
calcom:main
Choose a base branch
from
bcornish1797:feat/16378-guest-availability-reschedule
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
29 commits
Select commit
Hold shift + click to select a range
d4de6e2
feat: add findByEmails to UserRepository
bcornish1797 bd9929d
feat: add booking lookup methods for guest availability
bcornish1797 4a761e6
feat: merge guest busy times into availability computation
bcornish1797 4d728e1
feat: check guest availability when host reschedules (#16378)
bcornish1797 0d29c21
fix: address review feedback for guest availability rescheduling
bcornish1797 d558910
test: add unit tests for guest availability during reschedule
bcornish1797 4475b4f
refactor: improve error handling and move filtering to database level
bcornish1797 a2b98d7
fix: add host-initiator gating and narrow email filter
bcornish1797 b545426
fix: remove broken initiator gating, fix test contract conflict
bcornish1797 78269db
fix: correct variable name in excludeUid test (mockPrisma -> mockPris…
bcornish1797 fa147ce
fix: gate guest busy-time blocking on host-initiated reschedules only
bcornish1797 50c4055
fix: use empty string instead of undefined for EventBusyDetails source
bcornish1797 f8174f6
fix: resolve TS2802 Set iteration error and apply biome formatting
bcornish1797 13edf26
chore: trigger CLA recheck
bcornish1797 5e327e8
feat(slots): add observability to guest-busy-times degradation path
69733d3
fix(availability): drop Day.js round-trip in guestBusyTimesFormatted
bcornish1797 fb8da3c
fix(useEvent): restore restrictionSchedule/useStableTimezone wiring
bcornish1797 989b419
fix(users): findByEmails returns matchedEmails (primary and verified …
sochanged 23b3609
fix(schedule): fall back to V1 slots when rescheduledBy is set
sochanged 5ca694d
chore(tests): drop comments that restate their assertions
sochanged efb6cbc
test(users): tighten findByEmails assertions
sochanged c050464
fix(slots): bound degradation log payload in guest busy-time fallback
sochanged 0a83d1a
fix(bookings): dedupe findByUserIdsAndDateRange results by uid
sochanged e8a7275
chore(slots): address round-3 review nits
sochanged e5ee2dc
fix(slots): drop stale RoutingFormResponseRepository import and fix g…
sochanged 45899ba
fix(availability): always set source on guestBusyTimesFormatted entries
sochanged 37f58e2
fix(bookings): skip excludeUid filter on empty string + lock in dedup…
sochanged 7cec9fb
fix(schedule): normalize rescheduledBy URL param client-side
sochanged 577cb17
docs+test(slots): document attendee-default gating + tighten host-bra…
sochanged File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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,223 @@ 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" }, | ||
| }), | ||
| }) | ||
| ); | ||
| }); | ||
|
|
||
| it("should skip excludeUid filter when empty string is passed", async () => { | ||
| mockPrismaClient.booking.findMany.mockResolvedValue([]); | ||
|
|
||
| await repository.findByUserIdsAndDateRange({ | ||
| userIds: [1], | ||
| userEmails: [], | ||
| dateFrom, | ||
| dateTo, | ||
| excludeUid: "", | ||
| }); | ||
|
|
||
| const callArgs = mockPrismaClient.booking.findMany.mock.calls[0][0]; | ||
| expect(callArgs.where).not.toHaveProperty("uid"); | ||
| }); | ||
|
|
||
| it("should dedupe by uid when OR branches surface the same booking twice", async () => { | ||
| mockPrismaClient.booking.findMany.mockResolvedValue([ | ||
| { | ||
| uid: "booking-a", | ||
| startTime: new Date("2026-04-10T09:00:00Z"), | ||
| endTime: new Date("2026-04-10T10:00:00Z"), | ||
| title: "Team sync", | ||
| userId: 10, | ||
| status: "ACCEPTED", | ||
| }, | ||
| { | ||
| uid: "booking-a", | ||
| startTime: new Date("2026-04-10T09:00:00Z"), | ||
| endTime: new Date("2026-04-10T10:00:00Z"), | ||
| title: "Team sync", | ||
| userId: 10, | ||
| status: "ACCEPTED", | ||
| }, | ||
| ]); | ||
|
|
||
| const result = await repository.findByUserIdsAndDateRange({ | ||
| userIds: [10], | ||
| userEmails: ["guest@example.com"], | ||
| dateFrom, | ||
| dateTo, | ||
| }); | ||
|
|
||
| expect(result).toHaveLength(1); | ||
| expect(result[0].uid).toBe("booking-a"); | ||
| }); | ||
| }); | ||
| }); | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sanitize cache/log handling before adding email-valued
rescheduledBy.Line 107 forwards
rescheduledBy, which is later compared tooriginal.user.email, so this is an email/user identifier. The slots cache currently JSON-stringifies the full input and logs the resultingcacheKeyinpackages/trpc/server/routers/viewer/slots/util.tsLines 110, 131, and 139. Please make sure the server hashes or redacts that cache key/log payload so this new identifier is not persisted or emitted in raw form.🤖 Prompt for AI Agents