Hotfix: Cancelling recurring events follow-up (#3454)

* Other fixes and applying feedback

* Adds defaultResponder to handle zod errors

Co-authored-by: zomars <zomars@me.com>
This commit is contained in:
Leo Giovanetti 2022-07-19 20:29:52 -03:00 committed by GitHub
parent 0e9d754f64
commit aa166190e9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 45 additions and 28 deletions

View File

@ -126,6 +126,8 @@ function BookingListItem(booking: BookingItemProps) {
booking.listingStatus === "recurring" && booking.recurringEventId !== null
? t("cancel_all_remaining")
: t("cancel"),
/* When cancelling we need to let the UI and the API know if the intention is to
cancel all remaining bookings or just that booking instance. */
href: `/cancel/${booking.uid}${
booking.listingStatus === "recurring" && booking.recurringEventId !== null
? "?allRemainingBookings=true"

View File

@ -1,13 +1,14 @@
import {
BookingStatus,
Credential,
WebhookTriggerEvents,
Prisma,
PrismaPromise,
WebhookTriggerEvents,
WorkflowMethods,
} from "@prisma/client";
import async from "async";
import { NextApiRequest, NextApiResponse } from "next";
import z from "zod";
import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
import { FAKE_DAILY_CREDENTIAL } from "@calcom/app-store/dailyvideo/lib/VideoApiAdapter";
@ -15,6 +16,8 @@ import { deleteMeeting } from "@calcom/core/videoClient";
import dayjs from "@calcom/dayjs";
import { sendCancelledEmails } from "@calcom/emails";
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
import { HttpError } from "@calcom/lib/http-error";
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
import type { CalendarEvent } from "@calcom/types/Calendar";
import { refund } from "@ee/lib/stripe/server";
@ -22,23 +25,21 @@ import { deleteScheduledEmailReminder } from "@ee/lib/workflows/reminders/emailR
import { sendCancelledReminders } from "@ee/lib/workflows/reminders/reminderScheduler";
import { deleteScheduledSMSReminder } from "@ee/lib/workflows/reminders/smsReminderManager";
import { asStringOrNull } from "@lib/asStringOrNull";
import { getSession } from "@lib/auth";
import sendPayload from "@lib/webhooks/sendPayload";
import getWebhooks from "@lib/webhooks/subscriptions";
import { getTranslation } from "@server/lib/i18n";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
// just bail if it not a DELETE
if (req.method !== "DELETE" && req.method !== "POST") {
return res.status(405).end();
}
const bodySchema = z.object({
uid: z.string(),
allRemainingBookings: z.boolean().optional(),
cancellationReason: z.string().optional(),
});
const uid = asStringOrNull(req.body.uid) || "";
const allRemainingBookings = asStringOrNull(req.body.allRemainingBookings) || "";
const cancellationReason = asStringOrNull(req.body.reason) || "";
const session = await getSession({ req: req });
async function handler(req: NextApiRequest, res: NextApiResponse) {
const { uid, allRemainingBookings, cancellationReason } = bodySchema.parse(req.body);
const session = await getSession({ req });
const bookingToDelete = await prisma.booking.findUnique({
where: {
@ -93,15 +94,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
});
if (!bookingToDelete || !bookingToDelete.user) {
return res.status(404).end();
throw new HttpError({ statusCode: 404, message: "Booking not found" });
}
if ((!session || session.user?.id !== bookingToDelete.user?.id) && bookingToDelete.startTime < new Date()) {
return res.status(403).json({ message: "Cannot cancel past events" });
throw new HttpError({ statusCode: 403, message: "Cannot cancel past events" });
}
if (!bookingToDelete.userId) {
return res.status(404).json({ message: "User not found" });
throw new HttpError({ statusCode: 404, message: "User not found" });
}
const organizer = await prisma.user.findFirst({
@ -148,10 +149,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
attendees: attendeesList,
uid: bookingToDelete?.uid,
/* Include recurringEvent information only when cancelling all bookings */
recurringEvent:
allRemainingBookings === "true"
? parseRecurringEvent(bookingToDelete.eventType?.recurringEvent)
: undefined,
recurringEvent: allRemainingBookings
? parseRecurringEvent(bookingToDelete.eventType?.recurringEvent)
: undefined,
location: bookingToDelete?.location,
destinationCalendar: bookingToDelete?.destinationCalendar || bookingToDelete?.user.destinationCalendar,
cancellationReason: cancellationReason,
@ -174,13 +174,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
// by cancelling first, and blocking whilst doing so; we can ensure a cancel
// action always succeeds even if subsequent integrations fail cancellation.
if (bookingToDelete.eventType?.recurringEvent && allRemainingBookings === "true") {
if (bookingToDelete.eventType?.recurringEvent && bookingToDelete.recurringEventId && allRemainingBookings) {
const recurringEventId = bookingToDelete.recurringEventId;
const where = recurringEventId === null ? { uid } : { recurringEventId };
// Proceed to mark as cancelled all remaining recurring events instances (greater than or equal to right now)
await prisma.booking.updateMany({
where: {
...where,
recurringEventId,
startTime: {
gte: new Date(),
},
@ -341,3 +340,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
res.status(204).end();
}
export default defaultHandler({
DELETE: Promise.resolve({ default: defaultResponder(handler) }),
POST: Promise.resolve({ default: defaultResponder(handler) }),
});

View File

@ -3,6 +3,7 @@ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/r
import { GetServerSidePropsContext } from "next";
import { useRouter } from "next/router";
import { useState } from "react";
import z from "zod";
import dayjs from "@calcom/dayjs";
import classNames from "@calcom/lib/classNames";
@ -13,7 +14,6 @@ import prisma, { bookingMinimalSelect } from "@calcom/prisma";
import { Button } from "@calcom/ui/Button";
import { TextField } from "@calcom/ui/form/fields";
import { asStringOrNull, asStringOrUndefined } from "@lib/asStringOrNull";
import { getSession } from "@lib/auth";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
import { detectBrowserTimeFormat } from "@lib/timeFormat";
@ -24,11 +24,19 @@ import { HeadSeo } from "@components/seo/head-seo";
import { ssrInit } from "@server/lib/ssr";
const querySchema = z.object({
uid: z.string(),
allRemainingBookings: z
.string()
.optional()
.transform((val) => (val ? JSON.parse(val) : false)),
});
export default function Type(props: inferSSRProps<typeof getServerSideProps>) {
const { t } = useLocale();
// Get router variables
const router = useRouter();
const { uid, allRemainingBookings } = router.query;
const { uid, allRemainingBookings } = querySchema.parse(router.query);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(props.booking ? null : t("booking_already_cancelled"));
const [cancellationReason, setCancellationReason] = useState<string>("");
@ -85,7 +93,7 @@ export default function Type(props: inferSSRProps<typeof getServerSideProps>) {
? props.cancellationAllowed
? t("reschedule_instead")
: t("event_is_in_the_past")
: allRemainingBookings === "true"
: allRemainingBookings
? t("cancelling_all_recurring")
: t("cancelling_event_recurring")}
</p>
@ -229,10 +237,10 @@ export default function Type(props: inferSSRProps<typeof getServerSideProps>) {
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
const session = await getSession(context);
const allRemainingBookings = asStringOrNull(context.query.allRemainingBookings) || "";
const { allRemainingBookings, uid } = querySchema.parse(context.query);
const booking = await prisma.booking.findUnique({
where: {
uid: asStringOrUndefined(context.query.uid),
uid,
},
select: {
...bookingMinimalSelect,
@ -278,10 +286,13 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
});
let recurringInstances = null;
if (booking.eventType?.recurringEvent && allRemainingBookings === "true") {
if (booking.eventType?.recurringEvent && allRemainingBookings) {
recurringInstances = await prisma.booking.findMany({
where: {
recurringEventId: booking.recurringEventId,
startTime: {
gte: new Date(),
},
NOT: [{ status: "CANCELLED" }, { status: "REJECTED" }],
},
select: {

View File

@ -64,7 +64,7 @@ export default function CancelSuccess() {
{!loading && session?.user && (
<Button
data-testid="back-to-bookings"
href={isRecurringEvent ? "/bookings/recurring" : "/bookings"}
href={isRecurringEvent ? "/bookings/recurring" : "/bookings/upcoming"}
StartIcon={ArrowLeftIcon}>
{t("back_to_bookings")}
</Button>