Booking success query refactor (#5298)

* Booking succes query refactor

The query is now using the uid as its main identifier for the success page

* Minor changes to the succes.tsx and tests

* Convert eventtype dates to string, and only select eventtype slug from db to have a smaller query (we don't need more data, and this way we don't need to convert the dates in here to smaller strings either.)

* In the payment component get the bookingUid from props instead of the query

* Changed the recurringMutation to use the uid for the success booking page

Co-authored-by: alannnc <alannnc@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: Jeroen Reumkens <hello@jeroenreumkens.nl>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: zomars <zomars@me.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
pull/5514/head
mischarouleaux 2022-11-15 20:00:02 +01:00 committed by GitHub
parent d66f3d1dc9
commit 6af0428a18
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 94 additions and 145 deletions

View File

@ -185,26 +185,13 @@ function BookingListItem(booking: BookingItemProps) {
.concat(booking.recurringInfo?.bookings[BookingStatus.PENDING])
.sort((date1: Date, date2: Date) => date1.getTime() - date2.getTime());
const location = booking.location || "";
const onClickTableData = () => {
router.push({
pathname: "/success",
query: {
date: booking.startTime,
// TODO: Booking when fetched should have id 0 already(for Dynamic Events).
type: booking.eventType.id || 0,
eventSlug: booking.eventType.slug,
username: user?.username || "",
name: booking.attendees[0] ? booking.attendees[0].name : undefined,
email: booking.attendees[0] ? booking.attendees[0].email : undefined,
location: location,
eventName: booking.eventType.eventName || "",
bookingId: booking.id,
recur: booking.recurringEventId,
reschedule: isConfirmed,
uid: booking.uid,
listingStatus: booking.listingStatus,
status: booking.status,
email: booking.attendees[0] ? booking.attendees[0].email : undefined,
},
});
};

View File

@ -122,7 +122,7 @@ const BookingPage = ({
const mutation = useMutation(createBooking, {
onSuccess: async (responseData) => {
const { id, paymentUid } = responseData;
const { uid, paymentUid } = responseData;
if (paymentUid) {
return await router.push(
createPaymentLink({
@ -138,17 +138,10 @@ const BookingPage = ({
return router.push({
pathname: "/success",
query: {
date,
type: eventType.id,
eventSlug: eventType.slug,
username: profile.slug,
reschedule: !!rescheduleUid,
name: bookingForm.getValues("name"),
email: bookingForm.getValues("email"),
location: responseData.location,
eventName: profile.eventName || "",
bookingId: id,
uid,
isSuccessBookingPage: true,
email: bookingForm.getValues("email"),
eventTypeSlug: eventType.slug,
},
});
},
@ -156,31 +149,14 @@ const BookingPage = ({
const recurringMutation = useMutation(createRecurringBooking, {
onSuccess: async (responseData = []) => {
const { attendees = [], id, recurringEventId } = responseData[0] || {};
const location = (function humanReadableLocation(location) {
if (!location) {
return;
}
if (location.includes("integration")) {
return t("web_conferencing_details_to_follow");
}
return location;
})(responseData[0].location);
const { uid } = responseData[0] || {};
return router.push({
pathname: "/success",
query: {
date,
type: eventType.id,
eventSlug: eventType.slug,
recur: recurringEventId,
username: profile.slug,
reschedule: !!rescheduleUid,
name: attendees[0].name,
email: attendees[0].email,
location,
eventName: profile.eventName || "",
bookingId: id,
uid,
email: bookingForm.getValues("email"),
eventTypeSlug: eventType.slug,
},
});
},

View File

@ -1,4 +1,4 @@
import { Prisma } from "@prisma/client";
import { BookingStatus } from "@prisma/client";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible";
import classNames from "classnames";
import { createEvent } from "ics";
@ -33,6 +33,7 @@ import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calco
import { getIs24hClockFromLocalStorage, isBrowserLocale24h } from "@calcom/lib/timeFormat";
import { localStorage } from "@calcom/lib/webstorage";
import prisma, { baseUserSelect } from "@calcom/prisma";
import { Prisma } from "@calcom/prisma/client";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import Button from "@calcom/ui/Button";
import { Icon } from "@calcom/ui/Icon";
@ -144,28 +145,26 @@ type SuccessProps = inferSSRProps<typeof getServerSideProps>;
export default function Success(props: SuccessProps) {
const { t } = useLocale();
const router = useRouter();
const {
location: _location,
name,
email,
reschedule,
listingStatus,
status,
isSuccessBookingPage,
} = router.query;
const location: ReturnType<typeof getEventLocationValue> = Array.isArray(_location)
? _location[0] || ""
: _location || "";
const { listingStatus, isSuccessBookingPage } = router.query;
const location: ReturnType<typeof getEventLocationValue> = Array.isArray(props.bookingInfo.location)
? props.bookingInfo.location[0] || ""
: props.bookingInfo.location || "";
if (!location) {
// Can't use logger.error because it throws error on client. stdout isn't available to it.
console.error(`No location found `);
}
const name = props.bookingInfo?.user?.name;
const email = props.bookingInfo?.user?.email;
const status = props.bookingInfo?.status;
const reschedule = props.bookingInfo.status === BookingStatus.ACCEPTED;
const [is24h, setIs24h] = useState(isBrowserLocale24h());
const { data: session } = useSession();
const [date, setDate] = useState(dayjs.utc(asStringOrThrow(router.query.date)));
const [date, setDate] = useState(dayjs.utc(props.bookingInfo.startTime));
const { eventType, bookingInfo } = props;
const isBackgroundTransparent = useIsBackgroundTransparent();
@ -189,7 +188,7 @@ export default function Success(props: SuccessProps) {
const giphyImage = giphyAppData?.thankYouPage;
const eventName = getEventName(eventNameObject, true);
const needsConfirmation = eventType.requiresConfirmation && reschedule != "true";
const needsConfirmation = eventType.requiresConfirmation && reschedule != true;
const isCancelled = status === "CANCELLED" || status === "REJECTED";
const telemetry = useTelemetry();
useEffect(() => {
@ -596,7 +595,7 @@ export default function Success(props: SuccessProps) {
<EmailInput
name="email"
id="email"
defaultValue={router.query.email}
defaultValue={email || ""}
className="focus:border-brand border-bookinglightest dark:border-darkgray-300 mt-0 block w-full rounded-sm border-gray-300 shadow-sm focus:ring-black dark:bg-black dark:text-white sm:text-sm"
placeholder="rick.astley@cal.com"
/>
@ -740,6 +739,8 @@ const getEventTypesFromDB = async (id: number) => {
metadata: true,
seatsPerTimeSlot: true,
seatsShowAttendees: true,
periodStartDate: true,
periodEndDate: true,
},
});
@ -756,24 +757,10 @@ const getEventTypesFromDB = async (id: number) => {
};
};
const strToNumber = z.string().transform((val, ctx) => {
const parsed = parseInt(val);
if (isNaN(parsed)) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Not a number" });
return parsed;
});
const schema = z.object({
type: strToNumber,
date: z.string().optional(),
username: z.string().optional(),
reschedule: z.string().optional(),
name: z.string().optional(),
uid: z.string(),
email: z.string().optional(),
recur: z.string().optional(),
location: z.string().optional(),
eventSlug: z.string().default("15min"),
eventName: z.string().default(""),
bookingId: strToNumber,
eventTypeSlug: z.string().optional(),
});
const handleSeatsEventTypeOnBooking = (
@ -804,18 +791,60 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
const ssr = await ssrInit(context);
const parsedQuery = schema.safeParse(context.query);
if (!parsedQuery.success) return { notFound: true };
const {
type: eventTypeId,
recur: recurringEventIdQuery,
eventSlug: eventTypeSlug,
eventName: dynamicEventName,
bookingId,
username,
name,
email,
} = parsedQuery.data;
const { uid, email, eventTypeSlug } = parsedQuery.data;
const eventTypeRaw = !eventTypeId ? getDefaultEvent(eventTypeSlug) : await getEventTypesFromDB(eventTypeId);
const bookingInfo = await prisma.booking.findFirst({
where: {
uid,
},
select: {
title: true,
id: true,
uid: true,
description: true,
customInputs: true,
smsReminderNumber: true,
recurringEventId: true,
startTime: true,
location: true,
status: true,
user: {
select: {
id: true,
name: true,
email: true,
username: true,
},
},
attendees: {
select: {
name: true,
email: true,
},
},
eventTypeId: true,
eventType: {
select: {
eventName: true,
slug: true,
},
},
},
});
if (!bookingInfo) {
return {
notFound: true,
};
}
// @NOTE: had to do this because Server side cant return [Object objects]
// probably fixable with json.stringify -> json.parse
bookingInfo["startTime"] = (bookingInfo?.startTime as Date)?.toISOString() as unknown as Date;
const eventTypeRaw = !bookingInfo.eventTypeId
? getDefaultEvent(eventTypeSlug || "")
: await getEventTypesFromDB(bookingInfo.eventTypeId);
if (!eventTypeRaw) {
return {
notFound: true,
@ -843,6 +872,8 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
const eventType = {
...eventTypeRaw,
periodStartDate: eventTypeRaw.periodStartDate?.toString() ?? null,
periodEndDate: eventTypeRaw.periodEndDate?.toString() ?? null,
metadata: EventTypeMetaDataSchema.parse(eventTypeRaw.metadata),
recurringEvent: parseRecurringEvent(eventTypeRaw.recurringEvent),
};
@ -856,59 +887,16 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
slug: eventType.team?.slug || eventType.users[0]?.username || null,
};
const where: Prisma.BookingWhereInput = {
id: bookingId,
attendees: { some: { email, name } },
};
// Dynamic Event uses EventType from @calcom/lib/defaultEvents(a fake EventType) which doesn't have a real user/team/eventTypeId
// So, you can't look them up in DB.
if (!eventType.isDynamic) {
// A Team Event doesn't have a correct user query param as of now. It is equal to team/{eventSlug} which is not a user, so you can't look it up in DB.
if (!eventType.team) {
// username being equal to profile.slug isn't applicable for Team or Dynamic Events.
where.user = { username };
}
where.eventTypeId = eventType.id;
} else {
// username being equal to eventSlug for Dynamic Event Booking, it can't be used for user lookup. So, just use eventTypeId which would always be null for Dynamic Event Bookings
where.eventTypeId = null;
}
const bookingInfo = await prisma.booking.findFirst({
where,
select: {
title: true,
id: true,
uid: true,
description: true,
customInputs: true,
smsReminderNumber: true,
user: {
select: {
id: true,
name: true,
email: true,
},
},
attendees: {
select: {
name: true,
email: true,
},
},
},
});
if (bookingInfo !== null && email) {
handleSeatsEventTypeOnBooking(eventType, bookingInfo, email);
}
let recurringBookings = null;
if (recurringEventIdQuery) {
if (bookingInfo.recurringEventId) {
// We need to get the dates for the bookings to be able to show them in the UI
recurringBookings = await prisma.booking.findMany({
where: {
recurringEventId: recurringEventIdQuery,
recurringEventId: bookingInfo.recurringEventId,
},
select: {
startTime: true,
@ -923,7 +911,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
eventType,
recurringBookings: recurringBookings ? recurringBookings.map((obj) => obj.startTime.toString()) : null,
trpcState: ssr.dehydrate(),
dynamicEventName,
dynamicEventName: bookingInfo?.eventType?.eventName || "",
bookingInfo,
},
};

View File

@ -92,7 +92,7 @@ test.describe("pro user", () => {
await page.locator('[data-testid="confirm-reschedule-button"]').click();
await page.waitForNavigation({
url(url) {
return url.pathname === "/success" && url.searchParams.get("reschedule") === "true";
return url.pathname === "/success";
},
});
});

View File

@ -37,7 +37,7 @@ test("dynamic booking", async ({ page, users }) => {
await page.locator('[data-testid="confirm-reschedule-button"]').click();
await page.waitForNavigation({
url(url) {
return url.pathname === "/success" && url.searchParams.get("reschedule") === "true";
return url.pathname === "/success";
},
});
await expect(page.locator("[data-testid=success-page]")).toBeVisible();

View File

@ -510,6 +510,7 @@ async function handler(req: NextApiRequest & { userId?: number | undefined }) {
uid: reqBody.bookingUid,
},
select: {
uid: true,
id: true,
attendees: true,
userId: true,

View File

@ -36,6 +36,7 @@ type Props = {
user: { username: string | null };
location?: string | null;
bookingId: number;
bookingUid: string;
};
type States =
@ -47,7 +48,6 @@ type States =
export default function PaymentComponent(props: Props) {
const { t, i18n } = useLocale();
const router = useRouter();
const { email, name, date } = router.query;
const [state, setState] = useState<States>({ status: "idle" });
const stripe = useStripe();
const elements = useElements();
@ -83,12 +83,7 @@ export default function PaymentComponent(props: Props) {
});
} else {
const params: { [k: string]: any } = {
date,
type: props.eventType.id,
username: props.user.username,
email,
name,
bookingId: props.bookingId,
uid: props.bookingUid,
};
if (props.location) {

View File

@ -136,6 +136,7 @@ const PaymentPage: FC<PaymentPageProps> = (props) => {
user={props.user}
location={props.booking.location}
bookingId={props.booking.id}
bookingUid={props.booking.uid}
/>
</Elements>
)}

View File

@ -31,6 +31,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
booking: {
select: {
id: true,
uid: true,
description: true,
title: true,
startTime: true,