Files
calendar/packages/lib/payment/handlePayment.ts
T
Keith WilliamsGitHubkeith@cal.com <keithwillcode@gmail.com>Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
baaa04151d perf: optimize payment app imports to avoid loading entire app store (#23408)
* 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 <keithwillcode@gmail.com>

* Update build.ts

* 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 <keithwillcode@gmail.com>

* 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 <keithwillcode@gmail.com>

* 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 <keithwillcode@gmail.com>

* 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 <keithwillcode@gmail.com>

* 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 <keithwillcode@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-08-28 10:05:36 -03:00

111 lines
3.3 KiB
TypeScript

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 } from "@calcom/types/PaymentService";
const isPaymentService = (x: unknown): x is { PaymentService: any } =>
!!x && typeof x === "object" && "PaymentService" in x && typeof x.PaymentService === "function";
const isKeyOf = <T extends object>(obj: T, key: unknown): key is keyof T =>
typeof key === "string" && key in obj;
const handlePayment = async ({
evt,
selectedEventType,
paymentAppCredentials,
booking,
bookerName,
bookerEmail,
bookerPhoneNumber,
isDryRun = false,
}: {
evt: CalendarEvent;
selectedEventType: Pick<CompleteEventType, "metadata" | "title">;
paymentAppCredentials: {
key: Prisma.JsonValue;
appId: EventTypeAppsList;
app: {
dirName: string;
categories: AppCategories[];
} | null;
};
booking: {
user: { email: string | null; name: string | null; timeZone: string; username: string | null } | null;
id: number;
userId: number | null;
startTime: { toISOString: () => string };
uid: string;
};
bookerName: string;
bookerEmail: string;
bookerPhoneNumber?: string | null;
isDryRun?: boolean;
}) => {
if (isDryRun) return null;
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 null;
}
const paymentAppModule = await paymentAppImportFn;
if (!isPaymentService(paymentAppModule)) {
console.warn(`payment App service not found for key: ${key}`);
return null;
}
const PaymentService = paymentAppModule.PaymentService;
const paymentInstance = new PaymentService(paymentAppCredentials) as IAbstractPaymentService;
const apps = eventTypeAppMetadataOptionalSchema.parse(selectedEventType?.metadata?.apps);
const paymentOption = apps?.[paymentAppCredentials.appId].paymentOption || "ON_BOOKING";
let paymentData;
if (paymentOption === "HOLD") {
paymentData = await paymentInstance.collectCard(
{
amount: apps?.[paymentAppCredentials.appId].price,
currency: apps?.[paymentAppCredentials.appId].currency,
},
booking.id,
paymentOption,
bookerEmail,
bookerPhoneNumber
);
} else {
paymentData = await paymentInstance.create(
{
amount: apps?.[paymentAppCredentials.appId].price,
currency: apps?.[paymentAppCredentials.appId].currency,
},
booking.id,
booking.userId,
booking.user?.username ?? null,
bookerName,
paymentOption,
bookerEmail,
bookerPhoneNumber,
selectedEventType.title,
evt.title
);
}
if (!paymentData) {
console.error("Payment data is null");
throw new Error("Payment data is null");
}
try {
await paymentInstance.afterPayment(evt, booking, paymentData, selectedEventType?.metadata);
} catch (e) {
console.error(e);
}
return paymentData;
};
export { handlePayment };