feat: Add rescheduledBy & canceledBy fields in the DB (#15337)

* feat: Add rescheduledBy & canceledBy fields in the DB

* fix: type check

* fix: type check

* fix: use session user email for reschedule

* fix: unit test

* feat: db field email validation

* feat: rescheduledBy and cancelledBy in webhooks

* revert unrelated changes.

* make session user secondary, default to Anonymous

* if condition not required

* Make cancelledBy optional

Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>

* update cancel booking type

* fix: update cancelledBy in db when requesting reschedule

* remove default value for fields

* fix: type check

* feat: manage fields via api v1

* fix: add fields in booking read api v1

* test: expand to cover new fields

* fix: use cancelledBy param on booking page

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
This commit is contained in:
Amit Sharma
2024-08-25 11:31:46 +02:00
committed by GitHub
co-authored by Syed Ali Shahbaz Anik Dhabal Babu Peer Richelsen
parent 34075ee303
commit b6d31129a3
28 changed files with 139 additions and 20 deletions
+6
View File
@@ -14,6 +14,8 @@ const schemaBookingBaseBodyParams = Booking.pick({
startTime: true,
endTime: true,
status: true,
rescheduledBy: true,
cancelledBy: true,
}).partial();
export const schemaBookingCreateBodyParams = extendedBookingCreateBody.merge(schemaQueryUserId.partial());
@@ -33,6 +35,8 @@ const schemaBookingEditParams = z
title: z.string().optional(),
startTime: iso8601.optional(),
endTime: iso8601.optional(),
cancelledBy: z.string().email({ message: "Invalid Email" }).optional(),
rescheduledBy: z.string().email({ message: "Invalid Email" }).optional(),
// Not supporting responses in edit as that might require re-triggering emails
// responses
})
@@ -88,4 +92,6 @@ export const schemaBookingReadPublic = Booking.extend({
status: true,
responses: true,
fromReschedule: true,
cancelledBy: true,
rescheduledBy: true,
});
@@ -23,6 +23,7 @@ type Props = {
theme: string | null;
allRemainingBookings: boolean;
seatReferenceUid?: string;
currentUserEmail?: string;
bookingCancelledEventProps: {
booking: unknown;
organizer: {
@@ -38,7 +39,8 @@ export default function CancelBooking(props: Props) {
const [cancellationReason, setCancellationReason] = useState<string>("");
const { t } = useLocale();
const router = useRouter();
const { booking, allRemainingBookings, seatReferenceUid, bookingCancelledEventProps } = props;
const { booking, allRemainingBookings, seatReferenceUid, bookingCancelledEventProps, currentUserEmail } =
props;
const [loading, setLoading] = useState(false);
const telemetry = useTelemetry();
const [error, setError] = useState<string | null>(booking ? null : t("booking_already_cancelled"));
@@ -99,6 +101,7 @@ export default function CancelBooking(props: Props) {
allRemainingBookings,
// @NOTE: very important this shouldn't cancel with number ID use uid instead
seatReferenceUid,
cancelledBy: currentUserEmail,
}),
headers: {
"Content-Type": "application/json",
@@ -163,6 +163,8 @@ export default function Success(props: PageProps) {
const [calculatedDuration, setCalculatedDuration] = useState<number | undefined>(undefined);
const [comment, setComment] = useState("");
const parsedRating = rating ? parseInt(rating, 10) : 3;
const currentUserEmail =
searchParams?.get("cancelledBy") ?? searchParams?.get("email") ?? session?.user?.email ?? undefined;
const defaultRating = isNaN(parsedRating) ? 3 : parsedRating > 5 ? 5 : parsedRating < 1 ? 1 : parsedRating;
const [rateValue, setRateValue] = useState<number>(defaultRating);
@@ -700,7 +702,9 @@ export default function Success(props: PageProps) {
<span className="text-default inline">
<span className="underline" data-testid="reschedule-link">
<Link
href={`/reschedule/${seatReferenceUid || bookingInfo?.uid}`}
href={`/reschedule/${seatReferenceUid || bookingInfo?.uid}${
currentUserEmail ? `?rescheduledBy=${currentUserEmail}` : ""
}`}
legacyBehavior>
{t("reschedule")}
</Link>
@@ -738,6 +742,7 @@ export default function Success(props: PageProps) {
allRemainingBookings={allRemainingBookings}
seatReferenceUid={seatReferenceUid}
bookingCancelledEventProps={bookingCancelledEventProps}
currentUserEmail={currentUserEmail}
/>
</>
))}
+17 -2
View File
@@ -19,8 +19,16 @@ export default function Type() {
export async function getServerSideProps(context: GetServerSidePropsContext) {
const session = await getServerSession(context);
const { uid: bookingUid, seatReferenceUid } = z
.object({ uid: z.string(), seatReferenceUid: z.string().optional() })
const {
uid: bookingUid,
seatReferenceUid,
rescheduledBy,
} = z
.object({
uid: z.string(),
seatReferenceUid: z.string().optional(),
rescheduledBy: z.string().optional(),
})
.parse(context.query);
const coepFlag = context.query["flag.coep"];
const { uid, seatReferenceUid: maybeSeatReferenceUid } = await maybeGetBookingUidFromSeat(
@@ -147,6 +155,13 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
if (coepFlag) {
destinationUrlSearchParams.set("flag.coep", coepFlag as string);
}
const currentUserEmail = rescheduledBy ?? session?.user?.email;
if (currentUserEmail) {
destinationUrlSearchParams.set("rescheduledBy", currentUserEmail);
}
return {
redirect: {
destination: `${eventUrl}?${destinationUrlSearchParams.toString()}${
+22 -6
View File
@@ -461,13 +461,19 @@ export function expectSuccessfulBookingCreationEmails({
links: recurrence
? [
{
href: `${bookingUrlOrigin}/booking/${booking.uid}?cancel=true&allRemainingBookings=true`,
href: `${bookingUrlOrigin}/booking/${
booking.uid
}?cancel=true&allRemainingBookings=true&cancelledBy=${encodeURIComponent(
destinationEmail ?? organizer.email
)}`,
text: "cancel",
},
]
: [
{
href: `${bookingUrlOrigin}/reschedule/${booking.uid}`,
href: `${bookingUrlOrigin}/reschedule/${booking.uid}?rescheduledBy=${encodeURIComponent(
destinationEmail ?? organizer.email
)}`,
text: "reschedule",
},
],
@@ -514,13 +520,17 @@ export function expectSuccessfulBookingCreationEmails({
links: recurrence
? [
{
href: `${bookingUrlOrigin}/booking/${booking.uid}?cancel=true&allRemainingBookings=true`,
href: `${bookingUrlOrigin}/booking/${
booking.uid
}?cancel=true&allRemainingBookings=true&cancelledBy=${encodeURIComponent(booker.email)}`,
text: "cancel",
},
]
: [
{
href: `${bookingUrlOrigin}/reschedule/${booking.uid}`,
href: `${bookingUrlOrigin}/reschedule/${booking.uid}?rescheduledBy=${encodeURIComponent(
booker.email
)}`,
text: "reschedule",
},
],
@@ -552,11 +562,17 @@ export function expectSuccessfulBookingCreationEmails({
},
links: [
{
href: `${bookingUrlOrigin}/reschedule/${booking.uid}`,
href: `${bookingUrlOrigin}/reschedule/${booking.uid}?rescheduledBy=${encodeURIComponent(
otherTeamMember.email
)}`,
text: "reschedule",
},
{
href: `${bookingUrlOrigin}/booking/${booking.uid}?cancel=true&allRemainingBookings=false`,
href: `${bookingUrlOrigin}/booking/${
booking.uid
}?cancel=true&allRemainingBookings=false&cancelledBy=${encodeURIComponent(
otherTeamMember.email
)}`,
text: "cancel",
},
],
@@ -26,6 +26,8 @@ export function getMockRequestDataForBooking({
bookingUid?: string;
recurringEventId?: string;
recurringCount?: number;
rescheduledBy?: string;
cancelledBy?: string;
schedulingType?: SchedulingType;
responses: {
email: string;
@@ -5,8 +5,8 @@ export function ManageLink(props: { calEvent: CalendarEvent; attendee: Person })
// Only the original attendee can make changes to the event
// Guests cannot
const t = props.attendee.language.translate;
const cancelLink = getCancelLink(props.calEvent);
const rescheduleLink = getRescheduleLink(props.calEvent);
const cancelLink = getCancelLink(props.calEvent, props.attendee);
const rescheduleLink = getRescheduleLink(props.calEvent, props.attendee);
const bookingLink = getBookingUrl(props.calEvent);
const isOriginalAttendee = props.attendee.email === props.calEvent.attendees[0]?.email;
@@ -16,9 +16,10 @@ export function ManageLink(props: { calEvent: CalendarEvent; attendee: Person })
const hasBookingLink = Boolean(bookingLink);
const isRecurringEvent = props.calEvent.recurringEvent;
const shouldDisplayRescheduleLink = Boolean(hasRescheduleLink && !isRecurringEvent);
const isTeamMember = props.calEvent.team?.members.some((member) => props.attendee.email === member.email);
if (
(isOriginalAttendee || isOrganizer) &&
(isOriginalAttendee || isOrganizer || isTeamMember) &&
(hasCancelLink || (!isRecurringEvent && hasRescheduleLink) || hasBookingLink)
) {
return (
@@ -1,3 +1,4 @@
import { SchedulingType } from "@prisma/client";
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
import { BaseScheduledEmail } from "./BaseScheduledEmail";
@@ -32,6 +33,9 @@ export const OrganizerScheduledEmail = (
const locale = props.teamMember?.language.locale || props.calEvent.organizer.language.locale;
const timeFormat = props.teamMember?.timeFormat || props.calEvent.organizer?.timeFormat;
const isTeamEvent = props.calEvent.schedulingType === SchedulingType.ROUND_ROBIN || props.calEvent.schedulingType === SchedulingType.COLLECTIVE;
const attendee = isTeamEvent && props.teamMember ? props.teamMember : props.attendee;
return (
<BaseScheduledEmail
locale={locale}
@@ -50,6 +54,7 @@ export const OrganizerScheduledEmail = (
</>
}
{...props}
attendee={attendee}
/>
);
};
+11 -2
View File
@@ -27,6 +27,7 @@ type StoreInitializeType = {
bookingData?: GetBookingType | null | undefined;
verifiedEmail?: string | null;
rescheduleUid?: string | null;
rescheduledBy?: string | null;
seatReferenceUid?: string;
durationConfig?: number[] | null;
org?: string | null;
@@ -113,11 +114,12 @@ export type BookerStore = {
dayCount: number | null;
setDayCount: (dayCount: number | null) => void;
/**
* If booking is being rescheduled or it has seats, it receives a rescheduleUid or bookingUid
* If booking is being rescheduled or it has seats, it receives a rescheduleUid with rescheduledBy or bookingUid
* the current booking details are passed in. The `bookingData`
* object is something that's fetched server side.
*/
rescheduleUid: string | null;
rescheduledBy: string | null;
bookingUid: string | null;
bookingData: GetBookingType | null;
setBookingData: (bookingData: GetBookingType | null | undefined) => void;
@@ -207,6 +209,7 @@ export const useBookerStore = create<BookerStore>((set, get) => ({
username: null,
eventSlug: null,
eventId: null,
rescheduledBy: null,
verifiedEmail: null,
setVerifiedEmail: (email: string | null) => {
set({ verifiedEmail: email });
@@ -242,6 +245,7 @@ export const useBookerStore = create<BookerStore>((set, get) => ({
month,
eventId,
rescheduleUid = null,
rescheduledBy = null,
bookingUid = null,
bookingData = null,
layout,
@@ -260,7 +264,8 @@ export const useBookerStore = create<BookerStore>((set, get) => ({
get().rescheduleUid === rescheduleUid &&
get().bookingUid === bookingUid &&
get().bookingData?.responses.email === bookingData?.responses.email &&
get().layout === layout
get().layout === layout &&
get().rescheduledBy === rescheduledBy
)
return;
set({
@@ -269,6 +274,7 @@ export const useBookerStore = create<BookerStore>((set, get) => ({
eventId,
org,
rescheduleUid,
rescheduledBy,
bookingUid,
bookingData,
layout: layout || BookerLayouts.MONTH_VIEW,
@@ -358,6 +364,7 @@ export const useInitializeBookerStore = ({
month,
eventId,
rescheduleUid = null,
rescheduledBy = null,
bookingData = null,
verifiedEmail = null,
layout,
@@ -374,6 +381,7 @@ export const useInitializeBookerStore = ({
month,
eventId,
rescheduleUid,
rescheduledBy,
bookingData,
layout,
isTeamEvent,
@@ -390,6 +398,7 @@ export const useInitializeBookerStore = ({
month,
eventId,
rescheduleUid,
rescheduledBy,
bookingData,
layout,
isTeamEvent,
@@ -90,6 +90,7 @@ export interface BookerProps {
export type WrappedBookerPropsMain = {
sessionUsername?: string | null;
rescheduleUid: string | null;
rescheduledBy: string | null;
bookingUid: string | null;
isRedirect: boolean;
fromUserNameRedirected: string;
@@ -14,6 +14,7 @@ export type BookingOptions = {
timeZone: string;
language: string;
rescheduleUid: string | undefined;
rescheduledBy: string | undefined;
username: string;
metadata?: Record<string, string>;
bookingUid?: string;
@@ -31,6 +32,7 @@ export const mapBookingToMutationInput = ({
timeZone,
language,
rescheduleUid,
rescheduledBy,
username,
metadata,
bookingUid,
@@ -52,6 +54,7 @@ export const mapBookingToMutationInput = ({
timeZone: timeZone,
language: language,
rescheduleUid,
rescheduledBy,
metadata: metadata || {},
hasHashedBookingLink: hashedLink ? true : false,
bookingUid,
@@ -155,7 +155,7 @@ export type HandleCancelBookingResponse = {
};
async function handler(req: CustomRequest) {
const { id, uid, allRemainingBookings, cancellationReason, seatReferenceUid } =
const { id, uid, allRemainingBookings, cancellationReason, seatReferenceUid, cancelledBy } =
schemaBookingCancelParams.parse(req.body);
req.bookingToDelete = await getBookingToDelete(id, uid);
const {
@@ -342,6 +342,7 @@ async function handler(req: CustomRequest) {
...eventTypeInfo,
status: "CANCELLED",
smsReminderNumber: bookingToDelete.smsReminderNumber || undefined,
cancelledBy: cancelledBy,
}).catch((e) => {
logger.error(
`Error executing webhook for event: ${eventTrigger}, URL: ${webhook.subscriberUrl}, bookingId: ${evt.bookingId}, bookingUid: ${evt.uid}`,
@@ -398,6 +399,7 @@ async function handler(req: CustomRequest) {
data: {
status: BookingStatus.CANCELLED,
cancellationReason: cancellationReason,
cancelledBy: cancelledBy,
},
});
const allUpdatedBookings = await prisma.booking.findMany({
@@ -440,6 +442,7 @@ async function handler(req: CustomRequest) {
data: {
status: BookingStatus.CANCELLED,
cancellationReason: cancellationReason,
cancelledBy: cancelledBy,
// Assume that canceling the booking is the last action
iCalSequence: evt.iCalSequence || 100,
},
@@ -110,6 +110,7 @@ describe("Cancel Booking", () => {
body: {
id: idOfBookingToBeCancelled,
uid: uidOfBookingToBeCancelled,
cancelledBy: organizer.email,
},
});
@@ -121,6 +122,7 @@ describe("Cancel Booking", () => {
location: BookingLocations.CalVideo,
subscriberUrl: "http://my-webhook.example.com",
payload: {
cancelledBy: organizer.email,
organizer: {
id: organizer.id,
username: organizer.username,
@@ -996,6 +996,7 @@ async function handler(
eventTrigger,
responses,
workflows,
rescheduledBy: reqBody.rescheduledBy,
});
if (newBooking) {
@@ -1066,6 +1067,7 @@ async function handler(
data: {
rescheduled: true,
status: BookingStatus.CANCELLED,
rescheduledBy: reqBody.rescheduledBy,
},
});
}
@@ -1561,6 +1563,7 @@ async function handler(
eventTypeId,
status: "ACCEPTED",
smsReminderNumber: booking?.smsReminderNumber || undefined,
rescheduledBy: reqBody.rescheduledBy,
};
if (bookingRequiresPayment) {
@@ -173,6 +173,7 @@ describe("handleNewBooking", () => {
name: booker.name,
location: { optionValue: "", value: BookingLocations.CalVideo },
},
rescheduledBy: organizer.email,
},
});
@@ -199,6 +200,7 @@ describe("handleNewBooking", () => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
uid: uidOfBookingToBeRescheduled,
status: BookingStatus.CANCELLED,
rescheduledBy: organizer.email,
});
expect(previousBooking?.status).toBe(BookingStatus.CANCELLED);
@@ -284,6 +286,9 @@ describe("handleNewBooking", () => {
location: BookingLocations.CalVideo,
subscriberUrl: "http://my-webhook.example.com",
videoCallUrl: `${WEBAPP_URL}/video/${createdBooking.uid}`,
payload: {
rescheduledBy: organizer.email,
},
});
},
timeout
@@ -1957,6 +1962,7 @@ describe("handleNewBooking", () => {
name: booker.name,
location: { optionValue: "", value: BookingLocations.CalVideo },
},
rescheduledBy: booker.email,
},
});
const { req } = createMockNextJsRequest({
@@ -1982,6 +1988,7 @@ describe("handleNewBooking", () => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
uid: uidOfBookingToBeRescheduled,
status: BookingStatus.CANCELLED,
rescheduledBy: booker.email,
});
expect(previousBooking?.status).toBe(BookingStatus.CANCELLED);
@@ -30,6 +30,7 @@ const handleSeats = async (newSeatedBookingObject: NewSeatedBookingObject) => {
eventTrigger,
evt,
workflows,
rescheduledBy,
} = newSeatedBookingObject;
const loggerWithEventDetails = createLoggerWithEventDetails(eventType.id, reqBodyUser, eventType.slug);
@@ -141,6 +142,7 @@ const handleSeats = async (newSeatedBookingObject: NewSeatedBookingObject) => {
eventTypeId,
status: "ACCEPTED",
smsReminderNumber: seatedBooking?.smsReminderNumber || undefined,
rescheduledBy,
};
await handleWebhookTrigger({ subscriberOptions, eventTrigger, webhookData });
@@ -2671,6 +2671,7 @@ describe("handleSeats", () => {
location: { optionValue: "", value: BookingLocations.CalVideo },
},
rescheduleUid: firstBookingUid,
cancelledBy: organizer.email,
},
});
@@ -2690,10 +2691,12 @@ describe("handleSeats", () => {
},
select: {
status: true,
cancelledBy: true,
},
});
expect(cancelledBooking?.status).toEqual(BookingStatus.CANCELLED);
expect(cancelledBooking?.cancelledBy).toEqual(organizer.email);
});
});
});
+1
View File
@@ -38,6 +38,7 @@ export type NewSeatedBookingObject = {
subscriberOptions: GetSubscriberOptions;
eventTrigger: WebhookTriggerEvents;
responses: z.infer<ReturnType<typeof getBookingDataSchema>>["responses"] | null;
rescheduledBy?: string;
workflows: Workflow[];
};
@@ -60,6 +60,8 @@ export type WebhookDataType = CalendarEvent &
createdAt: string;
downloadLink?: string;
paymentId?: number;
rescheduledBy?: string;
cancelledBy?: string;
paymentData?: Payment;
};
+13 -4
View File
@@ -2,7 +2,7 @@ import type { TFunction } from "next-i18next";
import short from "short-uuid";
import { v5 as uuidv5 } from "uuid";
import type { CalendarEvent } from "@calcom/types/Calendar";
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
import { WEBAPP_URL } from "./constants";
import getLabelValueMapFromResponses from "./getLabelValueMapFromResponses";
@@ -217,7 +217,7 @@ export const getPlatformCancelLink = (
return "";
};
export const getCancelLink = (calEvent: CalendarEvent): string => {
export const getCancelLink = (calEvent: CalendarEvent, attendee?: Person): string => {
const Uid = getUid(calEvent);
const seatReferenceUid = getSeatReferenceId(calEvent);
if (calEvent.platformClientId) {
@@ -227,6 +227,9 @@ export const getCancelLink = (calEvent: CalendarEvent): string => {
const cancelLink = new URL(`${calEvent.bookerUrl ?? WEBAPP_URL}/booking/${Uid}`);
cancelLink.searchParams.append("cancel", "true");
cancelLink.searchParams.append("allRemainingBookings", String(!!calEvent.recurringEvent));
if (attendee?.email) {
cancelLink.searchParams.append("cancelledBy", attendee.email);
}
if (seatReferenceUid) cancelLink.searchParams.append("seatReferenceUid", seatReferenceUid);
return cancelLink.toString();
};
@@ -249,7 +252,7 @@ export const getPlatformRescheduleLink = (
return "";
};
export const getRescheduleLink = (calEvent: CalendarEvent): string => {
export const getRescheduleLink = (calEvent: CalendarEvent, attendee?: Person): string => {
const Uid = getUid(calEvent);
const seatUid = getSeatReferenceId(calEvent);
@@ -257,7 +260,13 @@ export const getRescheduleLink = (calEvent: CalendarEvent): string => {
return getPlatformRescheduleLink(calEvent, Uid, seatUid);
}
return `${calEvent.bookerUrl ?? WEBAPP_URL}/reschedule/${seatUid ? seatUid : Uid}`;
const rescheduleLink = new URL(`${calEvent.bookerUrl ?? WEBAPP_URL}/reschedule/${seatUid ? seatUid : Uid}`);
if (attendee?.email) {
rescheduleLink.searchParams.append("rescheduledBy", attendee.email);
}
return rescheduleLink.toString();
};
export const getRichDescription = (
+2
View File
@@ -53,6 +53,8 @@ export const buildBooking = (
status: BookingStatus.ACCEPTED,
paid: false,
destinationCalendarId: null,
cancelledBy: null,
rescheduledBy: null,
cancellationReason: null,
rejectionReason: null,
dynamicEventSlugRef: null,
@@ -47,6 +47,7 @@ import { AtomsWrapper } from "../src/components/atoms-wrapper";
export type BookerPlatformWrapperAtomProps = Omit<BookerProps, "username" | "entity"> & {
rescheduleUid?: string;
rescheduledBy?: string;
bookingUid?: string;
username: string | string[] | undefined;
entity?: BookerProps["entity"];
@@ -357,6 +358,7 @@ export const BookerPlatformWrapper = (props: BookerPlatformWrapperAtomProps) =>
}
}
rescheduleUid={props.rescheduleUid ?? null}
rescheduledBy={null}
bookingUid={props.bookingUid ?? null}
isRedirect={false}
fromUserNameRedirected=""
@@ -39,6 +39,8 @@ export const BookerWebWrapper = (props: BookerWebWrapperAtomProps) => {
const fromUserNameRedirected = searchParams?.get("username") || "";
const rescheduleUid =
typeof window !== "undefined" ? new URLSearchParams(window.location.search).get("rescheduleUid") : null;
const rescheduledBy =
typeof window !== "undefined" ? new URLSearchParams(window.location.search).get("rescheduledBy") : null;
const bookingUid =
typeof window !== "undefined" ? new URLSearchParams(window.location.search).get("bookingUid") : null;
const date = dayjs(selectedDate).format("YYYY-MM-DD");
@@ -52,6 +54,7 @@ export const BookerWebWrapper = (props: BookerWebWrapperAtomProps) => {
...props,
eventId: event?.data?.id,
rescheduleUid,
rescheduledBy,
bookingUid: bookingUid,
layout: bookerLayout.defaultLayout,
org: props.entity.orgSlug,
@@ -196,6 +199,7 @@ export const BookerWebWrapper = (props: BookerWebWrapperAtomProps) => {
isRedirect={isRedirect}
fromUserNameRedirected={fromUserNameRedirected}
rescheduleUid={rescheduleUid}
rescheduledBy={rescheduledBy}
bookingUid={bookingUid}
hasSession={hasSession}
extraOptions={routerQuery}
@@ -44,6 +44,7 @@ export const useHandleBookEvent = ({
const duration = useBookerStore((state) => state.selectedDuration);
const { timezone } = useTimePreferences();
const rescheduleUid = useBookerStore((state) => state.rescheduleUid);
const rescheduledBy = useBookerStore((state) => state.rescheduledBy);
const { t, i18n } = useLocale();
const username = useBookerStore((state) => state.username);
const recurringEventCount = useBookerStore((state) => state.recurringEventCount);
@@ -82,6 +83,7 @@ export const useHandleBookEvent = ({
timeZone: timezone,
language: i18n.language,
rescheduleUid: rescheduleUid || undefined,
rescheduledBy: rescheduledBy || undefined,
bookingUid: (bookingData && bookingData.uid) || seatedEventData?.bookingUid || undefined,
username: username || "",
metadata: metadata,
@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "Booking" ADD COLUMN "cancelledBy" TEXT,
ADD COLUMN "rescheduledBy" TEXT;
+4
View File
@@ -596,6 +596,10 @@ model Booking {
ratingFeedback String?
noShowHost Boolean? @default(false)
scheduledTriggers WebhookScheduledTriggers[]
/// @zod.email()
cancelledBy String?
/// @zod.email()
rescheduledBy String?
@@index([eventTypeId])
@@index([userId])
+2
View File
@@ -235,6 +235,7 @@ export const bookingCreateBodySchema = z.object({
eventTypeSlug: z.string().optional(),
rescheduleUid: z.string().optional(),
recurringEventId: z.string().optional(),
rescheduledBy: z.string().email({ message: "Invalid email" }).optional(),
start: z.string(),
timeZone: z.string().refine((value: string) => isSupportedTimeZone(value), { message: "Invalid timezone" }),
user: z.union([z.string(), z.array(z.string())]).optional(),
@@ -319,6 +320,7 @@ export const schemaBookingCancelParams = z.object({
allRemainingBookings: z.boolean().optional(),
cancellationReason: z.string().optional(),
seatReferenceUid: z.string().optional(),
cancelledBy: z.string().email({ message: "Invalid email" }).optional(),
});
export const vitalSettingsUpdateSchema = z.object({
@@ -144,6 +144,7 @@ export const requestRescheduleHandler = async ({ ctx, input }: RequestReschedule
cancellationReason,
status: BookingStatus.CANCELLED,
updatedAt: dayjs().toISOString(),
cancelledBy: user.email,
},
});
@@ -302,6 +303,7 @@ export const requestRescheduleHandler = async ({ ctx, input }: RequestReschedule
sendPayload(webhook.secret, eventTrigger, new Date().toISOString(), webhook, {
...evt,
smsReminderNumber: bookingToReschedule.smsReminderNumber || undefined,
cancelledBy: user.email,
}).catch((e) => {
log.error(
`Error executing webhook for event: ${eventTrigger}, URL: ${webhook.subscriberUrl}, bookingId: ${evt.bookingId}, bookingUid: ${evt.uid}`,