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:
parent
0e9d754f64
commit
aa166190e9
|
@ -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"
|
||||
|
|
|
@ -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) }),
|
||||
});
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue
Block a user