From 2b924f0eb09e5db46a4bca721d7a9f139e0800cd Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 27 Aug 2025 18:02:05 +0000 Subject: [PATCH 1/8] perf: optimize payment app imports to avoid loading entire app store - Add PaymentServiceMap generation to app-store-cli build process - Generate payment.services.generated.ts with lazy imports for 6 payment services - Update handlePayment.ts, deletePayment.ts, handlePaymentRefund.ts to use PaymentServiceMap - Update getConnectedApps.ts and tRPC payment routers to use PaymentServiceMap - Follow same pattern as analytics optimization in PR #23372 - Reduces bundle size by avoiding import of 100+ apps when only payment functionality needed Co-Authored-By: keith@cal.com --- packages/app-store-cli/src/build.ts | 36 +++++++++++++++++++ .../app-store/payment.services.generated.ts | 14 ++++++++ packages/lib/getConnectedApps.ts | 16 +++++---- packages/lib/payment/deletePayment.ts | 15 +++++--- packages/lib/payment/handlePayment.ts | 10 +++--- packages/lib/payment/handlePaymentRefund.ts | 15 +++++--- .../trpc/server/routers/viewer/payments.tsx | 14 ++++---- .../viewer/payments/chargeCard.handler.ts | 15 ++++---- 8 files changed, 102 insertions(+), 33 deletions(-) create mode 100644 packages/app-store/payment.services.generated.ts diff --git a/packages/app-store-cli/src/build.ts b/packages/app-store-cli/src/build.ts index b43a3b819ec5c0..2dda8fe4612593 100644 --- a/packages/app-store-cli/src/build.ts +++ b/packages/app-store-cli/src/build.ts @@ -415,6 +415,41 @@ function generateFiles() { analyticsOutput.push(...analyticsServices); } + const paymentOutput = []; + const paymentServices = getExportedObject( + "PaymentServiceMap", + { + importConfig: { + fileToBeImported: "index.ts", + importName: "default", + }, + lazyImport: true, + }, + (app: App) => { + const hasPaymentService = fs.existsSync(path.join(APP_STORE_PATH, app.path, "lib/PaymentService.ts")); + return hasPaymentService; + } + ); + + const paymentExportLineIndex = paymentServices.findIndex((line) => + line.startsWith("export const PaymentServiceMap") + ); + if (paymentExportLineIndex !== -1) { + const exportLine = paymentServices[paymentExportLineIndex]; + const objectContent = paymentServices.slice(paymentExportLineIndex + 1, -1); + + paymentOutput.push( + exportLine.replace( + "export const PaymentServiceMap = {", + "export const PaymentServiceMap = process.env.NEXT_PUBLIC_IS_E2E ? {} : {" + ), + ...objectContent, + "};" + ); + } else { + paymentOutput.push(...paymentServices); + } + const banner = `/** This file is autogenerated using the command \`yarn app-store:build --watch\`. Don't modify this file manually. @@ -422,6 +457,7 @@ function generateFiles() { `; const filesToGenerate: [string, string[]][] = [ ["analytics.services.generated.ts", analyticsOutput], + ["payment.services.generated.ts", paymentOutput], ["apps.metadata.generated.ts", metadataOutput], ["apps.server.generated.ts", serverOutput], ["apps.browser.generated.tsx", browserOutput], diff --git a/packages/app-store/payment.services.generated.ts b/packages/app-store/payment.services.generated.ts new file mode 100644 index 00000000000000..ccdf79a16b27f6 --- /dev/null +++ b/packages/app-store/payment.services.generated.ts @@ -0,0 +1,14 @@ +/** + This file is autogenerated using the command `yarn app-store:build --watch`. + Don't modify this file manually. +**/ +export const PaymentServiceMap = process.env.NEXT_PUBLIC_IS_E2E + ? {} + : { + alby: import("./alby/index"), + btcpayserver: import("./btcpayserver/index"), + hitpay: import("./hitpay/index"), + "mock-payment-app": import("./mock-payment-app/index"), + paypal: import("./paypal/index"), + stripepayment: import("./stripepayment/index"), + }; diff --git a/packages/lib/getConnectedApps.ts b/packages/lib/getConnectedApps.ts index 09567d64283b7f..730d63a38eb03e 100644 --- a/packages/lib/getConnectedApps.ts +++ b/packages/lib/getConnectedApps.ts @@ -1,7 +1,7 @@ import type { Prisma } from "@prisma/client"; -import appStore from "@calcom/app-store"; import type { TDependencyData } from "@calcom/app-store/_appRegistry"; +import { PaymentServiceMap } from "@calcom/app-store/payment.services.generated"; import type { CredentialOwner } from "@calcom/app-store/types"; import { getAppFromSlug } from "@calcom/app-store/utils"; import { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner"; @@ -12,7 +12,6 @@ import type { PrismaClient } from "@calcom/prisma"; import type { User } from "@calcom/prisma/client"; import type { AppCategories } from "@calcom/prisma/enums"; import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential"; -import type { PaymentApp } from "@calcom/types/PaymentService"; import { buildNonDelegationCredentials } from "./delegationCredential/clientAndServer"; @@ -184,11 +183,14 @@ export async function getConnectedApps({ // undefined it means that app don't require app/setup/page let isSetupAlready = undefined; if (credential && app.categories.includes("payment")) { - const paymentApp = (await appStore[app.dirName as keyof typeof appStore]?.()) as PaymentApp | null; - if (paymentApp && "lib" in paymentApp && paymentApp?.lib && "PaymentService" in paymentApp?.lib) { - const PaymentService = paymentApp.lib.PaymentService; - const paymentInstance = new PaymentService(credential); - isSetupAlready = paymentInstance.isSetupAlready(); + const paymentAppImportFn = PaymentServiceMap[app.dirName as keyof typeof PaymentServiceMap]; + if (paymentAppImportFn) { + const paymentApp = await paymentAppImportFn; + if (paymentApp && "lib" in paymentApp && paymentApp?.lib && "PaymentService" in paymentApp?.lib) { + const PaymentService = paymentApp.lib.PaymentService; + const paymentInstance = new PaymentService(credential); + isSetupAlready = paymentInstance.isSetupAlready(); + } } } diff --git a/packages/lib/payment/deletePayment.ts b/packages/lib/payment/deletePayment.ts index 20411bdf79c37b..cf3ce8d925ab8e 100644 --- a/packages/lib/payment/deletePayment.ts +++ b/packages/lib/payment/deletePayment.ts @@ -1,8 +1,8 @@ import type { Payment, Prisma } from "@prisma/client"; -import appStore from "@calcom/app-store"; +import { PaymentServiceMap } from "@calcom/app-store/payment.services.generated"; import type { AppCategories } from "@calcom/prisma/enums"; -import type { IAbstractPaymentService, PaymentApp } from "@calcom/types/PaymentService"; +import type { IAbstractPaymentService } from "@calcom/types/PaymentService"; const deletePayment = async ( paymentId: Payment["id"], @@ -15,9 +15,14 @@ const deletePayment = async ( } | null; } ): Promise => { - const paymentApp = (await appStore[ - paymentAppCredentials?.app?.dirName as keyof typeof appStore - ]?.()) as PaymentApp; + const key = paymentAppCredentials?.app?.dirName; + const paymentAppImportFn = PaymentServiceMap[key as keyof typeof PaymentServiceMap]; + if (!paymentAppImportFn) { + console.warn(`payment app not implemented for key: ${key}`); + return false; + } + + const paymentApp = await paymentAppImportFn; if (!paymentApp?.lib?.PaymentService) { console.warn(`payment App service of type ${paymentApp} is not implemented`); return false; diff --git a/packages/lib/payment/handlePayment.ts b/packages/lib/payment/handlePayment.ts index 74e72dc5c3e570..92e81fd692ee46 100644 --- a/packages/lib/payment/handlePayment.ts +++ b/packages/lib/payment/handlePayment.ts @@ -1,5 +1,6 @@ import type { AppCategories, Prisma } from "@prisma/client"; +import { PaymentServiceMap } from "@calcom/app-store/payment.services.generated"; import type { EventTypeAppsList } from "@calcom/app-store/utils"; import type { CompleteEventType } from "@calcom/prisma/zod"; import { eventTypeAppMetadataOptionalSchema } from "@calcom/prisma/zod-utils"; @@ -52,12 +53,13 @@ const handlePayment = async ({ if (isDryRun) return null; const key = paymentAppCredentials?.app?.dirName; - const appStore = await import("@calcom/app-store").then((m) => m.default); - if (!isKeyOf(appStore, key)) { - console.warn(`key: ${key} is not a valid key in appStore`); + const paymentAppImportFn = PaymentServiceMap[key as keyof typeof PaymentServiceMap]; + if (!paymentAppImportFn) { + console.warn(`payment app not implemented for key: ${key}`); return null; } - const paymentApp = await appStore[key]?.(); + + const paymentApp = await paymentAppImportFn; if (!isPaymentApp(paymentApp)) { console.warn(`payment App service of type ${paymentApp} is not implemented`); return null; diff --git a/packages/lib/payment/handlePaymentRefund.ts b/packages/lib/payment/handlePaymentRefund.ts index a51a12423ed15f..17fb1896fb155c 100644 --- a/packages/lib/payment/handlePaymentRefund.ts +++ b/packages/lib/payment/handlePaymentRefund.ts @@ -1,8 +1,8 @@ import type { Payment, Prisma } from "@prisma/client"; -import appStore from "@calcom/app-store"; +import { PaymentServiceMap } from "@calcom/app-store/payment.services.generated"; import type { AppCategories } from "@calcom/prisma/enums"; -import type { IAbstractPaymentService, PaymentApp } from "@calcom/types/PaymentService"; +import type { IAbstractPaymentService } from "@calcom/types/PaymentService"; const handlePaymentRefund = async ( paymentId: Payment["id"], @@ -15,9 +15,14 @@ const handlePaymentRefund = async ( } | null; } ) => { - const paymentApp = (await appStore[ - paymentAppCredentials?.app?.dirName as keyof typeof appStore - ]?.()) as PaymentApp; + const key = paymentAppCredentials?.app?.dirName; + const paymentAppImportFn = PaymentServiceMap[key as keyof typeof PaymentServiceMap]; + if (!paymentAppImportFn) { + console.warn(`payment app not implemented for key: ${key}`); + return false; + } + + const paymentApp = await paymentAppImportFn; if (!paymentApp?.lib?.PaymentService) { console.warn(`payment App service of type ${paymentApp} is not implemented`); return false; diff --git a/packages/trpc/server/routers/viewer/payments.tsx b/packages/trpc/server/routers/viewer/payments.tsx index 1485cbc6684543..9c5a356dc69f92 100644 --- a/packages/trpc/server/routers/viewer/payments.tsx +++ b/packages/trpc/server/routers/viewer/payments.tsx @@ -1,6 +1,6 @@ import { z } from "zod"; -import appStore from "@calcom/app-store"; +import { PaymentServiceMap } from "@calcom/app-store/payment.services.generated"; import dayjs from "@calcom/dayjs"; import { sendNoShowFeeChargedEmail } from "@calcom/emails"; import { WebhookService } from "@calcom/features/webhooks/lib/WebhookService"; @@ -9,7 +9,6 @@ import { getTranslation } from "@calcom/lib/server/i18n"; import { WebhookTriggerEvents } from "@calcom/prisma/enums"; import type { EventTypeMetadata } from "@calcom/prisma/zod-utils"; import type { CalendarEvent } from "@calcom/types/Calendar"; -import type { PaymentApp } from "@calcom/types/PaymentService"; import { TRPCError } from "@trpc/server"; @@ -108,10 +107,13 @@ export const paymentsRouter = router({ throw new TRPCError({ code: "BAD_REQUEST", message: "Invalid payment credential" }); } - const paymentApp = (await appStore[ - paymentCredential?.app?.dirName as keyof typeof appStore - ]?.()) as PaymentApp | null; + const key = paymentCredential?.app?.dirName; + const paymentAppImportFn = PaymentServiceMap[key as keyof typeof PaymentServiceMap]; + if (!paymentAppImportFn) { + throw new TRPCError({ code: "BAD_REQUEST", message: "Payment app not implemented" }); + } + const paymentApp = await paymentAppImportFn; if (!(paymentApp && paymentApp.lib && "lib" in paymentApp && "PaymentService" in paymentApp.lib)) { throw new TRPCError({ code: "BAD_REQUEST", message: "Payment service not found" }); } @@ -120,7 +122,7 @@ export const paymentsRouter = router({ const paymentInstance = new PaymentService(paymentCredential); try { - const paymentData = await paymentInstance.chargeCard(payment); + const paymentData = await paymentInstance.chargeCard(payment, booking.id); if (!paymentData) { throw new TRPCError({ code: "NOT_FOUND", message: `Could not generate payment data` }); diff --git a/packages/trpc/server/routers/viewer/payments/chargeCard.handler.ts b/packages/trpc/server/routers/viewer/payments/chargeCard.handler.ts index 44c577fa73feea..bd47d7d918cf8e 100644 --- a/packages/trpc/server/routers/viewer/payments/chargeCard.handler.ts +++ b/packages/trpc/server/routers/viewer/payments/chargeCard.handler.ts @@ -1,4 +1,4 @@ -import appStore from "@calcom/app-store"; +import { PaymentServiceMap } from "@calcom/app-store/payment.services.generated"; import dayjs from "@calcom/dayjs"; import { sendNoShowFeeChargedEmail } from "@calcom/emails"; import { ErrorCode } from "@calcom/lib/errorCodes"; @@ -10,7 +10,7 @@ import { TeamRepository } from "@calcom/lib/server/repository/team"; import type { PrismaClient } from "@calcom/prisma"; import type { EventTypeMetadata } from "@calcom/prisma/zod-utils"; import type { CalendarEvent } from "@calcom/types/Calendar"; -import type { IAbstractPaymentService, PaymentApp } from "@calcom/types/PaymentService"; +import type { IAbstractPaymentService } from "@calcom/types/PaymentService"; import { TRPCError } from "@trpc/server"; @@ -125,10 +125,13 @@ export const chargeCardHandler = async ({ ctx, input }: ChargeCardHandlerOptions throw new TRPCError({ code: "BAD_REQUEST", message: "Invalid payment credential" }); } - const paymentApp = (await appStore[ - paymentCredential?.app?.dirName as keyof typeof appStore - ]?.()) as PaymentApp; + const key = paymentCredential?.app?.dirName; + const paymentAppImportFn = PaymentServiceMap[key as keyof typeof PaymentServiceMap]; + if (!paymentAppImportFn) { + throw new TRPCError({ code: "BAD_REQUEST", message: "Payment app not implemented" }); + } + const paymentApp = await paymentAppImportFn; if (!paymentApp?.lib?.PaymentService) { throw new TRPCError({ code: "BAD_REQUEST", message: "Payment service not found" }); } @@ -137,7 +140,7 @@ export const chargeCardHandler = async ({ ctx, input }: ChargeCardHandlerOptions const paymentInstance = new PaymentService(paymentCredential) as IAbstractPaymentService; try { - const paymentData = await paymentInstance.chargeCard(booking.payment[0]); + const paymentData = await paymentInstance.chargeCard(booking.payment[0], booking.id); if (!paymentData) { throw new TRPCError({ code: "NOT_FOUND", message: `Could not generate payment data` }); From 7079405a4ae90ae6c580778901cdddc832b38d99 Mon Sep 17 00:00:00 2001 From: Keith Williams Date: Wed, 27 Aug 2025 15:06:14 -0300 Subject: [PATCH 2/8] Update build.ts --- packages/app-store-cli/src/build.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app-store-cli/src/build.ts b/packages/app-store-cli/src/build.ts index 2dda8fe4612593..50023455f4f4fb 100644 --- a/packages/app-store-cli/src/build.ts +++ b/packages/app-store-cli/src/build.ts @@ -457,7 +457,6 @@ function generateFiles() { `; const filesToGenerate: [string, string[]][] = [ ["analytics.services.generated.ts", analyticsOutput], - ["payment.services.generated.ts", paymentOutput], ["apps.metadata.generated.ts", metadataOutput], ["apps.server.generated.ts", serverOutput], ["apps.browser.generated.tsx", browserOutput], @@ -466,6 +465,7 @@ function generateFiles() { ["bookerApps.metadata.generated.ts", bookerMetadataOutput], ["crm.apps.generated.ts", crmOutput], ["calendar.services.generated.ts", calendarOutput], + ["payment.services.generated.ts", paymentOutput], ]; filesToGenerate.forEach(([fileName, output]) => { fs.writeFileSync(`${APP_STORE_PATH}/${fileName}`, formatOutput(`${banner}${output.join("\n")}`)); From 0e65f917c42e5e8685005ba0a5c04d506e79b3cd Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 27 Aug 2025 19:41:06 +0000 Subject: [PATCH 3/8] fix: update payment service test mocking to work with PaymentServiceMap - Remove obsolete appStoreMock line from bookingScenario.ts since handlePayment now uses PaymentServiceMap - Update setupVitest.ts to import prismaMock from correct PrismockClient instance - Add PaymentServiceMap mock following PR #22450 pattern for calendar services - Ensure MockPaymentService uses consistent externalId across test files - Fix webhook handler to return 200 status by ensuring payment records are found correctly Co-Authored-By: keith@cal.com --- .../bookingScenario/MockPaymentService.ts | 14 +- .../utils/bookingScenario/bookingScenario.ts | 13 +- setupVitest.ts | 120 ++++++++++++++++++ vitest.config.ts | 2 + 4 files changed, 132 insertions(+), 17 deletions(-) diff --git a/apps/web/test/utils/bookingScenario/MockPaymentService.ts b/apps/web/test/utils/bookingScenario/MockPaymentService.ts index a69c90b431e19b..13651ed4cf5844 100644 --- a/apps/web/test/utils/bookingScenario/MockPaymentService.ts +++ b/apps/web/test/utils/bookingScenario/MockPaymentService.ts @@ -1,7 +1,6 @@ import prismaMock from "../../../../../tests/libs/__mocks__/prisma"; import type { Payment, Prisma, PaymentOption, Booking } from "@prisma/client"; -import { v4 as uuidv4 } from "uuid"; import "vitest-fetch-mock"; import { sendAwaitingPaymentEmailAndSMS } from "@calcom/emails"; @@ -13,8 +12,8 @@ export function getMockPaymentService() { function createPaymentLink(/*{ paymentUid, name, email, date }*/) { return "http://mock-payment.example.com/"; } - const paymentUid = uuidv4(); - const externalId = uuidv4(); + const paymentUid = "MOCK_PAYMENT_UID"; + const externalId = "mock_payment_external_id"; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore @@ -37,7 +36,7 @@ export function getMockPaymentService() { bookingId, // booking Booking? @relation(fields: [bookingId], references: [id], onDelete: Cascade) fee: 10, - success: true, + success: false, refunded: false, data: {}, externalId, @@ -46,11 +45,16 @@ export function getMockPaymentService() { currency: payment.currency, }; - const paymentData = prismaMock.payment.create({ + const paymentData = await prismaMock.payment.create({ data: paymentCreateData, }); logger.silly("Created mock payment", JSON.stringify({ paymentData })); + const verifyPayment = await prismaMock.payment.findFirst({ + where: { externalId: paymentCreateData.externalId }, + }); + logger.silly("Verified payment exists", JSON.stringify({ verifyPayment })); + return paymentData; } async afterPayment( diff --git a/apps/web/test/utils/bookingScenario/bookingScenario.ts b/apps/web/test/utils/bookingScenario/bookingScenario.ts index e5a3068670c575..13008b83014b2a 100644 --- a/apps/web/test/utils/bookingScenario/bookingScenario.ts +++ b/apps/web/test/utils/bookingScenario/bookingScenario.ts @@ -2105,18 +2105,7 @@ export function mockPaymentApp({ appStoreLookupKey?: string; }) { appStoreLookupKey = appStoreLookupKey || metadataLookupKey; - const { paymentUid, externalId, MockPaymentService } = getMockPaymentService(); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - //@ts-ignore - appStoreMock.default[appStoreLookupKey as keyof typeof appStoreMock.default].mockImplementation(() => { - return new Promise((resolve) => { - resolve({ - lib: { - PaymentService: MockPaymentService, - }, - }); - }); - }); + const { paymentUid, externalId } = getMockPaymentService(); return { paymentUid, diff --git a/setupVitest.ts b/setupVitest.ts index 96f7aefab3f1ac..5238b7fbd108af 100644 --- a/setupVitest.ts +++ b/setupVitest.ts @@ -2,6 +2,9 @@ import matchers from "@testing-library/jest-dom/matchers"; import ResizeObserver from "resize-observer-polyfill"; import { vi, expect } from "vitest"; import createFetchMock from "vitest-fetch-mock"; +import type { CalendarService } from "@calcom/types/Calendar"; +import prismaMock from "./tests/libs/__mocks__/prisma"; +import { v4 as uuidv4 } from "uuid"; global.ResizeObserver = ResizeObserver; const fetchMocker = createFetchMock(vi); @@ -53,3 +56,120 @@ vi.mock("@calcom/exchange2013calendar/lib/CalendarService", () => ({ vi.mock("@calcom/exchange2016calendar/lib/CalendarService", () => ({ default: MockExchangeCalendarService, })); + +const MOCK_PAYMENT_UID = "MOCK_PAYMENT_UID"; + +class MockPaymentService { + constructor(credentials?: any) { + this.credentials = credentials; + } + + private credentials: any; + + async create( + payment: any, + bookingId: number, + userId: number, + username: string | null, + bookerName: string | null, + paymentOption: any, + bookerEmail: string, + bookerPhoneNumber?: string | null, + selectedEventTypeTitle?: string, + eventTitle?: string + ) { + const externalId = "mock_payment_external_id"; + + const paymentCreateData = { + uid: MOCK_PAYMENT_UID, + appId: null, + bookingId, + fee: 10, + success: false, + refunded: false, + data: {}, + externalId, + paymentOption, + amount: payment.amount, + currency: payment.currency, + }; + + const createdPayment = await prismaMock.payment.create({ + data: paymentCreateData, + }); + + return createdPayment; + } + async collectCard() { + return { success: true }; + } + async chargeCard() { + return { success: true }; + } + async refund() { + return { success: true }; + } + async deletePayment() { + return { success: true }; + } + async afterPayment(event: any, booking: any, paymentData: any) { + const { sendAwaitingPaymentEmailAndSMS } = await import("@calcom/emails"); + await sendAwaitingPaymentEmailAndSMS({ + ...event, + paymentInfo: { + link: "http://mock-payment.example.com/", + paymentOption: paymentData.paymentOption || "ON_BOOKING", + amount: paymentData.amount, + currency: paymentData.currency, + }, + }); + return { success: true }; + } +} + +vi.mock("@calcom/app-store/stripepayment/index", () => ({ + lib: { + PaymentService: MockPaymentService, + }, +})); + +vi.mock("@calcom/app-store/paypal/index", () => ({ + lib: { + PaymentService: MockPaymentService, + }, +})); + +vi.mock("@calcom/app-store/alby/index", () => ({ + lib: { + PaymentService: MockPaymentService, + }, +})); + +vi.mock("@calcom/app-store/hitpay/index", () => ({ + lib: { + PaymentService: MockPaymentService, + }, +})); + +vi.mock("@calcom/app-store/btcpayserver/index", () => ({ + lib: { + PaymentService: MockPaymentService, + }, +})); + +vi.mock("@calcom/app-store/mock-payment-app/index", () => ({ + lib: { + PaymentService: MockPaymentService, + }, +})); + +vi.mock("@calcom/app-store/payment.services.generated", () => ({ + PaymentServiceMap: { + stripepayment: Promise.resolve({ lib: { PaymentService: MockPaymentService } }), + paypal: Promise.resolve({ lib: { PaymentService: MockPaymentService } }), + alby: Promise.resolve({ lib: { PaymentService: MockPaymentService } }), + hitpay: Promise.resolve({ lib: { PaymentService: MockPaymentService } }), + btcpayserver: Promise.resolve({ lib: { PaymentService: MockPaymentService } }), + "mock-payment-app": Promise.resolve({ lib: { PaymentService: MockPaymentService } }), + }, +})); diff --git a/vitest.config.ts b/vitest.config.ts index e3109fbcc26a1c..c66b8d534d9288 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -3,6 +3,8 @@ process.env.INTEGRATION_TEST_MODE = "true"; export default defineConfig({ test: { + setupFiles: ["./setupVitest.ts"], + coverage: { provider: "v8", }, From f7614a9e60a00e6297071659a4b74dc542e2d93e Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 27 Aug 2025 20:10:09 +0000 Subject: [PATCH 4/8] fix: revert prismaMock import to avoid interfering with other tests' vi.spyOn() calls - Remove global prismaMock import from setupVitest.ts that was causing 'is not a spy' errors - Update MockPaymentService to import prismaMock locally to maintain payment test functionality - Fixes organization and outOfOffice tests while preserving payment service optimization Co-Authored-By: keith@cal.com --- setupVitest.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/setupVitest.ts b/setupVitest.ts index 5238b7fbd108af..9edd87d4923fb6 100644 --- a/setupVitest.ts +++ b/setupVitest.ts @@ -3,8 +3,6 @@ import ResizeObserver from "resize-observer-polyfill"; import { vi, expect } from "vitest"; import createFetchMock from "vitest-fetch-mock"; import type { CalendarService } from "@calcom/types/Calendar"; -import prismaMock from "./tests/libs/__mocks__/prisma"; -import { v4 as uuidv4 } from "uuid"; global.ResizeObserver = ResizeObserver; const fetchMocker = createFetchMock(vi); @@ -78,6 +76,7 @@ class MockPaymentService { selectedEventTypeTitle?: string, eventTitle?: string ) { + const { default: prismaMock } = await import("./tests/libs/__mocks__/prisma"); const externalId = "mock_payment_external_id"; const paymentCreateData = { From ca77702dd3c0d03bedd097be45b4eb6dc4f844b6 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 27 Aug 2025 22:58:26 +0000 Subject: [PATCH 5/8] fix: remove E2E conditional check from payment services map generation - Payment services map now always includes all payment apps regardless of E2E environment - Ensures payment functionality is consistently available across all environments - Addresses CI failures caused by conditional payment service loading Co-Authored-By: keith@cal.com --- packages/app-store-cli/src/build.ts | 19 +------------------ .../app-store/payment.services.generated.ts | 18 ++++++++---------- 2 files changed, 9 insertions(+), 28 deletions(-) diff --git a/packages/app-store-cli/src/build.ts b/packages/app-store-cli/src/build.ts index 50023455f4f4fb..773833c7a38317 100644 --- a/packages/app-store-cli/src/build.ts +++ b/packages/app-store-cli/src/build.ts @@ -431,24 +431,7 @@ function generateFiles() { } ); - const paymentExportLineIndex = paymentServices.findIndex((line) => - line.startsWith("export const PaymentServiceMap") - ); - if (paymentExportLineIndex !== -1) { - const exportLine = paymentServices[paymentExportLineIndex]; - const objectContent = paymentServices.slice(paymentExportLineIndex + 1, -1); - - paymentOutput.push( - exportLine.replace( - "export const PaymentServiceMap = {", - "export const PaymentServiceMap = process.env.NEXT_PUBLIC_IS_E2E ? {} : {" - ), - ...objectContent, - "};" - ); - } else { - paymentOutput.push(...paymentServices); - } + paymentOutput.push(...paymentServices); const banner = `/** This file is autogenerated using the command \`yarn app-store:build --watch\`. diff --git a/packages/app-store/payment.services.generated.ts b/packages/app-store/payment.services.generated.ts index ccdf79a16b27f6..5c815207b4f8b9 100644 --- a/packages/app-store/payment.services.generated.ts +++ b/packages/app-store/payment.services.generated.ts @@ -2,13 +2,11 @@ This file is autogenerated using the command `yarn app-store:build --watch`. Don't modify this file manually. **/ -export const PaymentServiceMap = process.env.NEXT_PUBLIC_IS_E2E - ? {} - : { - alby: import("./alby/index"), - btcpayserver: import("./btcpayserver/index"), - hitpay: import("./hitpay/index"), - "mock-payment-app": import("./mock-payment-app/index"), - paypal: import("./paypal/index"), - stripepayment: import("./stripepayment/index"), - }; +export const PaymentServiceMap = { + alby: import("./alby/index"), + btcpayserver: import("./btcpayserver/index"), + hitpay: import("./hitpay/index"), + "mock-payment-app": import("./mock-payment-app/index"), + paypal: import("./paypal/index"), + stripepayment: import("./stripepayment/index"), +}; From 7306ab94bbdd0cf918dd707a4861bcacfef3c3c2 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 28 Aug 2025 00:05:20 +0000 Subject: [PATCH 6/8] refactor: use direct PaymentService imports instead of .lib structure - Update app-store-cli to import directly from lib/PaymentService.ts files - Modify all payment handlers to access PaymentService directly - Update test mocks to match new direct import structure - Remove .lib property access pattern across payment system - Maintain backward compatibility while improving import efficiency Co-Authored-By: keith@cal.com --- packages/app-store-cli/src/build.ts | 4 +-- .../app-store/payment.services.generated.ts | 12 +++---- packages/lib/getConnectedApps.ts | 4 +-- packages/lib/payment/deletePayment.ts | 9 +++-- packages/lib/payment/handlePayment.ts | 19 ++++------ packages/lib/payment/handlePaymentRefund.ts | 9 +++-- .../trpc/server/routers/viewer/payments.tsx | 4 +-- .../viewer/payments/chargeCard.handler.ts | 5 ++- setupVitest.ts | 36 +++++++------------ 9 files changed, 41 insertions(+), 61 deletions(-) diff --git a/packages/app-store-cli/src/build.ts b/packages/app-store-cli/src/build.ts index 773833c7a38317..f6b8b988e16301 100644 --- a/packages/app-store-cli/src/build.ts +++ b/packages/app-store-cli/src/build.ts @@ -420,8 +420,8 @@ function generateFiles() { "PaymentServiceMap", { importConfig: { - fileToBeImported: "index.ts", - importName: "default", + fileToBeImported: "lib/PaymentService.ts", + importName: "PaymentService", }, lazyImport: true, }, diff --git a/packages/app-store/payment.services.generated.ts b/packages/app-store/payment.services.generated.ts index 5c815207b4f8b9..2e42d632580810 100644 --- a/packages/app-store/payment.services.generated.ts +++ b/packages/app-store/payment.services.generated.ts @@ -3,10 +3,10 @@ Don't modify this file manually. **/ export const PaymentServiceMap = { - alby: import("./alby/index"), - btcpayserver: import("./btcpayserver/index"), - hitpay: import("./hitpay/index"), - "mock-payment-app": import("./mock-payment-app/index"), - paypal: import("./paypal/index"), - stripepayment: import("./stripepayment/index"), + alby: import("./alby/lib/PaymentService"), + btcpayserver: import("./btcpayserver/lib/PaymentService"), + hitpay: import("./hitpay/lib/PaymentService"), + "mock-payment-app": import("./mock-payment-app/lib/PaymentService"), + paypal: import("./paypal/lib/PaymentService"), + stripepayment: import("./stripepayment/lib/PaymentService"), }; diff --git a/packages/lib/getConnectedApps.ts b/packages/lib/getConnectedApps.ts index 730d63a38eb03e..09fde58bc01639 100644 --- a/packages/lib/getConnectedApps.ts +++ b/packages/lib/getConnectedApps.ts @@ -186,8 +186,8 @@ export async function getConnectedApps({ const paymentAppImportFn = PaymentServiceMap[app.dirName as keyof typeof PaymentServiceMap]; if (paymentAppImportFn) { const paymentApp = await paymentAppImportFn; - if (paymentApp && "lib" in paymentApp && paymentApp?.lib && "PaymentService" in paymentApp?.lib) { - const PaymentService = paymentApp.lib.PaymentService; + if (paymentApp && "PaymentService" in paymentApp && paymentApp?.PaymentService) { + const PaymentService = paymentApp.PaymentService; const paymentInstance = new PaymentService(credential); isSetupAlready = paymentInstance.isSetupAlready(); } diff --git a/packages/lib/payment/deletePayment.ts b/packages/lib/payment/deletePayment.ts index cf3ce8d925ab8e..8861125a4658a3 100644 --- a/packages/lib/payment/deletePayment.ts +++ b/packages/lib/payment/deletePayment.ts @@ -22,13 +22,12 @@ const deletePayment = async ( return false; } - const paymentApp = await paymentAppImportFn; - if (!paymentApp?.lib?.PaymentService) { - console.warn(`payment App service of type ${paymentApp} is not implemented`); + const paymentAppModule = await paymentAppImportFn; + if (!paymentAppModule?.PaymentService) { + console.warn(`payment App service not found for key: ${key}`); return false; } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const PaymentService = paymentApp.lib.PaymentService as any; + const PaymentService = paymentAppModule.PaymentService; const paymentInstance = new PaymentService(paymentAppCredentials) as IAbstractPaymentService; const deleted = await paymentInstance.deletePayment(paymentId); return deleted; diff --git a/packages/lib/payment/handlePayment.ts b/packages/lib/payment/handlePayment.ts index 92e81fd692ee46..ef54e30cbda2fb 100644 --- a/packages/lib/payment/handlePayment.ts +++ b/packages/lib/payment/handlePayment.ts @@ -5,15 +5,10 @@ import type { EventTypeAppsList } from "@calcom/app-store/utils"; import type { CompleteEventType } from "@calcom/prisma/zod"; import { eventTypeAppMetadataOptionalSchema } from "@calcom/prisma/zod-utils"; import type { CalendarEvent } from "@calcom/types/Calendar"; -import type { IAbstractPaymentService, PaymentApp } from "@calcom/types/PaymentService"; +import type { IAbstractPaymentService } from "@calcom/types/PaymentService"; -const isPaymentApp = (x: unknown): x is PaymentApp => - !!x && - typeof x === "object" && - "lib" in x && - typeof x.lib === "object" && - !!x.lib && - "PaymentService" in x.lib; +const isPaymentService = (x: unknown): x is { PaymentService: any } => + !!x && typeof x === "object" && "PaymentService" in x && typeof x.PaymentService === "function"; const isKeyOf = (obj: T, key: unknown): key is keyof T => typeof key === "string" && key in obj; @@ -59,12 +54,12 @@ const handlePayment = async ({ return null; } - const paymentApp = await paymentAppImportFn; - if (!isPaymentApp(paymentApp)) { - console.warn(`payment App service of type ${paymentApp} is not implemented`); + const paymentAppModule = await paymentAppImportFn; + if (!isPaymentService(paymentAppModule)) { + console.warn(`payment App service not found for key: ${key}`); return null; } - const PaymentService = paymentApp.lib.PaymentService; + const PaymentService = paymentAppModule.PaymentService; const paymentInstance = new PaymentService(paymentAppCredentials) as IAbstractPaymentService; const apps = eventTypeAppMetadataOptionalSchema.parse(selectedEventType?.metadata?.apps); diff --git a/packages/lib/payment/handlePaymentRefund.ts b/packages/lib/payment/handlePaymentRefund.ts index 17fb1896fb155c..4e25bf4abcfc29 100644 --- a/packages/lib/payment/handlePaymentRefund.ts +++ b/packages/lib/payment/handlePaymentRefund.ts @@ -22,13 +22,12 @@ const handlePaymentRefund = async ( return false; } - const paymentApp = await paymentAppImportFn; - if (!paymentApp?.lib?.PaymentService) { - console.warn(`payment App service of type ${paymentApp} is not implemented`); + const paymentAppModule = await paymentAppImportFn; + if (!paymentAppModule?.PaymentService) { + console.warn(`payment App service not found for key: ${key}`); return false; } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const PaymentService = paymentApp.lib.PaymentService as any; + const PaymentService = paymentAppModule.PaymentService; const paymentInstance = new PaymentService(paymentAppCredentials) as IAbstractPaymentService; const refund = await paymentInstance.refund(paymentId); return refund; diff --git a/packages/trpc/server/routers/viewer/payments.tsx b/packages/trpc/server/routers/viewer/payments.tsx index 9c5a356dc69f92..aec5c752213453 100644 --- a/packages/trpc/server/routers/viewer/payments.tsx +++ b/packages/trpc/server/routers/viewer/payments.tsx @@ -114,11 +114,11 @@ export const paymentsRouter = router({ } const paymentApp = await paymentAppImportFn; - if (!(paymentApp && paymentApp.lib && "lib" in paymentApp && "PaymentService" in paymentApp.lib)) { + if (!(paymentApp && "PaymentService" in paymentApp && paymentApp?.PaymentService)) { throw new TRPCError({ code: "BAD_REQUEST", message: "Payment service not found" }); } - const PaymentService = paymentApp.lib.PaymentService; + const PaymentService = paymentApp.PaymentService; const paymentInstance = new PaymentService(paymentCredential); try { diff --git a/packages/trpc/server/routers/viewer/payments/chargeCard.handler.ts b/packages/trpc/server/routers/viewer/payments/chargeCard.handler.ts index bd47d7d918cf8e..36582ff8f03e97 100644 --- a/packages/trpc/server/routers/viewer/payments/chargeCard.handler.ts +++ b/packages/trpc/server/routers/viewer/payments/chargeCard.handler.ts @@ -132,11 +132,10 @@ export const chargeCardHandler = async ({ ctx, input }: ChargeCardHandlerOptions } const paymentApp = await paymentAppImportFn; - if (!paymentApp?.lib?.PaymentService) { + if (!paymentApp?.PaymentService) { throw new TRPCError({ code: "BAD_REQUEST", message: "Payment service not found" }); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const PaymentService = paymentApp.lib.PaymentService as any; + const PaymentService = paymentApp.PaymentService; const paymentInstance = new PaymentService(paymentCredential) as IAbstractPaymentService; try { diff --git a/setupVitest.ts b/setupVitest.ts index 9edd87d4923fb6..91d20c1c8c016a 100644 --- a/setupVitest.ts +++ b/setupVitest.ts @@ -127,48 +127,36 @@ class MockPaymentService { } vi.mock("@calcom/app-store/stripepayment/index", () => ({ - lib: { - PaymentService: MockPaymentService, - }, + PaymentService: MockPaymentService, })); vi.mock("@calcom/app-store/paypal/index", () => ({ - lib: { - PaymentService: MockPaymentService, - }, + PaymentService: MockPaymentService, })); vi.mock("@calcom/app-store/alby/index", () => ({ - lib: { - PaymentService: MockPaymentService, - }, + PaymentService: MockPaymentService, })); vi.mock("@calcom/app-store/hitpay/index", () => ({ - lib: { - PaymentService: MockPaymentService, - }, + PaymentService: MockPaymentService, })); vi.mock("@calcom/app-store/btcpayserver/index", () => ({ - lib: { - PaymentService: MockPaymentService, - }, + PaymentService: MockPaymentService, })); vi.mock("@calcom/app-store/mock-payment-app/index", () => ({ - lib: { - PaymentService: MockPaymentService, - }, + PaymentService: MockPaymentService, })); vi.mock("@calcom/app-store/payment.services.generated", () => ({ PaymentServiceMap: { - stripepayment: Promise.resolve({ lib: { PaymentService: MockPaymentService } }), - paypal: Promise.resolve({ lib: { PaymentService: MockPaymentService } }), - alby: Promise.resolve({ lib: { PaymentService: MockPaymentService } }), - hitpay: Promise.resolve({ lib: { PaymentService: MockPaymentService } }), - btcpayserver: Promise.resolve({ lib: { PaymentService: MockPaymentService } }), - "mock-payment-app": Promise.resolve({ lib: { PaymentService: MockPaymentService } }), + stripepayment: Promise.resolve({ PaymentService: MockPaymentService }), + paypal: Promise.resolve({ PaymentService: MockPaymentService }), + alby: Promise.resolve({ PaymentService: MockPaymentService }), + hitpay: Promise.resolve({ PaymentService: MockPaymentService }), + btcpayserver: Promise.resolve({ PaymentService: MockPaymentService }), + "mock-payment-app": Promise.resolve({ PaymentService: MockPaymentService }), }, })); From 825f1277097623a2bcb326f81e7cb3822b9f00bd Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 28 Aug 2025 11:14:34 +0000 Subject: [PATCH 7/8] fix: revert chargeCard booking.id parameter additions - Remove booking.id parameter from chargeCard calls in chargeCard.handler.ts and payments.tsx - Addresses GitHub feedback to investigate chargeCard signature changes in separate PR - Keeps all other direct PaymentService import refactor changes intact Co-Authored-By: keith@cal.com --- packages/trpc/server/routers/viewer/payments.tsx | 2 +- .../trpc/server/routers/viewer/payments/chargeCard.handler.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/trpc/server/routers/viewer/payments.tsx b/packages/trpc/server/routers/viewer/payments.tsx index aec5c752213453..f9e2be37eb3f7b 100644 --- a/packages/trpc/server/routers/viewer/payments.tsx +++ b/packages/trpc/server/routers/viewer/payments.tsx @@ -122,7 +122,7 @@ export const paymentsRouter = router({ const paymentInstance = new PaymentService(paymentCredential); try { - const paymentData = await paymentInstance.chargeCard(payment, booking.id); + const paymentData = await paymentInstance.chargeCard(payment); if (!paymentData) { throw new TRPCError({ code: "NOT_FOUND", message: `Could not generate payment data` }); diff --git a/packages/trpc/server/routers/viewer/payments/chargeCard.handler.ts b/packages/trpc/server/routers/viewer/payments/chargeCard.handler.ts index 36582ff8f03e97..792cba56523a97 100644 --- a/packages/trpc/server/routers/viewer/payments/chargeCard.handler.ts +++ b/packages/trpc/server/routers/viewer/payments/chargeCard.handler.ts @@ -139,7 +139,7 @@ export const chargeCardHandler = async ({ ctx, input }: ChargeCardHandlerOptions const paymentInstance = new PaymentService(paymentCredential) as IAbstractPaymentService; try { - const paymentData = await paymentInstance.chargeCard(booking.payment[0], booking.id); + const paymentData = await paymentInstance.chargeCard(booking.payment[0]); if (!paymentData) { throw new TRPCError({ code: "NOT_FOUND", message: `Could not generate payment data` }); From 3411778a894b8210c1c155b22ba0e7d52ba3b007 Mon Sep 17 00:00:00 2001 From: Keith Williams Date: Thu, 28 Aug 2025 09:28:08 -0300 Subject: [PATCH 8/8] Put back the bookingId on the chargeCard calls --- packages/trpc/server/routers/viewer/payments.tsx | 2 +- .../trpc/server/routers/viewer/payments/chargeCard.handler.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/trpc/server/routers/viewer/payments.tsx b/packages/trpc/server/routers/viewer/payments.tsx index f9e2be37eb3f7b..aec5c752213453 100644 --- a/packages/trpc/server/routers/viewer/payments.tsx +++ b/packages/trpc/server/routers/viewer/payments.tsx @@ -122,7 +122,7 @@ export const paymentsRouter = router({ const paymentInstance = new PaymentService(paymentCredential); try { - const paymentData = await paymentInstance.chargeCard(payment); + const paymentData = await paymentInstance.chargeCard(payment, booking.id); if (!paymentData) { throw new TRPCError({ code: "NOT_FOUND", message: `Could not generate payment data` }); diff --git a/packages/trpc/server/routers/viewer/payments/chargeCard.handler.ts b/packages/trpc/server/routers/viewer/payments/chargeCard.handler.ts index 792cba56523a97..36582ff8f03e97 100644 --- a/packages/trpc/server/routers/viewer/payments/chargeCard.handler.ts +++ b/packages/trpc/server/routers/viewer/payments/chargeCard.handler.ts @@ -139,7 +139,7 @@ export const chargeCardHandler = async ({ ctx, input }: ChargeCardHandlerOptions const paymentInstance = new PaymentService(paymentCredential) as IAbstractPaymentService; try { - const paymentData = await paymentInstance.chargeCard(booking.payment[0]); + const paymentData = await paymentInstance.chargeCard(booking.payment[0], booking.id); if (!paymentData) { throw new TRPCError({ code: "NOT_FOUND", message: `Could not generate payment data` });