Skip to content
Merged
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
53 changes: 53 additions & 0 deletions packages/features/booking-audit/todo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Booking Audit - TODO

## Pending Tasks

### 1. Add audit log for spam-blocked bookings (Early Return Path)
**Location**: `packages/features/bookings/lib/service/RegularBookingService.ts` (line ~1486)

**Issue**: When spam check blocks a booking and returns a decoy response, no audit log is created.

**Details**:
- Spam-blocked bookings return early with `isShortCircuitedBooking: true`
- These decoy bookings have `id: 0` and are never saved to the database
- No audit trail exists for these blocked attempts

**Considerations**:
- Should we create audit logs for bookings that never actually exist in the database?
- If yes, we need a special audit entry type (e.g., "BOOKING_BLOCKED_SPAM")
- Actor would be the blocked user (email/phone)
- Need to decide if this should be a security audit log vs booking audit log

**Recommendation**: Consider creating a separate security/spam audit log system rather than using booking audit for non-existent bookings.

---

### 2. Add audit log for seats booking early return
**Location**: `packages/features/bookings/lib/service/RegularBookingService.ts` (line ~1605)

**Issue**: When `handleSeats()` returns a booking (adding a new attendee to an existing seated event), the early return at line 1605 bypasses `onBookingCreated()`.

**Details**:
- Happens when adding attendees to existing seated bookings
- The function returns at line 1605 without calling `deps.bookingEventHandler.onBookingCreated()`
- Result: No audit log is created for the new booking/seat

**Solution**:
- Add `onBookingCreated()` call before the return statement at line 1605
- Similar to the fix implemented for PENDING bookings (line 675)
- Should capture the booking creation even when it's a seat addition

**Priority**: High - This affects actual bookings that exist in the database

---

## Completed Tasks

### ✅ 1. Add audit log for PENDING bookings early return
**Fixed**: 2024 (PR/Commit TBD)
- Created `buildBookingCreatedPayload()` helper function (line 416) to avoid code duplication
- Added `onBookingCreatedPayload()` call at line 698 before early return using the helper
- Updated main booking creation path (line 2277) to also use the helper function
- Ensures audit logs are created when existing PENDING bookings are returned
- Handles both new and existing bookings in the PENDING state

2 changes: 2 additions & 0 deletions packages/features/bookings/lib/getBookingToDelete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ export async function getBookingToDelete(id: number | undefined, uid: string | u
iCalUID: true,
iCalSequence: true,
status: true,
cancellationReason: true,
cancelledBy: true,
},
});
}
Expand Down
53 changes: 44 additions & 9 deletions packages/features/bookings/lib/handleCancelBooking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { FAKE_DAILY_CREDENTIAL } from "@calcom/app-store/dailyvideo/lib/VideoApi
import { eventTypeMetaDataSchemaWithTypedApps } from "@calcom/app-store/zod-utils";
import dayjs from "@calcom/dayjs";
import { sendCancelledEmailsAndSMS } from "@calcom/emails/email-manager";
import { getBookingEventHandlerService } from "@calcom/features/bookings/di/BookingEventHandlerService.container";
import EventManager from "@calcom/features/bookings/lib/EventManager";
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
import { processNoShowFeeOnCancellation } from "@calcom/features/bookings/lib/payment/processNoShowFeeOnCancellation";
Expand Down Expand Up @@ -51,6 +52,8 @@ import { getBookingToDelete } from "./getBookingToDelete";
import { handleInternalNote } from "./handleInternalNote";
import cancelAttendeeSeat from "./handleSeats/cancel/cancelAttendeeSeat";
import type { IBookingCancelService } from "./interfaces/IBookingCancelService";
import { makeSystemActor } from "./types/actor";
import type { Actor } from "./types/actor";

const log = logger.getSubLogger({ prefix: ["handleCancelBooking"] });

Expand All @@ -67,6 +70,12 @@ export type BookingToDelete = Awaited<ReturnType<typeof getBookingToDelete>>;
export type CancelBookingInput = {
userId?: number;
bookingData: z.infer<typeof bookingCancelInput>;
/**
* The actor performing the cancellation.
* Used for audit logging to track who cancelled the booking.
* Optional for backward compatibility - defaults to System actor if not provided.
*/
actor?: Actor;
} & PlatformParams;

