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:
co-authored by
Syed Ali Shahbaz
Anik Dhabal Babu
Peer Richelsen
parent
34075ee303
commit
b6d31129a3
@@ -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}
|
||||
/>
|
||||
</>
|
||||
))}
|
||||
|
||||
@@ -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()}${
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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,
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Booking" ADD COLUMN "cancelledBy" TEXT,
|
||||
ADD COLUMN "rescheduledBy" TEXT;
|
||||
@@ -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])
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
Reference in New Issue
Block a user