diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index 9854474d4a..0295994819 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -550,6 +550,9 @@ async function handler( eventType.schedulingType === SchedulingType.ROUND_ROBIN ) { const bookingRepo = new BookingRepository(prisma); + + const requiresPayment = !Number.isNaN(paymentAppData.price) && paymentAppData.price > 0; + const existingBooking = await bookingRepo.getValidBookingFromEventTypeForAttendee({ eventTypeId, bookerEmail, @@ -559,13 +562,20 @@ async function handler( }); if (existingBooking) { + const hasPayments = existingBooking.payment.length > 0; + const isPaidBooking = existingBooking.paid || !hasPayments; + + const shouldShowPaymentForm = requiresPayment && !isPaidBooking; + + const firstPayment = shouldShowPaymentForm ? existingBooking.payment[0] : undefined; + const bookingResponse = { ...existingBooking, user: { ...existingBooking.user, email: null, }, - paymentRequired: false, + paymentRequired: shouldShowPaymentForm, seatReferenceUid: "", }; @@ -574,8 +584,8 @@ async function handler( luckyUsers: bookingResponse.userId ? [bookingResponse.userId] : [], isDryRun, ...(isDryRun ? { troubleshooterData } : {}), - paymentUid: undefined, - paymentId: undefined, + paymentUid: firstPayment?.uid, + paymentId: firstPayment?.id, }; } } diff --git a/packages/features/bookings/lib/handleNewBooking/test/fresh-booking.test.ts b/packages/features/bookings/lib/handleNewBooking/test/fresh-booking.test.ts index 4e462adf66..1d75498d55 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/fresh-booking.test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/fresh-booking.test.ts @@ -7,6 +7,8 @@ * * They don't intend to test what the apps logic should do, but rather test if the apps are called with the correct data. For testing that, once should write tests within each app. */ +import prismaMock from "../../../../../../tests/libs/__mocks__/prisma"; + import { createBookingScenario, getDate, @@ -3277,6 +3279,122 @@ describe("handleNewBooking", () => { }, timeout ); + + test( + `Payment retry scenario - should return existing payment UID and prevent duplicate bookings when retrying canceled payment`, + async ({ emails }) => { + const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default; + const booker = getBooker({ + email: "booker@example.com", + name: "Booker", + }); + + const organizer = getOrganizer({ + name: "Organizer", + email: "organizer@example.com", + id: 101, + schedules: [TestData.schedules.IstWorkHours], + credentials: [getGoogleCalendarCredential(), getStripeAppCredential()], + selectedCalendars: [TestData.selectedCalendars.google], + }); + + const scenarioData = getScenarioData({ + eventTypes: [ + { + id: 1, + title: "Paid Event", + description: "It's a test Paid Event", + slotInterval: 30, + requiresConfirmation: false, + metadata: { + apps: { + stripe: { + price: 100, + enabled: true, + currency: "usd", + }, + }, + }, + length: 30, + users: [ + { + id: 101, + }, + ], + }, + ], + organizer, + apps: [TestData.apps["stripe-payment"]], + }); + + await createBookingScenario(scenarioData); + mockSuccessfulVideoMeetingCreation({ + metadataLookupKey: "dailyvideo", + }); + const { paymentUid } = mockPaymentApp({ + metadataLookupKey: "stripe", + appStoreLookupKey: "stripepayment", + }); + mockCalendarToHaveNoBusySlots("googlecalendar"); + + const mockBookingData = getMockRequestDataForBooking({ + data: { + eventTypeId: 1, + responses: { + email: booker.email, + name: booker.name, + location: { optionValue: "", value: "New York" }, + }, + }, + }); + + const firstBooking = await handleNewBooking({ + bookingData: mockBookingData, + }); + + expect(firstBooking).toEqual( + expect.objectContaining({ + paymentUid: expect.any(String), + paymentRequired: true, + uid: expect.any(String), + }) + ); + + const firstBookingInDb = await prismaMock.booking.findUnique({ + where: { uid: firstBooking.uid }, + include: { payment: true }, + }); + + expect(firstBookingInDb).toBeTruthy(); + expect(firstBookingInDb?.payment).toHaveLength(1); + expect(firstBookingInDb?.paid).toBe(false); + + const secondBooking = await handleNewBooking({ + bookingData: mockBookingData, + }); + + expect(secondBooking.uid).toBe(firstBooking.uid); + expect(secondBooking.paymentUid).toBe(firstBooking.paymentUid); + expect(secondBooking.paymentRequired).toBe(true); + + const bookingsInDb = await prismaMock.booking.findMany({ + where: { + eventTypeId: 1, + attendees: { + some: { + email: booker.email, + }, + }, + }, + include: { payment: true }, + }); + + expect(bookingsInDb).toHaveLength(1); + expect(bookingsInDb[0].payment).toHaveLength(1); + expect(bookingsInDb[0].uid).toBe(firstBooking.uid); + }, + timeout + ); }); }); diff --git a/packages/lib/server/repository/booking.ts b/packages/lib/server/repository/booking.ts index e459989fb8..589d00bb4c 100644 --- a/packages/lib/server/repository/booking.ts +++ b/packages/lib/server/repository/booking.ts @@ -828,6 +828,7 @@ export class BookingRepository { attendees: true, references: true, user: true, + payment: true, }, }); }