From 802fbdf491acc48c4ba93fbb978494c05dff6ef5 Mon Sep 17 00:00:00 2001 From: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> Date: Wed, 13 Mar 2024 17:46:07 -0400 Subject: [PATCH] fix: Do not refund payments when cancelling bookings (#13924) * Remove logic that refunds on cancel booking * Remove cancelled refund booking test --- .../web/playwright/integrations-stripe.e2e.ts | 8 -- .../bookings/lib/handleCancelBooking.ts | 128 +----------------- 2 files changed, 1 insertion(+), 135 deletions(-) diff --git a/apps/web/playwright/integrations-stripe.e2e.ts b/apps/web/playwright/integrations-stripe.e2e.ts index 1d76db2699..19291b6345 100644 --- a/apps/web/playwright/integrations-stripe.e2e.ts +++ b/apps/web/playwright/integrations-stripe.e2e.ts @@ -219,14 +219,6 @@ test.describe("Stripe integration", () => { await user.confirmPendingPayment(); }); - test("Cancelled paid booking should be refunded", async ({ page, users, request }) => { - await page.click('[data-testid="cancel"]'); - await page.click('[data-testid="confirm_cancel"]'); - - await expect(await page.locator('[data-testid="cancelled-headline"]').first()).toBeVisible(); - await expect(page.getByText("This booking payment has been refunded")).toBeVisible(); - }); - test("Payment should confirm pending payment booking", async ({ page, users }) => { await page.goto("/bookings/upcoming"); diff --git a/packages/features/bookings/lib/handleCancelBooking.ts b/packages/features/bookings/lib/handleCancelBooking.ts index 3378411e8c..197cd030e6 100644 --- a/packages/features/bookings/lib/handleCancelBooking.ts +++ b/packages/features/bookings/lib/handleCancelBooking.ts @@ -1,7 +1,6 @@ import type { Prisma, WorkflowReminder } from "@prisma/client"; import type { NextApiRequest } from "next"; -import appStore from "@calcom/app-store"; import { getCalendar } from "@calcom/app-store/_utils/getCalendar"; import { FAKE_DAILY_CREDENTIAL } from "@calcom/app-store/dailyvideo/lib/VideoApiAdapter"; import { DailyLocationType } from "@calcom/app-store/locations"; @@ -21,16 +20,14 @@ import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib"; import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType"; import { HttpError } from "@calcom/lib/http-error"; import logger from "@calcom/lib/logger"; -import { handleRefundError } from "@calcom/lib/payment/handleRefundError"; import { getTranslation } from "@calcom/lib/server/i18n"; import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat"; import prisma, { bookingMinimalSelect } from "@calcom/prisma"; import type { WebhookTriggerEvents } from "@calcom/prisma/enums"; -import { BookingStatus, MembershipRole, WorkflowMethods } from "@calcom/prisma/enums"; +import { BookingStatus, WorkflowMethods } from "@calcom/prisma/enums"; import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential"; import { schemaBookingCancelParams } from "@calcom/prisma/zod-utils"; import type { CalendarEvent } from "@calcom/types/Calendar"; -import type { IAbstractPaymentService, PaymentApp } from "@calcom/types/PaymentService"; import cancelAttendeeSeat from "./handleSeats/cancel/cancelAttendeeSeat"; @@ -510,129 +507,6 @@ async function handler(req: CustomRequest) { } } - // Avoiding taking care of recurrence for now as Payments are not supported with Recurring Events at the moment - if (bookingToDelete && bookingToDelete.paid) { - const evt: CalendarEvent = { - type: bookingToDelete?.eventType?.slug as string, - title: bookingToDelete.title, - description: bookingToDelete.description ?? "", - customInputs: isPrismaObjOrUndefined(bookingToDelete.customInputs), - ...getCalEventResponses({ - booking: bookingToDelete, - bookingFields: bookingToDelete.eventType?.bookingFields ?? null, - }), - startTime: bookingToDelete.startTime.toISOString(), - endTime: bookingToDelete.endTime.toISOString(), - organizer: { - email: bookingToDelete?.userPrimaryEmail ?? bookingToDelete.user?.email ?? "dev@calendso.com", - name: bookingToDelete.user?.name ?? "no user", - timeZone: bookingToDelete.user?.timeZone ?? "", - timeFormat: getTimeFormatStringFromUserTimeFormat(organizer.timeFormat), - language: { translate: tOrganizer, locale: organizer.locale ?? "en" }, - }, - attendees: attendeesList, - location: bookingToDelete.location ?? "", - uid: bookingToDelete.uid ?? "", - destinationCalendar: bookingToDelete?.destinationCalendar - ? [bookingToDelete?.destinationCalendar] - : bookingToDelete?.user.destinationCalendar - ? [bookingToDelete?.user.destinationCalendar] - : [], - }; - - const successPayment = bookingToDelete.payment.find((payment) => payment.success); - if (!successPayment?.externalId) { - throw new Error("Cannot reject a booking without a successful payment"); - } - - let eventTypeOwnerId; - if (bookingToDelete.eventType?.owner) { - eventTypeOwnerId = bookingToDelete.eventType.owner.id; - } else if (bookingToDelete.eventType?.team?.id) { - const teamOwner = await prisma.membership.findFirst({ - where: { - teamId: bookingToDelete.eventType?.team.id, - role: MembershipRole.OWNER, - }, - 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, - }, - }, - }, - }); - - const paymentAppCredential = paymentAppCredentials.find((credential) => { - return credential.appId === successPayment.appId; - }); - - if (!paymentAppCredential) { - throw new Error("Payment app credentials not found"); - } - - // Posible to refactor TODO: - const paymentApp = (await appStore[ - paymentAppCredential?.app?.dirName as keyof typeof appStore - ]?.()) as PaymentApp; - - if (!paymentApp?.lib?.PaymentService) { - console.warn(`payment App service of type ${paymentApp} is not implemented`); - return null; - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const PaymentService = paymentApp.lib.PaymentService as unknown as any; - const paymentInstance = new PaymentService(paymentAppCredential) as IAbstractPaymentService; - - try { - await paymentInstance.refund(successPayment.id); - } catch (error) { - await handleRefundError({ - event: evt, - reason: error?.toString() || "unknown", - paymentId: successPayment.externalId, - }); - } - - await prisma.booking.update({ - where: { - id: bookingToDelete.id, - }, - data: { - status: BookingStatus.REJECTED, - }, - }); - - // We skip the deletion of the event, because that would also delete the payment reference, which we should keep - try { - await apiDeletes; - } catch (error) { - console.error("Error deleting event", error); - } - req.statusCode = 200; - return { message: "Booking successfully cancelled." }; - } - const bookingReferenceDeletes = prisma.bookingReference.deleteMany({ where: { bookingId: bookingToDelete.id,