Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
ae92162
chore: Integrate booking confirmation booking audit
devin-ai-integration[bot] Jan 7, 2026
41c2bdb
fix: Use BookingStatus type instead of string for booking.status
devin-ai-integration[bot] Jan 7, 2026
e1f4470
Make booking-audit integration test utils reusable
hariombalhara Jan 7, 2026
d3f4287
Merge remote-tracking branch 'origin/make-audit-integration-tests-reu…
hariombalhara Jan 7, 2026
c9800d8
refactor: enhance handlePaymentSuccess to accept appSlug and actor id…
hariombalhara Jan 7, 2026
5c07201
fix: Correct import paths and getActor function call
devin-ai-integration[bot] Jan 7, 2026
06fe8bd
fix: Use ActorIdentification type instead of AuditActor in getActor
devin-ai-integration[bot] Jan 7, 2026
8f1eae8
fix: Add guard clause for undefined actor in fireBookingAcceptedEvent
devin-ai-integration[bot] Jan 7, 2026
ea9ed6c
fix: Add actionSource to all confirm calls
devin-ai-integration[bot] Jan 7, 2026
5953813
refactor: Integrate actor identification and action source updates ac…
hariombalhara Jan 7, 2026
79c14f5
Remvoe formatting changes
hariombalhara Jan 7, 2026
e823548
Merge remote-tracking branch 'origin/main' into devin/booking-confirm…
hariombalhara Jan 7, 2026
620aa2a
Merge remote-tracking branch 'origin/main' into devin/booking-confirm…
hariombalhara Jan 7, 2026
cdedd66
Merge branch 'devin/booking-confirmation-audit-1767764589' of github.…
hariombalhara Jan 7, 2026
7a5cddf
Merge remote-tracking branch 'origin/main' into devin/booking-confirm…
hariombalhara Jan 7, 2026
e6154b2
feat: Implement bulk rejection handling for bookings
hariombalhara Jan 7, 2026
472ed09
Add test
hariombalhara Jan 7, 2026
4676fea
refactor: Enhance booking confirmation tests and event handling
hariombalhara Jan 7, 2026
360bacc
fix tests
hariombalhara Jan 8, 2026
3e2a5d3
Merge remote-tracking branch 'origin/main' into devin/booking-confirm…
hariombalhara Jan 8, 2026
1798394
refactor: Use confirmHandler directly in link and verify-booking-toke…
devin-ai-integration[bot] Jan 8, 2026
cbf0cfd
Merge branch 'devin/refactor-confirm-handler-direct-call-1767852097' …
devin-ai-integration[bot] Jan 8, 2026
01b2898
fix ts errors due to merge frommain
hariombalhara Jan 8, 2026
2390a37
feat: introduce getAppActor utility for actor creation
hariombalhara Jan 8, 2026
039ed6a
Merge remote-tracking branch 'origin/main' into devin/booking-confirm…
hariombalhara Jan 8, 2026
de1e3c4
Merge branch 'devin/booking-confirmation-audit-1767764589' of github.…
hariombalhara Jan 8, 2026
b0d0f5b
Merge remote-tracking branch 'origin/main' into devin/booking-confirm…
hariombalhara Jan 8, 2026
c4331d9
refactor: Update appSlug in payment success handlers to use appConfig…
hariombalhara Jan 8, 2026
577a001
test: Enhance booking token verification and audit action tests
hariombalhara Jan 8, 2026
6f44b99
Merge branch 'main' into devin/booking-confirmation-audit-1767764589
Udit-takkar Jan 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ import {
import type { RescheduleSeatedBookingInput_2024_08_13 } from "@calcom/platform-types";
import type { PrismaClient } from "@calcom/prisma";
import type { EventType, User, Team } from "@calcom/prisma/client";
import { makeUserActor } from "@calcom/features/booking-audit/lib/makeActor";

type CreatedBooking = {
hosts: { id: number }[];
Expand Down Expand Up @@ -921,40 +922,40 @@ export class BookingsService_2024_08_13 {
return await this.getBooking(recurringBookingUid, authUser);
}

async markAbsent(
bookingUid: string,
bookingOwnerId: number,
body: MarkAbsentBookingInput_2024_08_13,
userUuid?: string
) {
const bodyTransformed = this.inputService.transformInputMarkAbsentBooking(body);
const bookingBefore = await this.bookingsRepository.getByUid(bookingUid);

if (!bookingBefore) {
throw new NotFoundException(`Booking with uid=${bookingUid} not found.`);
}
async markAbsent(
bookingUid: string,
bookingOwnerId: number,
body: MarkAbsentBookingInput_2024_08_13,
userUuid?: string
) {
const bodyTransformed = this.inputService.transformInputMarkAbsentBooking(body);
const bookingBefore = await this.bookingsRepository.getByUid(bookingUid);

if (!bookingBefore) {
throw new NotFoundException(`Booking with uid=${bookingUid} not found.`);
}

const nowUtc = DateTime.utc();
const bookingStartTimeUtc = DateTime.fromJSDate(bookingBefore.startTime, { zone: "utc" });
const nowUtc = DateTime.utc();
const bookingStartTimeUtc = DateTime.fromJSDate(bookingBefore.startTime, { zone: "utc" });

if (nowUtc < bookingStartTimeUtc) {
throw new BadRequestException(
`Bookings can only be marked as absent after their scheduled start time. Current time in UTC+0: ${nowUtc.toISO()}, Booking start time in UTC+0: ${bookingStartTimeUtc.toISO()}`
);
}
if (nowUtc < bookingStartTimeUtc) {
throw new BadRequestException(
`Bookings can only be marked as absent after their scheduled start time. Current time in UTC+0: ${nowUtc.toISO()}, Booking start time in UTC+0: ${bookingStartTimeUtc.toISO()}`
);
}

const platformClientParams = bookingBefore?.eventTypeId
? await this.platformBookingsService.getOAuthClientParams(bookingBefore.eventTypeId)
: undefined;
const platformClientParams = bookingBefore?.eventTypeId
? await this.platformBookingsService.getOAuthClientParams(bookingBefore.eventTypeId)
: undefined;

await handleMarkNoShow({
bookingUid,
attendees: bodyTransformed.attendees,
noShowHost: bodyTransformed.noShowHost,
userId: bookingOwnerId,
userUuid,
platformClientParams,
});
await handleMarkNoShow({
bookingUid,
attendees: bodyTransformed.attendees,
noShowHost: bodyTransformed.noShowHost,
userId: bookingOwnerId,
userUuid,
platformClientParams,
});

const booking = await this.bookingsRepository.getByUidWithAttendeesAndUserAndEvent(bookingUid);

Expand Down Expand Up @@ -1146,6 +1147,8 @@ export class BookingsService_2024_08_13 {
recurringEventId: booking.recurringEventId ?? undefined,
emailsEnabled,
platformClientParams,
actionSource: "API_V2",
actor: makeUserActor(requestUser.uuid),
},
});

Expand Down Expand Up @@ -1179,6 +1182,8 @@ export class BookingsService_2024_08_13 {
reason,
emailsEnabled,
platformClientParams,
actionSource: "API_V2",
actor: makeUserActor(requestUser.uuid),
},
});

