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
3 changes: 2 additions & 1 deletion docs/developing/guides/automation/webhooks.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
```
Expand Down
46 changes: 30 additions & 16 deletions packages/features/bookings/lib/handleCancelBooking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"] });
Expand Down Expand Up @@ -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 });
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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}`,
Expand Down Expand Up @@ -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 = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ async function cancelAttendeeSeat(
...eventTypeInfo,
status: "CANCELLED",
smsReminderNumber: bookingToDelete.smsReminderNumber || undefined,
requestReschedule: false,
};

const promises = webhooks.map((webhook) =>
Expand Down
71 changes: 40 additions & 31 deletions packages/features/bookings/repositories/BookingRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,8 @@ const buildWhereClauseForActiveBookings = ({
},
...(!includeNoShowInRRCalculation
? {
OR: [{ noShowHost: false }, { noShowHost: null }],
}
OR: [{ noShowHost: false }, { noShowHost: null }],
}
: {}),
},
{
Expand All @@ -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,
},
}
: {}),
});

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
},
status: {
in: [BookingStatus.PENDING],
where: {
startTime: { lte: endDate },
endTime: { gte: startDate },
eventType: {
id: eventTypeId,
requiresConfirmation: true,
requiresConfirmationWillBlockSlot: true,
},
status: {
in: [BookingStatus.PENDING],
},
},
},
select: bookingsSelect,
})
select: bookingsSelect,
})
: [];

const [resultOne, resultTwo, resultThree] = await Promise.all([
Expand Down Expand Up @@ -1659,6 +1659,8 @@ export class BookingRepository {
teamId: true,
parentId: true,
slug: true,
title: true,
length: true,
hideOrganizerEmail: true,
customReplyToEmail: true,
bookingFields: true,
Expand All @@ -1683,6 +1685,7 @@ export class BookingRepository {
workflowReminders: true,
responses: true,
iCalUID: true,
iCalSequence: true,
},
});
}
Expand Down Expand Up @@ -1913,7 +1916,13 @@ export class BookingRepository {
});
}

async updateRecordedStatus({ bookingUid, isRecorded }: { bookingUid: string; isRecorded: boolean }): Promise<void> {
async updateRecordedStatus({
bookingUid,
isRecorded,
}: {
bookingUid: string;
isRecorded: boolean;
}): Promise<void> {
await this.prismaClient.booking.update({
where: { uid: bookingUid },
data: { isRecorded },
Expand Down
3 changes: 3 additions & 0 deletions packages/features/webhooks/lib/dto/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -582,6 +584,7 @@ export type EventPayloadType = CalendarEvent &
rescheduledBy?: string;
cancelledBy?: string;
paymentData?: Record<string, unknown>;
requestReschedule?: boolean;
};

// dto/types.ts
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export class BookingPayloadBuilder extends BaseBookingPayloadBuilder {
extra: {
cancelledBy: dto.cancelledBy,
cancellationReason: dto.cancellationReason,
requestReschedule: dto.requestReschedule ?? false,
},
});

Expand Down
1 change: 1 addition & 0 deletions packages/features/webhooks/lib/sendPayload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export type EventPayloadType = CalendarEvent &
rescheduledBy?: string;
cancelledBy?: string;
paymentData?: PaymentData;
requestReschedule?: boolean;
};

export type WebhookPayloadType =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions packages/features/webhooks/lib/types/params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export interface BookingCancelledParams {
eventTypeId: number | null;
userId: number | null;
smsReminderNumber?: string | null;
iCalSequence?: number | null;
};
eventType: {
id: number;
Expand All @@ -57,6 +58,7 @@ export interface BookingCancelledParams {
};
cancelledBy?: string;
cancellationReason?: string;
requestReschedule?: boolean;
teamId?: number | null;
orgId?: number | null;
platformClientId?: string;
Expand Down
11 changes: 11 additions & 0 deletions packages/lib/server/service/BookingWebhookFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@ describe("BookingWebhookFactory", () => {
"description",
"customInputs",
"responses",
"eventTitle",
"eventTypeId",
"length",
"requestReschedule",
"iCalSequence",
"userFieldsResponses",
"startTime",
"endTime",
Expand All @@ -104,6 +109,7 @@ describe("BookingWebhookFactory", () => {
"smsReminderNumber",
"cancellationReason",
"cancelledBy",
"status",
];
const actualKeys = Object.keys(payload).sort();
expect(actualKeys).toEqual(expectedKeys.sort());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<EventType> = {};
if (bookingToReschedule.eventType) {
event = bookingToReschedule.eventType;
}
const event: Partial<EventType> = bookingToReschedule.eventType ?? {};
await bookingRepository.updateBookingStatus({
bookingId: bookingToReschedule.id,
status: BookingStatus.CANCELLED,
Expand All @@ -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 || "",
Expand Down Expand Up @@ -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
Expand Down
Loading