import { describe, it, expect, beforeAll, afterAll, afterEach } from "vitest"; import handleDeleteCredential from "@calcom/features/credentials/handleDeleteCredential"; import prisma from "@calcom/prisma"; import type { Credential, EventType, User } from "@calcom/prisma/client"; import { BookingStatus } from "@calcom/prisma/enums"; describe("handleDeleteCredential Integration Tests - BookingReference Soft Delete", () => { let testUser: User; let testCredential: Credential; let testEventType: EventType; const createdBookingReferenceIds: number[] = []; const createdBookingIds: number[] = []; async function deleteCredentialWithMetadata(credentialId: number, userId: number) { const user = await prisma.user.findUnique({ where: { id: userId }, select: { metadata: true }, }); return handleDeleteCredential({ userId, userMetadata: user?.metadata ?? {}, credentialId, }); } beforeAll(async () => { testUser = await prisma.user.create({ data: { email: "delete-credential-test@example.com", username: "delete-credential-test", }, }); testCredential = await prisma.credential.create({ data: { type: "stripe_payment", key: { test_key: "test_value" }, userId: testUser.id, appId: "stripe", }, }); testEventType = await prisma.eventType.create({ data: { title: "Paid Event", slug: "paid-event-test", length: 30, userId: testUser.id, price: 1000, // $10 currency: "usd", }, }); }); afterEach(async () => { if (createdBookingReferenceIds.length > 0) { await prisma.bookingReference.deleteMany({ where: { id: { in: createdBookingReferenceIds, }, }, }); createdBookingReferenceIds.splice(0, createdBookingReferenceIds.length); } if (createdBookingIds.length > 0) { await prisma.attendee.deleteMany({ where: { bookingId: { in: createdBookingIds, }, }, }); await prisma.booking.deleteMany({ where: { id: { in: createdBookingIds, }, }, }); createdBookingIds.splice(0, createdBookingIds.length); } }); afterAll(async () => { await prisma.eventType.delete({ where: { id: testEventType.id, }, }); try { await prisma.credential.delete({ where: { id: testCredential.id, }, }); } catch (error) { // Credential already deleted by test console.log("Credential deletion error:", error); } await prisma.user.delete({ where: { id: testUser.id, }, }); }); describe("Soft-delete bookingReferences when deleting payment credential", () => { it("should soft-delete booking references for unpaid bookings when credential is deleted", async () => { const unpaidBooking = await prisma.booking.create({ data: { uid: "unpaid-booking-uid-123", title: "Unpaid Booking", startTime: new Date(Date.now() + 86400000), // Tomorrow endTime: new Date(Date.now() + 90000000), userId: testUser.id, eventTypeId: testEventType.id, status: BookingStatus.PENDING, paid: false, attendees: { create: [ { email: "attendee@test.com", name: "Test Attendee", timeZone: "America/New_York", }, ], }, payment: { create: { uid: "payment-uid-123", appId: "stripe", amount: 1000, fee: 0, currency: "usd", success: false, refunded: false, data: {}, externalId: "ext-payment-123", }, }, }, }); createdBookingIds.push(unpaidBooking.id); const bookingRef1 = await prisma.bookingReference.create({ data: { type: "stripe_payment", uid: "stripe-ref-123", meetingId: "stripe-meeting-123", credentialId: testCredential.id, bookingId: unpaidBooking.id, }, }); createdBookingReferenceIds.push(bookingRef1.id); const bookingRef2 = await prisma.bookingReference.create({ data: { type: "google_calendar", uid: "gcal-ref-123", meetingId: "gcal-meeting-123", credentialId: testCredential.id, bookingId: unpaidBooking.id, }, }); createdBookingReferenceIds.push(bookingRef2.id); const refsBeforeDeletion = await prisma.bookingReference.findMany({ where: { bookingId: unpaidBooking.id, }, }); expect(refsBeforeDeletion).toHaveLength(2); expect(refsBeforeDeletion.every((ref) => ref.deleted === null)).toBe(true); await deleteCredentialWithMetadata(testCredential.id, testUser.id); const refsAfterDeletion = await prisma.bookingReference.findMany({ where: { bookingId: unpaidBooking.id, }, }); expect(refsAfterDeletion).toHaveLength(2); expect(refsAfterDeletion.every((ref) => ref.deleted === true)).toBe(true); const cancelledBooking = await prisma.booking.findUnique({ where: { id: unpaidBooking.id }, }); expect(cancelledBooking?.status).toBe(BookingStatus.CANCELLED); }); it("should persist soft-deleted references in database", async () => { const newCredential = await prisma.credential.create({ data: { type: "stripe_payment", key: { test_key: "test_value" }, userId: testUser.id, appId: "stripe", }, }); const unpaidBooking = await prisma.booking.create({ data: { uid: "unpaid-booking-uid-456", title: "Another Unpaid Booking", startTime: new Date(Date.now() + 86400000), endTime: new Date(Date.now() + 90000000), userId: testUser.id, eventTypeId: testEventType.id, status: BookingStatus.PENDING, paid: false, attendees: { create: [ { email: "attendee2@test.com", name: "Test Attendee 2", timeZone: "America/New_York", }, ], }, payment: { create: { uid: "payment-uid-456", appId: "stripe", amount: 1000, fee: 0, currency: "usd", success: false, refunded: false, data: {}, externalId: "ext-payment-456", }, }, }, }); createdBookingIds.push(unpaidBooking.id); const bookingRef = await prisma.bookingReference.create({ data: { type: "stripe_payment", uid: "stripe-ref-456", meetingId: "stripe-meeting-456", meetingUrl: "https://example.com/meeting", credentialId: newCredential.id, bookingId: unpaidBooking.id, }, }); createdBookingReferenceIds.push(bookingRef.id); await deleteCredentialWithMetadata(newCredential.id, testUser.id); const deletedRef = await prisma.bookingReference.findUnique({ where: { id: bookingRef.id }, }); expect(deletedRef).toBeDefined(); expect(deletedRef?.deleted).toBe(true); expect(deletedRef?.uid).toBe("stripe-ref-456"); expect(deletedRef?.meetingUrl).toBe("https://example.com/meeting"); expect(deletedRef?.type).toBe("stripe_payment"); }); it("should soft-delete references for multiple unpaid bookings when credential is deleted", async () => { const newCredential = await prisma.credential.create({ data: { type: "stripe_payment", key: { test_key: "test_value" }, userId: testUser.id, appId: "stripe", }, }); const booking1 = await prisma.booking.create({ data: { uid: "booking-1-uid", title: "Booking 1", startTime: new Date(Date.now() + 86400000), endTime: new Date(Date.now() + 90000000), userId: testUser.id, eventTypeId: testEventType.id, status: BookingStatus.PENDING, paid: false, attendees: { create: [ { email: "attendee-b1@test.com", name: "Attendee B1", timeZone: "America/New_York", }, ], }, payment: { create: { uid: "payment-uid-b1", appId: "stripe", amount: 1000, fee: 0, currency: "usd", success: false, refunded: false, data: {}, externalId: "ext-payment-b1", }, }, }, }); createdBookingIds.push(booking1.id); const booking2 = await prisma.booking.create({ data: { uid: "booking-2-uid", title: "Booking 2", startTime: new Date(Date.now() + 172800000), // 2 days from now endTime: new Date(Date.now() + 176400000), userId: testUser.id, eventTypeId: testEventType.id, status: BookingStatus.PENDING, paid: false, attendees: { create: [ { email: "attendee-b2@test.com", name: "Attendee B2", timeZone: "America/New_York", }, ], }, payment: { create: { uid: "payment-uid-b2", appId: "stripe", amount: 1000, fee: 0, currency: "usd", success: false, refunded: false, data: {}, externalId: "ext-payment-b2", }, }, }, }); createdBookingIds.push(booking2.id); const ref1 = await prisma.bookingReference.create({ data: { type: "stripe_payment", uid: "stripe-ref-b1", meetingId: "meeting-b1", credentialId: newCredential.id, bookingId: booking1.id, }, }); createdBookingReferenceIds.push(ref1.id); const ref2 = await prisma.bookingReference.create({ data: { type: "stripe_payment", uid: "stripe-ref-b2", meetingId: "meeting-b2", credentialId: newCredential.id, bookingId: booking2.id, }, }); createdBookingReferenceIds.push(ref2.id); await deleteCredentialWithMetadata(newCredential.id, testUser.id); const allRefsAfter = await prisma.bookingReference.findMany({ where: { id: { in: [ref1.id, ref2.id], }, }, }); expect(allRefsAfter).toHaveLength(2); expect(allRefsAfter.every((ref) => ref.deleted === true)).toBe(true); }); it("should not affect references of paid bookings when credential is deleted", async () => { const newCredential = await prisma.credential.create({ data: { type: "stripe_payment", key: { test_key: "test_value" }, userId: testUser.id, appId: "stripe", }, }); const paidBooking = await prisma.booking.create({ data: { uid: "paid-booking-uid", title: "Paid Booking", startTime: new Date(Date.now() + 86400000), endTime: new Date(Date.now() + 90000000), userId: testUser.id, eventTypeId: testEventType.id, status: BookingStatus.ACCEPTED, paid: true, attendees: { create: [ { email: "paid-attendee@test.com", name: "Paid Attendee", timeZone: "America/New_York", }, ], }, }, }); createdBookingIds.push(paidBooking.id); const paidBookingRef = await prisma.bookingReference.create({ data: { type: "stripe_payment", uid: "stripe-paid-ref", meetingId: "stripe-paid-meeting", credentialId: newCredential.id, bookingId: paidBooking.id, }, }); createdBookingReferenceIds.push(paidBookingRef.id); await deleteCredentialWithMetadata(newCredential.id, testUser.id); const refAfter = await prisma.bookingReference.findUnique({ where: { id: paidBookingRef.id }, }); expect(refAfter).toBeDefined(); expect(refAfter?.deleted).toBe(null); const bookingAfter = await prisma.booking.findUnique({ where: { id: paidBooking.id }, }); expect(bookingAfter?.status).toBe(BookingStatus.ACCEPTED); }); }); describe("Soft-delete behavior with queries after credential deletion", () => { it("should exclude soft-deleted references from regular queries", async () => { const newCredential = await prisma.credential.create({ data: { type: "stripe_payment", key: { test_key: "test_value" }, userId: testUser.id, appId: "stripe", }, }); const unpaidBooking = await prisma.booking.create({ data: { uid: "query-test-booking-uid", title: "Query Test Booking", startTime: new Date(Date.now() + 86400000), endTime: new Date(Date.now() + 90000000), userId: testUser.id, eventTypeId: testEventType.id, status: BookingStatus.PENDING, paid: false, attendees: { create: [ { email: "query-test@test.com", name: "Query Test", timeZone: "America/New_York", }, ], }, payment: { create: { uid: "payment-uid-query-test", appId: "stripe", amount: 1000, fee: 0, currency: "usd", success: false, refunded: false, data: {}, externalId: "ext-payment-query-test", }, }, }, }); createdBookingIds.push(unpaidBooking.id); const ref = await prisma.bookingReference.create({ data: { type: "stripe_payment", uid: "query-test-ref", meetingId: "query-test-meeting", credentialId: newCredential.id, bookingId: unpaidBooking.id, }, }); createdBookingReferenceIds.push(ref.id); await deleteCredentialWithMetadata(newCredential.id, testUser.id); const activeRefs = await prisma.bookingReference.findMany({ where: { bookingId: unpaidBooking.id, deleted: null, }, }); expect(activeRefs).toHaveLength(0); const allRefs = await prisma.bookingReference.findMany({ where: { bookingId: unpaidBooking.id, }, }); expect(allRefs).toHaveLength(1); expect(allRefs[0].deleted).toBe(true); }); }); });