Skip to content

Commit 77f59b1

Browse files
chore: Integrate confirmation booking audit (calcom#26523)
* chore: Integrate booking confirmation booking audit Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * fix: Use BookingStatus type instead of string for booking.status Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * Make booking-audit integration test utils reusable * refactor: enhance handlePaymentSuccess to accept appSlug and actor identification - Updated handlePaymentSuccess function to accept an object with paymentId, bookingId, appSlug, and traceContext. - Introduced getActor function to determine the actor based on appSlug and credentialId. - Modified webhook handlers for Alby, BTCPayServer, HitPay, PayPal, and Stripe to pass the new parameters. - Improved logging for missing credentialId in payment processing. - Adjusted related schemas to ensure proper type handling for booking status and actor identification. * fix: Correct import paths and getActor function call Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * fix: Use ActorIdentification type instead of AuditActor in getActor Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * fix: Add guard clause for undefined actor in fireBookingAcceptedEvent Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * fix: Add actionSource to all confirm calls Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * refactor: Integrate actor identification and action source updates across booking services - Added `makeUserActor` to various booking service files to enhance actor identification. - Updated action source handling in booking confirmation and related functions to include "MAGIC_LINK". - Refactored schemas to accommodate new actor and action source requirements, ensuring consistent type handling. - Improved error handling and logging for booking actions to enhance traceability. * Remvoe formatting changes * Add test * refactor: Enhance booking confirmation tests and event handling - Updated `confirm.handler.test.ts` to improve test coverage for booking acceptance and rejection scenarios, including bulk bookings. - Refactored the mock implementation of `BookingEventHandlerService` to streamline event handling for accepted and rejected bookings. - Adjusted the `handleConfirmation` function to utilize `acceptedBookings` for better clarity and maintainability. - Added tests to ensure correct invocation of event handler methods for both single and bulk booking confirmations and rejections. * fix tests * refactor: Use confirmHandler directly in link and verify-booking-token routes Refactors the link and verify-booking-token API routes to call confirmHandler directly instead of using the tRPC caller pattern. This simplifies the code by removing the need for session getter, legacy request building, and context creation. Changes: - apps/web/app/api/link/route.ts: Use confirmHandler directly with ctx and input - apps/web/app/api/verify-booking-token/route.ts: Use confirmHandler directly with ctx and input - Updated tests to mock confirmHandler instead of tRPC caller Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * fix ts errors due to merge frommain * feat: introduce getAppActor utility for actor creation This commit adds a new utility function, getAppActor, to streamline the process of creating actor objects for apps based on their slug and credential ID. The function is integrated into handlePaymentSuccess and handleStripePaymentSuccess, replacing the previous inline actor creation logic. This refactor enhances code maintainability and readability by centralizing actor creation logic. Changes: - New file: packages/app-store/_utils/getAppActor.ts - Updated handlePaymentSuccess and handleStripePaymentSuccess to use getAppActor - Removed redundant actor creation code from these functions * refactor: Update appSlug in payment success handlers to use appConfig.slug This commit modifies the appSlug parameter in the handlePaymentSuccess function across multiple payment webhook handlers to utilize the appConfig.slug value instead of hardcoded strings. This change enhances consistency and maintainability of the code. Changes: - Updated appSlug in handlePaymentSuccess for btcpayserver, hitpay, and paypal webhooks. - Adjusted a test case to reflect the new appSlug value for consistency. * test: Enhance booking token verification and audit action tests This commit adds additional assertions to the booking token verification tests to ensure that the confirmHandler is not called when an error occurs. It also introduces a mechanism to clean up additional booking UIDs after each test in the accepted action integration tests, improving test reliability. Furthermore, it updates the action source schema comment for clarity. Changes: - Updated tests in `verify-booking-token` to check that `mockConfirmHandler` is not called on error. - Implemented cleanup logic for additional booking UIDs in `accepted-action.integration-test.ts`. - Clarified comment in `actionSource.ts` regarding the schema for action sources. - Refactored `handleConfirmation.ts` and `confirm.handler.ts` to include tracing logger for error handling in booking events. --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent c9bb03b commit 77f59b1

26 files changed

Lines changed: 1782 additions & 415 deletions

File tree

apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts

Lines changed: 35 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ import {
6969
import type { RescheduleSeatedBookingInput_2024_08_13 } from "@calcom/platform-types";
7070
import type { PrismaClient } from "@calcom/prisma";
7171
import type { EventType, User, Team } from "@calcom/prisma/client";
72+
import { makeUserActor } from "@calcom/features/booking-audit/lib/makeActor";
7273

7374
type CreatedBooking = {
7475
hosts: { id: number }[];
@@ -921,40 +922,40 @@ export class BookingsService_2024_08_13 {
921922
return await this.getBooking(recurringBookingUid, authUser);
922923
}
923924

924-
async markAbsent(
925-
bookingUid: string,
926-
bookingOwnerId: number,
927-
body: MarkAbsentBookingInput_2024_08_13,
928-
userUuid?: string
929-
) {
930-
const bodyTransformed = this.inputService.transformInputMarkAbsentBooking(body);
931-
const bookingBefore = await this.bookingsRepository.getByUid(bookingUid);
932-
933-
if (!bookingBefore) {
934-
throw new NotFoundException(`Booking with uid=${bookingUid} not found.`);
935-
}
925+
async markAbsent(
926+
bookingUid: string,
927+
bookingOwnerId: number,
928+
body: MarkAbsentBookingInput_2024_08_13,
929+
userUuid?: string
930+
) {
931+
const bodyTransformed = this.inputService.transformInputMarkAbsentBooking(body);
932+
const bookingBefore = await this.bookingsRepository.getByUid(bookingUid);
933+
934+
if (!bookingBefore) {
935+
throw new NotFoundException(`Booking with uid=${bookingUid} not found.`);
936+
}
936937

937-
const nowUtc = DateTime.utc();
938-
const bookingStartTimeUtc = DateTime.fromJSDate(bookingBefore.startTime, { zone: "utc" });
938+
const nowUtc = DateTime.utc();
939+
const bookingStartTimeUtc = DateTime.fromJSDate(bookingBefore.startTime, { zone: "utc" });
939940

940-
if (nowUtc < bookingStartTimeUtc) {
941-
throw new BadRequestException(
942-
`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()}`
943-
);
944-
}
941+
if (nowUtc < bookingStartTimeUtc) {
942+
throw new BadRequestException(
943+
`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()}`
944+
);
945+
}
945946

946-
const platformClientParams = bookingBefore?.eventTypeId
947-
? await this.platformBookingsService.getOAuthClientParams(bookingBefore.eventTypeId)
948-
: undefined;
947+
const platformClientParams = bookingBefore?.eventTypeId
948+
? await this.platformBookingsService.getOAuthClientParams(bookingBefore.eventTypeId)
949+
: undefined;
949950

950-
await handleMarkNoShow({
951-
bookingUid,
952-
attendees: bodyTransformed.attendees,
953-
noShowHost: bodyTransformed.noShowHost,
954-
userId: bookingOwnerId,
955-
userUuid,
956-
platformClientParams,
957-
});
951+
await handleMarkNoShow({
952+
bookingUid,
953+
attendees: bodyTransformed.attendees,
954+
noShowHost: bodyTransformed.noShowHost,
955+
userId: bookingOwnerId,
956+
userUuid,
957+
platformClientParams,
958+
});
958959

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

@@ -1146,6 +1147,8 @@ export class BookingsService_2024_08_13 {
11461147
recurringEventId: booking.recurringEventId ?? undefined,
11471148
emailsEnabled,
11481149
platformClientParams,
1150+
actionSource: "API_V2",
1151+
actor: makeUserActor(requestUser.uuid),
11491152
},
11501153
});
11511154

@@ -1179,6 +1182,8 @@ export class BookingsService_2024_08_13 {
11791182
reason,
11801183
emailsEnabled,
11811184
platformClientParams,
1185+
actionSource: "API_V2",
1186+
actor: makeUserActor(requestUser.uuid),
11821187
},
11831188
});
11841189

apps/web/app/api/link/__tests__/route.test.ts

Lines changed: 125 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ vi.mock("@calcom/lib/tracing/factory", () => ({
7878
},
7979
}));
8080

