diff --git a/apps/web/pages/booking/[uid].tsx b/apps/web/pages/booking/[uid].tsx index e58b853aa5..4c0f2cfa45 100644 --- a/apps/web/pages/booking/[uid].tsx +++ b/apps/web/pages/booking/[uid].tsx @@ -208,7 +208,7 @@ export default function Success(props: SuccessProps) { } const status = props.bookingInfo?.status; const reschedule = props.bookingInfo.status === BookingStatus.ACCEPTED; - const cancellationReason = props.bookingInfo.cancellationReason; + const cancellationReason = props.bookingInfo.cancellationReason || props.bookingInfo.rejectionReason; const attendeeName = typeof props?.bookingInfo?.attendees?.[0]?.name === "string" @@ -440,6 +440,17 @@ export default function Success(props: SuccessProps) {

{getTitle()}

+ {props.paymentStatus && ( +

+ {!props.paymentStatus.success && + !props.paymentStatus.refunded && + t("booking_with_payment_cancelled")} + {props.paymentStatus.success && + !props.paymentStatus.refunded && + t("booking_with_payment_cancelled_already_paid")} + {props.paymentStatus.refunded && t("booking_with_payment_cancelled_refunded")} +

+ )}
{(isCancelled || reschedule) && cancellationReason && ( @@ -1013,6 +1024,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { status: true, metadata: true, cancellationReason: true, + rejectionReason: true, user: { select: { id: true, @@ -1115,6 +1127,20 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { }); } + const payment = await prisma.payment.findFirst({ + where: { + bookingId: bookingInfo.id, + }, + select: { + success: true, + refunded: true, + }, + }); + const paymentStatus = { + success: payment?.success, + refunded: payment?.refunded, + }; + return { props: { hideBranding: eventType.team ? eventType.team.hideBranding : eventType.users[0].hideBranding, @@ -1124,6 +1150,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { trpcState: ssr.dehydrate(), dynamicEventName: bookingInfo?.eventType?.eventName || "", bookingInfo, + paymentStatus: payment ? paymentStatus : null, }, }; } diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 2cb00bb0aa..132e1040ba 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -1591,5 +1591,9 @@ "book_your_team_members": "Book your team members together with collective events or cycle through to get the right person with round-robin.", "under_maintenance": "Down for maintenance", "under_maintenance_description": "The {{appName}} team are performing scheduled maintenance. If you have any questions, please contact support.", - "event_type_seats": "{{numberOfSeats}} seats" + "event_type_seats": "{{numberOfSeats}} seats", + "booking_with_payment_cancelled": "Paying for this event is no longer possible", + "booking_with_payment_cancelled_already_paid": "A refund for this booking payment it's on the way.", + "booking_with_payment_cancelled_refunded": "This booking payment has been refunded.", + "booking_confirmation_failed": "Booking confirmation failed" } diff --git a/packages/features/ee/payments/pages/payment.tsx b/packages/features/ee/payments/pages/payment.tsx index e4513f9041..fc2839700d 100644 --- a/packages/features/ee/payments/pages/payment.tsx +++ b/packages/features/ee/payments/pages/payment.tsx @@ -8,6 +8,8 @@ import type { inferSSRProps } from "@calcom/types/inferSSRProps"; import { ssrInit } from "@server/lib/ssr"; +import { BookingStatus } from ".prisma/client"; + export type PaymentPageProps = inferSSRProps; const querySchema = z.object({ @@ -44,6 +46,9 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => }, eventTypeId: true, location: true, + status: true, + rejectionReason: true, + cancellationReason: true, eventType: { select: { id: true, @@ -104,6 +109,19 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => hideBranding: eventType.team?.hideBranding || user?.hideBranding || null, }; + if ( + ([BookingStatus.CANCELLED, BookingStatus.REJECTED] as BookingStatus[]).includes( + booking.status as BookingStatus + ) + ) { + return { + redirect: { + destination: `/booking/${booking.uid}`, + permanent: false, + }, + }; + } + return { props: { user, diff --git a/packages/trpc/server/routers/viewer/bookings.tsx b/packages/trpc/server/routers/viewer/bookings.tsx index a06178f083..c65fca6d8b 100644 --- a/packages/trpc/server/routers/viewer/bookings.tsx +++ b/packages/trpc/server/routers/viewer/bookings.tsx @@ -772,12 +772,8 @@ export const bookingsRouter = router({ const isConfirmed = booking.status === BookingStatus.ACCEPTED; if (isConfirmed) throw new TRPCError({ code: "BAD_REQUEST", message: "Booking already confirmed" }); - /** When a booking that requires payment its being confirmed but doesn't have any payment, - * we shouldn’t save it on DestinationCalendars - * - * FIXME: This can cause unintended confirmations on rejection. - */ - if (booking.payment.length > 0 && !booking.paid) { + // If booking requires payment and is not paid, we don't allow confirmation + if (confirmed && booking.payment.length > 0 && !booking.paid) { await prisma.booking.update({ where: { id: bookingId, @@ -789,6 +785,7 @@ export const bookingsRouter = router({ return { message: "Booking confirmed", status: BookingStatus.ACCEPTED }; } + const attendeesListPromises = booking.attendees.map(async (attendee) => { return { name: attendee.name, @@ -1083,66 +1080,66 @@ export const bookingsRouter = router({ if (!!booking.payment.length) { const successPayment = booking.payment.find((payment) => payment.success); if (!successPayment) { - throw new Error("Cannot reject a booking without a successful payment"); - } + // Disable paymentLink for this booking + } else { + let eventTypeOwnerId; + if (booking.eventType?.owner) { + eventTypeOwnerId = booking.eventType.owner.id; + } else if (booking.eventType?.teamId) { + const teamOwner = await prisma.membership.findFirst({ + where: { + teamId: booking.eventType.teamId, + role: MembershipRole.OWNER, + }, + select: { + userId: true, + }, + }); + eventTypeOwnerId = teamOwner?.userId; + } - let eventTypeOwnerId; - if (booking.eventType?.owner) { - eventTypeOwnerId = booking.eventType.owner.id; - } else if (booking.eventType?.teamId) { - const teamOwner = await prisma.membership.findFirst({ + if (!eventTypeOwnerId) { + throw new Error("Event Type owner not found for obtaining payment app credentials"); + } + + const paymentAppCredentials = await prisma.credential.findMany({ where: { - teamId: booking.eventType.teamId, - role: MembershipRole.OWNER, + userId: eventTypeOwnerId, + appId: successPayment.appId, }, select: { - userId: true, - }, - }); - eventTypeOwnerId = teamOwner?.userId; - } - - if (!eventTypeOwnerId) { - throw new Error("Event Type owner not found for obtaining payment app credentials"); - } - - const paymentAppCredentials = await prisma.credential.findMany({ - where: { - userId: eventTypeOwnerId, - appId: successPayment.appId, - }, - select: { - key: true, - appId: true, - app: { - select: { - categories: true, - dirName: true, + key: true, + appId: true, + app: { + select: { + categories: true, + dirName: true, + }, }, }, - }, - }); + }); - const paymentAppCredential = paymentAppCredentials.find((credential) => { - return credential.appId === successPayment.appId; - }); + const paymentAppCredential = paymentAppCredentials.find((credential) => { + return credential.appId === successPayment.appId; + }); - if (!paymentAppCredential) { - throw new Error("Payment app credentials not found"); - } + if (!paymentAppCredential) { + throw new Error("Payment app credentials not found"); + } - // Posible to refactor TODO: - const paymentApp = appStore[paymentAppCredential?.app?.dirName as keyof typeof appStore]; - if (!(paymentApp && "lib" in paymentApp && "PaymentService" in paymentApp.lib)) { - console.warn(`payment App service of type ${paymentApp} is not implemented`); - return null; - } + // Posible to refactor TODO: + const paymentApp = appStore[paymentAppCredential?.app?.dirName as keyof typeof appStore]; + if (!(paymentApp && "lib" in paymentApp && "PaymentService" in paymentApp.lib)) { + console.warn(`payment App service of type ${paymentApp} is not implemented`); + return null; + } - const PaymentService = paymentApp.lib.PaymentService; - const paymentInstance = new PaymentService(paymentAppCredential); - const paymentData = await paymentInstance.refund(successPayment.id); - if (!paymentData.refunded) { - throw new Error("Payment could not be refunded"); + const PaymentService = paymentApp.lib.PaymentService; + const paymentInstance = new PaymentService(paymentAppCredential); + const paymentData = await paymentInstance.refund(successPayment.id); + if (!paymentData.refunded) { + throw new Error("Payment could not be refunded"); + } } } // end handle refunds.