Expand Down
126 changes: 125 additions & 1 deletion apps/web/app/api/link/__tests__/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ vi.mock("@calcom/lib/tracing/factory", () => ({
},
}));

vi.mock("@calcom/features/booking-audit/lib/makeActor", () => ({
makeUserActor: vi.fn().mockReturnValue({ type: "user", id: "test-uuid" }),
}));

// Import after mocks are set up
import { GET } from "../route";
import prisma from "@calcom/prisma";
Expand Down Expand Up @@ -171,7 +175,9 @@ describe("link route", () => {
const { TRPCError } = await import("@trpc/server");

// Mock confirmHandler to throw a TRPCError
mockConfirmHandler.mockRejectedValueOnce(new TRPCError({ code: "BAD_REQUEST", message: "Custom error" }));
mockConfirmHandler.mockRejectedValueOnce(
new TRPCError({ code: "BAD_REQUEST", message: "Custom error" })
);

const baseUrl = "https://app.example.com/api/link?action=accept&token=encrypted-token";
const req = createMockRequest(baseUrl);
Expand Down Expand Up @@ -206,4 +212,122 @@ describe("link route", () => {
expect(location).not.toContain("localhost");
});
});

describe("confirmHandler flow", () => {
it("should call confirmHandler with correct arguments for accept action", async () => {
const baseUrl = "https://app.example.com/api/link?action=accept&token=encrypted-token";
const req = createMockRequest(baseUrl);

await GET(req, { params: Promise.resolve({}) });

expect(mockConfirmHandler).toHaveBeenCalledWith(
expect.objectContaining({
input: expect.objectContaining({
bookingId: 1,
confirmed: true,
emailsEnabled: true,
actionSource: "MAGIC_LINK",
}),
})
);
});

it("should call confirmHandler with confirmed=false for reject action", async () => {
const baseUrl = "https://app.example.com/api/link?action=reject&token=encrypted-token";
const req = createMockRequest(baseUrl);

await GET(req, { params: Promise.resolve({}) });

expect(mockConfirmHandler).toHaveBeenCalledWith(
expect.objectContaining({
input: expect.objectContaining({
bookingId: 1,
confirmed: false,
emailsEnabled: true,
actionSource: "MAGIC_LINK",
}),
})
);
});

it("should call confirmHandler with reason when provided in query params", async () => {
const baseUrl =
"https://app.example.com/api/link?action=reject&token=encrypted-token&reason=test-reason";
const req = createMockRequest(baseUrl);

await GET(req, { params: Promise.resolve({}) });

expect(mockConfirmHandler).toHaveBeenCalledWith(
expect.objectContaining({
input: expect.objectContaining({
bookingId: 1,
confirmed: false,
reason: "test-reason",
emailsEnabled: true,
actionSource: "MAGIC_LINK",
}),
})
);
});

it("should pass recurringEventId when booking has one", async () => {
// Update mock to return booking with recurringEventId
vi.mocked(prisma.booking.findUniqueOrThrow).mockResolvedValueOnce({
id: 1,
uid: "test-booking-uid",
recurringEventId: "recurring-123",
} as Awaited<ReturnType<typeof prisma.booking.findUniqueOrThrow>>);

const baseUrl = "https://app.example.com/api/link?action=accept&token=encrypted-token";
const req = createMockRequest(baseUrl);

await GET(req, { params: Promise.resolve({}) });

expect(mockConfirmHandler).toHaveBeenCalledWith(
expect.objectContaining({
input: expect.objectContaining({
bookingId: 1,
recurringEventId: "recurring-123",
confirmed: true,
}),
})
);
});

it("should pass user context to confirmHandler", async () => {
const baseUrl = "https://app.example.com/api/link?action=accept&token=encrypted-token";
const req = createMockRequest(baseUrl);

await GET(req, { params: Promise.resolve({}) });

expect(mockConfirmHandler).toHaveBeenCalledWith(
expect.objectContaining({
ctx: expect.objectContaining({
user: expect.objectContaining({
id: 1,
uuid: "user-uuid",
email: "test@example.com",
username: "testuser",
role: "USER",
}),
}),
})
);
});

it("should pass actor from makeUserActor to confirmHandler", async () => {
const baseUrl = "https://app.example.com/api/link?action=accept&token=encrypted-token";
const req = createMockRequest(baseUrl);

await GET(req, { params: Promise.resolve({}) });

expect(mockConfirmHandler).toHaveBeenCalledWith(
expect.objectContaining({
input: expect.objectContaining({
actor: { type: "user", id: "test-uuid" },
}),
})
);
});
});
});
3 changes: 3 additions & 0 deletions apps/web/app/api/link/route.ts
Copy link
Member Author

@hariombalhara hariombalhara Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved this file to use the confirm handler directly the way API V2 uses it, instead of going through the trpc caller flow. It simplified the file.

This was required because we don't want the trpc endpoint to accept actor and actionSource param as they should be derived by backend

Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { distributedTracing } from "@calcom/lib/tracing/factory";
import prisma from "@calcom/prisma";
import { confirmHandler } from "@calcom/trpc/server/routers/viewer/bookings/confirm.handler";
import { TRPCError } from "@trpc/server";
import { makeUserActor } from "@calcom/features/booking-audit/lib/makeActor";

enum DirectAction {
ACCEPT = "accept",
Expand Down Expand Up @@ -90,6 +91,8 @@ async function handler(request: NextRequest) {
platformBookingUrl,
}
: undefined,
actionSource: "MAGIC_LINK",
actor: makeUserActor(user.uuid),
},
});
} catch (e) {
Expand Down
Loading
Loading