fix: Do not refund payments when cancelling bookings (#13924)
* Remove logic that refunds on cancel booking * Remove cancelled refund booking test
This commit is contained in:
@@ -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");
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user