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/packages/app-store-cli/src/build.ts b/packages/app-store-cli/src/build.ts index b43a3b819ec5c0..f6b8b988e16301 100644 --- a/packages/app-store-cli/src/build.ts +++ b/packages/app-store-cli/src/build.ts @@ -415,6 +415,24 @@ function generateFiles() { analyticsOutput.push(...analyticsServices); } + const paymentOutput = []; + const paymentServices = getExportedObject( + "PaymentServiceMap", + { + importConfig: { + fileToBeImported: "lib/PaymentService.ts", + importName: "PaymentService", + }, + lazyImport: true, + }, + (app: App) => { + const hasPaymentService = fs.existsSync(path.join(APP_STORE_PATH, app.path, "lib/PaymentService.ts")); + return hasPaymentService; + } + ); + + paymentOutput.push(...paymentServices); + const banner = `/** This file is autogenerated using the command \`yarn app-store:build --watch\`. Don't modify this file manually. @@ -430,6 +448,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")}`)); diff --git a/packages/app-store/payment.services.generated.ts b/packages/app-store/payment.services.generated.ts new file mode 100644 index 00000000000000..2e42d632580810 --- /dev/null +++ b/packages/app-store/payment.services.generated.ts @@ -0,0 +1,12 @@ +/** + This file is autogenerated using the command `yarn app-store:build --watch`. + Don't modify this file manually. +**/ +export const PaymentServiceMap = { + 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 09567d64283b7f..09fde58bc01639 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 && "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 20411bdf79c37b..8861125a4658a3 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,15 +15,19 @@ const deletePayment = async ( } | null; } ): Promise => { - const paymentApp = (await appStore[ - paymentAppCredentials?.app?.dirName as keyof typeof appStore - ]?.()) as PaymentApp; - if (!paymentApp?.lib?.PaymentService) { - console.warn(`payment App service of type ${paymentApp} is not implemented`); + 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; } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const PaymentService = paymentApp.lib.PaymentService as any; + + const paymentAppModule = await paymentAppImportFn; + if (!paymentAppModule?.PaymentService) { + console.warn(`payment App service not found for key: ${key}`); + return false; + } + 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 74e72dc5c3e570..ef54e30cbda2fb 100644 --- a/packages/lib/payment/handlePayment.ts +++ b/packages/lib/payment/handlePayment.ts @@ -1,18 +1,14 @@ 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"; 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; @@ -52,17 +48,18 @@ 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]?.(); - 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 a51a12423ed15f..4e25bf4abcfc29 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,15 +15,19 @@ const handlePaymentRefund = async ( } | null; } ) => { - const paymentApp = (await appStore[ - paymentAppCredentials?.app?.dirName as keyof typeof appStore - ]?.()) as PaymentApp; - if (!paymentApp?.lib?.PaymentService) { - console.warn(`payment App service of type ${paymentApp} is not implemented`); + 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; } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const PaymentService = paymentApp.lib.PaymentService as any; + + const paymentAppModule = await paymentAppImportFn; + if (!paymentAppModule?.PaymentService) { + console.warn(`payment App service not found for key: ${key}`); + return false; + } + 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 1485cbc6684543..aec5c752213453 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,19 +107,22 @@ 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" }); + } - if (!(paymentApp && paymentApp.lib && "lib" in paymentApp && "PaymentService" in paymentApp.lib)) { + const paymentApp = await paymentAppImportFn; + 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 { - 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..36582ff8f03e97 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,19 +125,21 @@ 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" }); + } - if (!paymentApp?.lib?.PaymentService) { + const paymentApp = await paymentAppImportFn; + 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 { - 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` }); diff --git a/setupVitest.ts b/setupVitest.ts index 96f7aefab3f1ac..91d20c1c8c016a 100644 --- a/setupVitest.ts +++ b/setupVitest.ts @@ -2,6 +2,7 @@ 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"; global.ResizeObserver = ResizeObserver; const fetchMocker = createFetchMock(vi); @@ -53,3 +54,109 @@ 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 { default: prismaMock } = await import("./tests/libs/__mocks__/prisma"); + 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", () => ({ + PaymentService: MockPaymentService, +})); + +vi.mock("@calcom/app-store/paypal/index", () => ({ + PaymentService: MockPaymentService, +})); + +vi.mock("@calcom/app-store/alby/index", () => ({ + PaymentService: MockPaymentService, +})); + +vi.mock("@calcom/app-store/hitpay/index", () => ({ + PaymentService: MockPaymentService, +})); + +vi.mock("@calcom/app-store/btcpayserver/index", () => ({ + PaymentService: MockPaymentService, +})); + +vi.mock("@calcom/app-store/mock-payment-app/index", () => ({ + PaymentService: MockPaymentService, +})); + +vi.mock("@calcom/app-store/payment.services.generated", () => ({ + PaymentServiceMap: { + 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 }), + }, +})); 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", },