Skip to content

Commit

Permalink
fix: Do not refund payments when cancelling bookings (#13924)
Browse files Browse the repository at this point in the history
* Remove logic that refunds on cancel booking

* Remove cancelled refund booking test
  • Loading branch information
joeauyeung authored Mar 13, 2024
1 parent 2803b50 commit 802fbdf
Show file tree
Hide file tree
Showing 2 changed files with 1 addition and 135 deletions.
8 changes: 0 additions & 8 deletions apps/web/playwright/integrations-stripe.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down
128 changes: 1 addition & 127 deletions packages/features/bookings/lib/handleCancelBooking.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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";

Expand Down Expand Up @@ -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 ?? "[email protected]",
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,
Expand Down

0 comments on commit 802fbdf

Please sign in to comment.