81+
vi.mock("@calcom/features/booking-audit/lib/makeActor", () => ({
82+
makeUserActor: vi.fn().mockReturnValue({ type: "user", id: "test-uuid" }),
83+
}));
84+
8185
// Import after mocks are set up
8286
import { GET } from "../route";
8387
import prisma from "@calcom/prisma";
@@ -171,7 +175,9 @@ describe("link route", () => {
171175
const { TRPCError } = await import("@trpc/server");
172176

173177
// Mock confirmHandler to throw a TRPCError
174-
mockConfirmHandler.mockRejectedValueOnce(new TRPCError({ code: "BAD_REQUEST", message: "Custom error" }));
178+
mockConfirmHandler.mockRejectedValueOnce(
179+
new TRPCError({ code: "BAD_REQUEST", message: "Custom error" })
180+
);
175181

176182
const baseUrl = "https://app.example.com/api/link?action=accept&token=encrypted-token";
177183
const req = createMockRequest(baseUrl);
@@ -206,4 +212,122 @@ describe("link route", () => {
206212
expect(location).not.toContain("localhost");
207213
});
208214
});
215+
216+
describe("confirmHandler flow", () => {
217+
it("should call confirmHandler with correct arguments for accept action", async () => {
218+
const baseUrl = "https://app.example.com/api/link?action=accept&token=encrypted-token";
219+
const req = createMockRequest(baseUrl);
220+
221+
await GET(req, { params: Promise.resolve({}) });
222+
223+
expect(mockConfirmHandler).toHaveBeenCalledWith(
224+
expect.objectContaining({
225+
input: expect.objectContaining({
226+
bookingId: 1,
227+
confirmed: true,
228+
emailsEnabled: true,
229+
actionSource: "MAGIC_LINK",
230+
}),
231+
})
232+
);
233+
});
234+
235+
it("should call confirmHandler with confirmed=false for reject action", async () => {
236+
const baseUrl = "https://app.example.com/api/link?action=reject&token=encrypted-token";
237+
const req = createMockRequest(baseUrl);
238+
239+
await GET(req, { params: Promise.resolve({}) });
240+
241+
expect(mockConfirmHandler).toHaveBeenCalledWith(
242+
expect.objectContaining({
243+
input: expect.objectContaining({
244+
bookingId: 1,
245+
confirmed: false,
246+
emailsEnabled: true,
247+
actionSource: "MAGIC_LINK",
248+
}),
249+
})
250+
);
251+
});
252+
253+
it("should call confirmHandler with reason when provided in query params", async () => {
254+
const baseUrl =
255+
"https://app.example.com/api/link?action=reject&token=encrypted-token&reason=test-reason";
256+
const req = createMockRequest(baseUrl);
257+
258+
await GET(req, { params: Promise.resolve({}) });
259+
260+
expect(mockConfirmHandler).toHaveBeenCalledWith(
261+
expect.objectContaining({
262+
input: expect.objectContaining({
263+
bookingId: 1,
264+
confirmed: false,
265+
reason: "test-reason",
266+
emailsEnabled: true,
267+
actionSource: "MAGIC_LINK",
268+
}),
269+
})
270+
);
271+
});
272+
273+
it("should pass recurringEventId when booking has one", async () => {
274+
// Update mock to return booking with recurringEventId
275+
vi.mocked(prisma.booking.findUniqueOrThrow).mockResolvedValueOnce({
276+
id: 1,
277+
uid: "test-booking-uid",
278+
recurringEventId: "recurring-123",
279+
} as Awaited<ReturnType<typeof prisma.booking.findUniqueOrThrow>>);
280+
281+
const baseUrl = "https://app.example.com/api/link?action=accept&token=encrypted-token";
282+
const req = createMockRequest(baseUrl);
283+
284+
await GET(req, { params: Promise.resolve({}) });
285+
286+
expect(mockConfirmHandler).toHaveBeenCalledWith(
287+
expect.objectContaining({
288+
input: expect.objectContaining({
289+
bookingId: 1,
290+
recurringEventId: "recurring-123",
291+
confirmed: true,
292+
}),
293+
})
294+
);
295+
});
296+
297+
it("should pass user context to confirmHandler", async () => {
298+
const baseUrl = "https://app.example.com/api/link?action=accept&token=encrypted-token";
299+
const req = createMockRequest(baseUrl);
300+
301+
await GET(req, { params: Promise.resolve({}) });
302+
303+
expect(mockConfirmHandler).toHaveBeenCalledWith(
304+
expect.objectContaining({
305+
ctx: expect.objectContaining({
306+
user: expect.objectContaining({
307+
id: 1,
308+
uuid: "user-uuid",
309+
email: "test@example.com",
310+
username: "testuser",
311+
role: "USER",
312+
}),
313+
}),
314+
})
315+
);
316+
});
317+
318+
it("should pass actor from makeUserActor to confirmHandler", async () => {
319+
const baseUrl = "https://app.example.com/api/link?action=accept&token=encrypted-token";
320+
const req = createMockRequest(baseUrl);
321+
322+
await GET(req, { params: Promise.resolve({}) });
323+
324+
expect(mockConfirmHandler).toHaveBeenCalledWith(
325+
expect.objectContaining({
326+
input: expect.objectContaining({
327+
actor: { type: "user", id: "test-uuid" },
328+
}),
329+
})
330+
);
331+
});
332+
});
209333
});

apps/web/app/api/link/route.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { distributedTracing } from "@calcom/lib/tracing/factory";
88
import prisma from "@calcom/prisma";
99
import { confirmHandler } from "@calcom/trpc/server/routers/viewer/bookings/confirm.handler";
1010
import { TRPCError } from "@trpc/server";
11+
import { makeUserActor } from "@calcom/features/booking-audit/lib/makeActor";
1112

1213
enum DirectAction {
1314
ACCEPT = "accept",
@@ -90,6 +91,8 @@ async function handler(request: NextRequest) {
9091
platformBookingUrl,
9192
}
9293
: undefined,
94+
actionSource: "MAGIC_LINK",
95+
actor: makeUserActor(user.uuid),
9396
},
9497
});
9598
} catch (e) {

0 commit comments

Comments
 (0)