Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
65 changes: 35 additions & 30 deletions apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts
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
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