async function handler(input: CancelBookingInput) {
Expand All @@ -90,6 +99,7 @@ async function handler(input: CancelBookingInput) {
platformClientId,
platformRescheduleUrl,
arePlatformEmailsEnabled,
actor,
} = input;

/**
Expand Down Expand Up @@ -292,17 +302,17 @@ async function handler(input: CancelBookingInput) {
destinationCalendar: bookingToDelete?.destinationCalendar
? [bookingToDelete?.destinationCalendar]
: bookingToDelete?.user.destinationCalendar
? [bookingToDelete?.user.destinationCalendar]
: [],
? [bookingToDelete?.user.destinationCalendar]
: [],
cancellationReason: cancellationReason,
...(teamMembers &&
teamId && {
team: {
name: bookingToDelete?.eventType?.team?.name || "Nameless",
members: teamMembers,
id: teamId,
},
}),
team: {
name: bookingToDelete?.eventType?.team?.name || "Nameless",
members: teamMembers,
id: teamId,
},
}),
seatsPerTimeSlot: bookingToDelete.eventType?.seatsPerTimeSlot,
seatsShowAttendees: bookingToDelete.eventType?.seatsShowAttendees,
iCalUID: bookingToDelete.iCalUID,
Expand Down Expand Up @@ -476,6 +486,31 @@ async function handler(input: CancelBookingInput) {
});
updatedBookings.push(updatedBooking);

try {
const bookingEventHandlerService = getBookingEventHandlerService();
const actorToUse = actor ?? makeSystemActor();
await bookingEventHandlerService.onBookingCancelled(
updatedBooking.uid,
actorToUse,
{
cancellationReason: {
old: bookingToDelete.cancellationReason,
new: cancellationReason ?? null,
},
cancelledBy: {
old: bookingToDelete.cancelledBy,
new: cancelledBy ?? null,
},
status: {
old: bookingToDelete.status,
new: "CANCELLED",
},
}
);
} catch (error) {
log.error("Failed to create booking audit log for cancellation", error);
}

if (bookingToDelete.payment.some((payment) => payment.paymentOption === "ON_BOOKING")) {
try {
await processPaymentRefund({
Expand Down Expand Up @@ -616,7 +651,7 @@ type BookingCancelServiceDependencies = {
* Handles both individual booking cancellations and bulk cancellations for recurring events.
*/
export class BookingCancelService implements IBookingCancelService {
constructor(private readonly deps: BookingCancelServiceDependencies) {}
constructor(private readonly deps: BookingCancelServiceDependencies) { }

async cancelBooking(input: { bookingData: CancelRegularBookingData; bookingMeta?: CancelBookingMeta }) {
const cancelBookingInput: CancelBookingInput = {
Expand Down
24 changes: 23 additions & 1 deletion packages/features/bookings/lib/handleConfirmation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,15 @@ import { getAllWorkflowsFromEventType } from "@calcom/trpc/server/routers/viewer
import type { AdditionalInformation, CalendarEvent } from "@calcom/types/Calendar";

import { getCalEventResponses } from "./getCalEventResponses";
import type { AcceptedAuditData } from "@calcom/features/booking-audit/lib/actions/AcceptedAuditActionService";
import { getBookingEventHandlerService } from "@calcom/features/bookings/di/BookingEventHandlerService.container";
import { scheduleNoShowTriggers } from "./handleNewBooking/scheduleNoShowTriggers";
import { makeUserActor } from "./types/actor";

const log = logger.getSubLogger({ prefix: ["[handleConfirmation] book:user"] });

export async function handleConfirmation(args: {
user: EventManagerUser & { username: string | null };
user: EventManagerUser & { username: string | null; uuid: string };
evt: CalendarEvent;
recurringEventId?: string;
prisma: PrismaClient;
Expand All @@ -44,6 +47,7 @@ export async function handleConfirmation(args: {
startTime: Date;
id: number;
uid: string;
status: BookingStatus;
eventType: {
currency: string;
description: string | null;
Expand Down Expand Up @@ -313,6 +317,24 @@ export async function handleConfirmation(args: {
},
});
updatedBookings.push(updatedBooking);

try {
const bookingEventHandlerService = getBookingEventHandlerService();
const auditData: AcceptedAuditData = {
status: {
old: booking.status,
new: BookingStatus.ACCEPTED,
},
};
const actor = makeUserActor(user.uuid);
await bookingEventHandlerService.onBookingAccepted(
updatedBooking.uid,
actor,
auditData
);
} catch (error) {
log.error("Failed to create booking audit log for confirmation", error);
}
}

const teamId = await getTeamIdFromEventType({
Expand Down
1 change: 1 addition & 0 deletions packages/features/bookings/lib/payment/getBooking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export async function getBooking(bookingId: number) {
user: {
select: {
id: true,
uuid: true,
username: true,
timeZone: true,
credentials: { select: credentialForCalendarServiceSelect },
Expand Down
Loading
Loading