init (#26782)
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -164,6 +164,7 @@ async function cancelAttendeeSeat(
|
||||
...eventTypeInfo,
|
||||
status: "CANCELLED",
|
||||
smsReminderNumber: bookingToDelete.smsReminderNumber || undefined,
|
||||
requestReschedule: false,
|
||||
};
|
||||
|
||||
const promises = webhooks.map((webhook) =>
|
||||
|
||||
@@ -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<void> {
|
||||
async updateRecordedStatus({
|
||||
bookingUid,
|
||||
isRecorded,
|
||||
}: {
|
||||
bookingUid: string;
|
||||
isRecorded: boolean;
|
||||
}): Promise<void> {
|
||||
await this.prismaClient.booking.update({
|
||||
where: { uid: bookingUid },
|
||||
data: { isRecorded },
|
||||
|
||||
@@ -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<string, unknown>;
|
||||
requestReschedule?: boolean;
|
||||
};
|
||||
|
||||
// dto/types.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]: {
|
||||
|
||||
@@ -45,6 +45,7 @@ export class BookingPayloadBuilder extends BaseBookingPayloadBuilder {
|
||||
extra: {
|
||||
cancelledBy: dto.cancelledBy,
|
||||
cancellationReason: dto.cancellationReason,
|
||||
requestReschedule: dto.requestReschedule ?? false,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -96,6 +96,7 @@ export type EventPayloadType = CalendarEvent &
|
||||
rescheduledBy?: string;
|
||||
cancelledBy?: string;
|
||||
paymentData?: PaymentData;
|
||||
requestReschedule?: boolean;
|
||||
};
|
||||
|
||||
export type WebhookPayloadType =
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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,
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user