diff --git a/docs/developing/guides/automation/webhooks.mdx b/docs/developing/guides/automation/webhooks.mdx index d4d4a8952d..ff2f863d6a 100644 --- a/docs/developing/guides/automation/webhooks.mdx +++ b/docs/developing/guides/automation/webhooks.mdx @@ -311,7 +311,8 @@ Select a version and trigger event to view the example payload: "requiresConfirmation": true, "price": null, "currency": "usd", - "status": "CANCELLED" + "status": "CANCELLED", + "requestReschedule": false } } ``` diff --git a/packages/features/bookings/lib/handleCancelBooking.ts b/packages/features/bookings/lib/handleCancelBooking.ts index 19ffa6e88e..a1dee57b98 100644 --- a/packages/features/bookings/lib/handleCancelBooking.ts +++ b/packages/features/bookings/lib/handleCancelBooking.ts @@ -54,7 +54,12 @@ import { getBookingToDelete } from "./getBookingToDelete"; import { handleInternalNote } from "./handleInternalNote"; import cancelAttendeeSeat from "./handleSeats/cancel/cancelAttendeeSeat"; import type { IBookingCancelService } from "./interfaces/IBookingCancelService"; -import { buildActorEmail, getUniqueIdentifier, makeGuestActor, makeUserActor } from "@calcom/features/booking-audit/lib/makeActor"; +import { + buildActorEmail, + getUniqueIdentifier, + makeGuestActor, + makeUserActor, +} from "@calcom/features/booking-audit/lib/makeActor"; import type { Actor } from "@calcom/features/booking-audit/lib/dto/types"; const log = logger.getSubLogger({ prefix: ["handleCancelBooking"] }); @@ -102,12 +107,17 @@ function getAuditActor({ }) ); // Having fallback prefix makes it clear that we created guest actor from fallback logic - actorEmail = buildActorEmail({ identifier: getUniqueIdentifier({ prefix: "fallback" }), actorType: "guest" }); - } - else { + actorEmail = buildActorEmail({ + identifier: getUniqueIdentifier({ prefix: "fallback" }), + actorType: "guest", + }); + } else { // We can't trust cancelledByEmail and thus can't reuse it as is because it can be set anything by anyone. If we use that as guest actor, we could accidentally attribute the action to the wrong guest actor. // Having param prefix makes it clear that we created guest actor from query param and we still don't use the email as is. - actorEmail = buildActorEmail({ identifier: getUniqueIdentifier({ prefix: "param" }), actorType: "guest" }); + actorEmail = buildActorEmail({ + identifier: getUniqueIdentifier({ prefix: "param" }), + actorType: "guest", + }); } return makeGuestActor({ email: actorEmail, name: null }); @@ -141,10 +151,13 @@ async function handler(input: CancelBookingInput) { // Extract action source once for reuse const actionSource = input.actionSource ?? "UNKNOWN"; if (actionSource === "UNKNOWN") { - log.warn("Booking cancellation with unknown actionSource", safeStringify({ - bookingUid: bookingToDelete.uid, - userUuid, - })); + log.warn( + "Booking cancellation with unknown actionSource", + safeStringify({ + bookingUid: bookingToDelete.uid, + userUuid, + }) + ); } const actorToUse = getAuditActor({ @@ -358,12 +371,12 @@ async function handler(input: CancelBookingInput) { 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, @@ -404,6 +417,7 @@ async function handler(input: CancelBookingInput) { status: "CANCELLED", smsReminderNumber: bookingToDelete.smsReminderNumber || undefined, cancelledBy: cancelledBy, + requestReschedule: false, }).catch((e) => { logger.error( `Error executing webhook for event: ${eventTrigger}, URL: ${webhook.subscriberUrl}, bookingId: ${evt.bookingId}, bookingUid: ${evt.uid}`, @@ -714,7 +728,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 = { diff --git a/packages/features/bookings/lib/handleSeats/cancel/cancelAttendeeSeat.ts b/packages/features/bookings/lib/handleSeats/cancel/cancelAttendeeSeat.ts index 00cb2f50e1..7616b6b448 100644 --- a/packages/features/bookings/lib/handleSeats/cancel/cancelAttendeeSeat.ts +++ b/packages/features/bookings/lib/handleSeats/cancel/cancelAttendeeSeat.ts @@ -164,6 +164,7 @@ async function cancelAttendeeSeat( ...eventTypeInfo, status: "CANCELLED", smsReminderNumber: bookingToDelete.smsReminderNumber || undefined, + requestReschedule: false, }; const promises = webhooks.map((webhook) => diff --git a/packages/features/bookings/repositories/BookingRepository.ts b/packages/features/bookings/repositories/BookingRepository.ts index 37d569adc9..64e2656ddf 100644 --- a/packages/features/bookings/repositories/BookingRepository.ts +++ b/packages/features/bookings/repositories/BookingRepository.ts @@ -139,8 +139,8 @@ const buildWhereClauseForActiveBookings = ({ }, ...(!includeNoShowInRRCalculation ? { - OR: [{ noShowHost: false }, { noShowHost: null }], - } + OR: [{ noShowHost: false }, { noShowHost: null }], + } : {}), }, { @@ -159,24 +159,24 @@ const buildWhereClauseForActiveBookings = ({ ...(startDate || endDate ? rrTimestampBasis === RRTimestampBasis.CREATED_AT ? { - createdAt: { - ...(startDate ? { gte: startDate } : {}), - ...(endDate ? { lte: endDate } : {}), - }, - } + createdAt: { + ...(startDate ? { gte: startDate } : {}), + ...(endDate ? { lte: endDate } : {}), + }, + } : { - startTime: { - ...(startDate ? { gte: startDate } : {}), - ...(endDate ? { lte: endDate } : {}), - }, - } + startTime: { + ...(startDate ? { gte: startDate } : {}), + ...(endDate ? { lte: endDate } : {}), + }, + } : {}), ...(virtualQueuesData ? { - routedFromRoutingFormReponse: { - chosenRouteId: virtualQueuesData.chosenRouteId, - }, - } + routedFromRoutingFormReponse: { + chosenRouteId: virtualQueuesData.chosenRouteId, + }, + } : {}), }); @@ -325,7 +325,7 @@ const selectStatementToGetBookingForCalEventBuilder = { }; export class BookingRepository { - constructor(private prismaClient: PrismaClient) { } + constructor(private prismaClient: PrismaClient) {} /** * Gets the fromReschedule field for a booking by UID @@ -656,20 +656,20 @@ export class BookingRepository { const currentBookingsAllUsersQueryThree = eventTypeId ? this.prismaClient.booking.findMany({ - where: { - startTime: { lte: endDate }, - endTime: { gte: startDate }, - eventType: { - id: eventTypeId, - requiresConfirmation: true, - requiresConfirmationWillBlockSlot: true, + where: { + startTime: { lte: endDate }, + endTime: { gte: startDate }, + eventType: { + id: eventTypeId, + requiresConfirmation: true, + requiresConfirmationWillBlockSlot: true, + }, + status: { + in: [BookingStatus.PENDING], + }, }, - status: { - in: [BookingStatus.PENDING], - }, - }, - select: bookingsSelect, - }) + select: bookingsSelect, + }) : []; const [resultOne, resultTwo, resultThree] = await Promise.all([ @@ -1659,6 +1659,8 @@ export class BookingRepository { teamId: true, parentId: true, slug: true, + title: true, + length: true, hideOrganizerEmail: true, customReplyToEmail: true, bookingFields: true, @@ -1683,6 +1685,7 @@ export class BookingRepository { workflowReminders: true, responses: true, iCalUID: true, + iCalSequence: true, }, }); } @@ -1913,7 +1916,13 @@ export class BookingRepository { }); } - async updateRecordedStatus({ bookingUid, isRecorded }: { bookingUid: string; isRecorded: boolean }): Promise { + async updateRecordedStatus({ + bookingUid, + isRecorded, + }: { + bookingUid: string; + isRecorded: boolean; + }): Promise { await this.prismaClient.booking.update({ where: { uid: bookingUid }, data: { isRecorded }, diff --git a/packages/features/webhooks/lib/dto/types.ts b/packages/features/webhooks/lib/dto/types.ts index 89db2bcd1b..b3e0890e2b 100644 --- a/packages/features/webhooks/lib/dto/types.ts +++ b/packages/features/webhooks/lib/dto/types.ts @@ -50,9 +50,11 @@ export interface BookingCancelledDTO extends BaseEventDTO { eventTypeId: number | null; userId: number | null; smsReminderNumber?: string | null; + iCalSequence?: number | null; }; cancelledBy?: string; cancellationReason?: string; + requestReschedule?: boolean; } export interface BookingRejectedDTO extends BaseEventDTO { @@ -582,6 +584,7 @@ export type EventPayloadType = CalendarEvent & rescheduledBy?: string; cancelledBy?: string; paymentData?: Record; + requestReschedule?: boolean; }; // dto/types.ts diff --git a/packages/features/webhooks/lib/factory/base/BaseBookingPayloadBuilder.ts b/packages/features/webhooks/lib/factory/base/BaseBookingPayloadBuilder.ts index c92727d42c..f26f4f8d79 100644 --- a/packages/features/webhooks/lib/factory/base/BaseBookingPayloadBuilder.ts +++ b/packages/features/webhooks/lib/factory/base/BaseBookingPayloadBuilder.ts @@ -11,7 +11,11 @@ import type { IBookingPayloadBuilder } from "../versioned/PayloadBuilderFactory" */ export type BookingExtraDataMap = { [WebhookTriggerEvents.BOOKING_CREATED]: null; - [WebhookTriggerEvents.BOOKING_CANCELLED]: { cancelledBy?: string; cancellationReason?: string }; + [WebhookTriggerEvents.BOOKING_CANCELLED]: { + cancelledBy?: string; + cancellationReason?: string; + requestReschedule?: boolean; + }; [WebhookTriggerEvents.BOOKING_REQUESTED]: null; [WebhookTriggerEvents.BOOKING_REJECTED]: null; [WebhookTriggerEvents.BOOKING_RESCHEDULED]: { diff --git a/packages/features/webhooks/lib/factory/versioned/v2021-10-20/BookingPayloadBuilder.ts b/packages/features/webhooks/lib/factory/versioned/v2021-10-20/BookingPayloadBuilder.ts index 8f2d65e050..e6e57c4500 100644 --- a/packages/features/webhooks/lib/factory/versioned/v2021-10-20/BookingPayloadBuilder.ts +++ b/packages/features/webhooks/lib/factory/versioned/v2021-10-20/BookingPayloadBuilder.ts @@ -45,6 +45,7 @@ export class BookingPayloadBuilder extends BaseBookingPayloadBuilder { extra: { cancelledBy: dto.cancelledBy, cancellationReason: dto.cancellationReason, + requestReschedule: dto.requestReschedule ?? false, }, }); diff --git a/packages/features/webhooks/lib/sendPayload.ts b/packages/features/webhooks/lib/sendPayload.ts index e8487d799b..106f0cc06e 100644 --- a/packages/features/webhooks/lib/sendPayload.ts +++ b/packages/features/webhooks/lib/sendPayload.ts @@ -96,6 +96,7 @@ export type EventPayloadType = CalendarEvent & rescheduledBy?: string; cancelledBy?: string; paymentData?: PaymentData; + requestReschedule?: boolean; }; export type WebhookPayloadType = diff --git a/packages/features/webhooks/lib/service/BookingWebhookService.ts b/packages/features/webhooks/lib/service/BookingWebhookService.ts index 06cc1454d0..8cfb3c7d1a 100644 --- a/packages/features/webhooks/lib/service/BookingWebhookService.ts +++ b/packages/features/webhooks/lib/service/BookingWebhookService.ts @@ -116,6 +116,7 @@ export class BookingWebhookService implements IBookingWebhookService { booking: params.booking, cancelledBy: params.cancelledBy, cancellationReason: params.cancellationReason, + requestReschedule: params.requestReschedule, }; await this.webhookNotifier.emitWebhook(dto, params.isDryRun); diff --git a/packages/features/webhooks/lib/types/params.ts b/packages/features/webhooks/lib/types/params.ts index 300bb7720b..1493b66a55 100644 --- a/packages/features/webhooks/lib/types/params.ts +++ b/packages/features/webhooks/lib/types/params.ts @@ -44,6 +44,7 @@ export interface BookingCancelledParams { eventTypeId: number | null; userId: number | null; smsReminderNumber?: string | null; + iCalSequence?: number | null; }; eventType: { id: number; @@ -57,6 +58,7 @@ export interface BookingCancelledParams { }; cancelledBy?: string; cancellationReason?: string; + requestReschedule?: boolean; teamId?: number | null; orgId?: number | null; platformClientId?: string; diff --git a/packages/lib/server/service/BookingWebhookFactory.ts b/packages/lib/server/service/BookingWebhookFactory.ts index 44141099a7..4f3f6c4199 100644 --- a/packages/lib/server/service/BookingWebhookFactory.ts +++ b/packages/lib/server/service/BookingWebhookFactory.ts @@ -49,6 +49,11 @@ interface BaseWebhookPayload { interface CancelledEventPayload extends BaseWebhookPayload { cancelledBy: string; cancellationReason: string; + eventTypeId?: number | null; + length?: number | null; + iCalSequence?: number | null; + eventTitle?: string | null; + requestReschedule?: boolean; } export class BookingWebhookFactory { @@ -122,6 +127,12 @@ export class BookingWebhookFactory { ...basePayload, cancelledBy: params.cancelledBy, cancellationReason: params.cancellationReason, + status: "CANCELLED" as const, + eventTypeId: params.eventTypeId ?? null, + length: params.length ?? null, + iCalSequence: params.iCalSequence ?? null, + eventTitle: params.eventTitle ?? null, + requestReschedule: params.requestReschedule ?? false, }; } } diff --git a/packages/lib/server/service/__tests__/BookingWebhookFactory.test.ts b/packages/lib/server/service/__tests__/BookingWebhookFactory.test.ts index 11da828849..ddfc3e4fb2 100644 --- a/packages/lib/server/service/__tests__/BookingWebhookFactory.test.ts +++ b/packages/lib/server/service/__tests__/BookingWebhookFactory.test.ts @@ -92,6 +92,11 @@ describe("BookingWebhookFactory", () => { "description", "customInputs", "responses", + "eventTitle", + "eventTypeId", + "length", + "requestReschedule", + "iCalSequence", "userFieldsResponses", "startTime", "endTime", @@ -104,6 +109,7 @@ describe("BookingWebhookFactory", () => { "smsReminderNumber", "cancellationReason", "cancelledBy", + "status", ]; const actualKeys = Object.keys(payload).sort(); expect(actualKeys).toEqual(expectedKeys.sort()); diff --git a/packages/trpc/server/routers/viewer/bookings/requestReschedule.handler.ts b/packages/trpc/server/routers/viewer/bookings/requestReschedule.handler.ts index 7ee5bd3fbd..5e60f3bde1 100644 --- a/packages/trpc/server/routers/viewer/bookings/requestReschedule.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/requestReschedule.handler.ts @@ -103,10 +103,7 @@ export const requestRescheduleHandler = async ({ ctx, input, source }: RequestRe throw new TRPCError({ code: "FORBIDDEN", message: "User isn't owner of the current booking" }); } - let event: Partial = {}; - if (bookingToReschedule.eventType) { - event = bookingToReschedule.eventType; - } + const event: Partial = bookingToReschedule.eventType ?? {}; await bookingRepository.updateBookingStatus({ bookingId: bookingToReschedule.id, status: BookingStatus.CANCELLED, @@ -133,6 +130,7 @@ export const requestRescheduleHandler = async ({ ctx, input, source }: RequestRe const usersToPeopleType = (users: PersonAttendeeCommonFields[], selectedLanguage: TFunction): Person[] => { return users?.map((user) => { return { + id: user.id, email: user.email || "", name: user.name || "", username: user?.username || "", @@ -274,6 +272,11 @@ export const requestRescheduleHandler = async ({ ctx, input, source }: RequestRe smsReminderNumber: bookingToReschedule.smsReminderNumber, }), cancelledBy: user.email, + eventTypeId: bookingToReschedule.eventTypeId, + length: bookingToReschedule.eventType?.length ?? null, + iCalSequence: (bookingToReschedule.iCalSequence ?? 0) + 1, + eventTitle: bookingToReschedule.eventType?.title ?? null, + requestReschedule: true, }); // Send webhook