Fixes: https://github.com/calcom/cal.com/issues/12297 Fixes https://github.com/calcom/cal.com/issues/11234 - Displaying error message and X-Vercel-Id( Unique Request Id ) to user on book event form - Improve error logging - Add Error codes Few things to discuss 1) How to handle calendar integration failures ? Currently if for example google integration is broken and someone is trying to book that person then we log the error but don't inform the user that the google calendar is broken and the meeting goes through. Should I throw error when integration is broken ? <img width="758" alt="Screenshot 2023-11-12 at 12 52 36 AM" src="https://github.com/calcom/cal.com/assets/53316345/c4d921c4-9c8a-4b9b-82a2-bbe0fdbcb3d4"> 2) How to handle conferencing app failures? We just default to Cal Video as location if we are unable to generated conferencing url and log the error and not inform the user(organizer).
209 lines
5.9 KiB
TypeScript
209 lines
5.9 KiB
TypeScript
import type { Booking, Payment, PaymentOption, Prisma } from "@prisma/client";
|
|
import { v4 as uuidv4 } from "uuid";
|
|
import z from "zod";
|
|
|
|
import Paypal from "@calcom/app-store/paypal/lib/Paypal";
|
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
|
import { ErrorCode } from "@calcom/lib/errorCodes";
|
|
import logger from "@calcom/lib/logger";
|
|
import prisma from "@calcom/prisma";
|
|
import type { CalendarEvent } from "@calcom/types/Calendar";
|
|
import type { IAbstractPaymentService } from "@calcom/types/PaymentService";
|
|
|
|
import { paymentOptionEnum } from "../zod";
|
|
|
|
const log = logger.getSubLogger({ prefix: ["payment-service:paypal"] });
|
|
|
|
export const paypalCredentialKeysSchema = z.object({
|
|
client_id: z.string(),
|
|
secret_key: z.string(),
|
|
webhook_id: z.string(),
|
|
});
|
|
|
|
export class PaymentService implements IAbstractPaymentService {
|
|
private credentials: z.infer<typeof paypalCredentialKeysSchema> | null;
|
|
|
|
constructor(credentials: { key: Prisma.JsonValue }) {
|
|
const keyParsing = paypalCredentialKeysSchema.safeParse(credentials.key);
|
|
if (keyParsing.success) {
|
|
this.credentials = keyParsing.data;
|
|
} else {
|
|
this.credentials = null;
|
|
}
|
|
}
|
|
|
|
async create(
|
|
payment: Pick<Prisma.PaymentUncheckedCreateInput, "amount" | "currency">,
|
|
bookingId: Booking["id"]
|
|
) {
|
|
try {
|
|
const booking = await prisma.booking.findFirst({
|
|
select: {
|
|
uid: true,
|
|
title: true,
|
|
},
|
|
where: {
|
|
id: bookingId,
|
|
},
|
|
});
|
|
if (!booking || !this.credentials) {
|
|
throw new Error();
|
|
}
|
|
|
|
const uid = uuidv4();
|
|
|
|
const paypalClient = new Paypal({
|
|
clientId: this.credentials.client_id,
|
|
secretKey: this.credentials.secret_key,
|
|
});
|
|
const orderResult = await paypalClient.createOrder({
|
|
referenceId: uid,
|
|
amount: payment.amount,
|
|
currency: payment.currency,
|
|
returnUrl: `${WEBAPP_URL}/api/integrations/paypal/capture?bookingUid=${booking.uid}`,
|
|
cancelUrl: `${WEBAPP_URL}/payment/${uid}`,
|
|
});
|
|
const paymentData = await prisma.payment.create({
|
|
data: {
|
|
uid,
|
|
app: {
|
|
connect: {
|
|
slug: "paypal",
|
|
},
|
|
},
|
|
booking: {
|
|
connect: {
|
|
id: bookingId,
|
|
},
|
|
},
|
|
amount: payment.amount,
|
|
externalId: orderResult.id,
|
|
currency: payment.currency,
|
|
data: Object.assign({}, { order: orderResult }) as unknown as Prisma.InputJsonValue,
|
|
fee: 0,
|
|
refunded: false,
|
|
success: false,
|
|
},
|
|
});
|
|
|
|
if (!paymentData) {
|
|
throw new Error();
|
|
}
|
|
return paymentData;
|
|
} catch (error) {
|
|
log.error("Paypal: Payment could not be created for bookingId", bookingId);
|
|
throw new Error(ErrorCode.PaymentCreationFailure);
|
|
}
|
|
}
|
|
async update(): Promise<Payment> {
|
|
throw new Error("Method not implemented.");
|
|
}
|
|
async refund(): Promise<Payment> {
|
|
throw new Error("Method not implemented.");
|
|
}
|
|
|
|
async collectCard(
|
|
payment: Pick<Prisma.PaymentUncheckedCreateInput, "amount" | "currency">,
|
|
bookingId: number,
|
|
_bookerEmail: string,
|
|
paymentOption: PaymentOption
|
|
): Promise<Payment> {
|
|
// Ensure that the payment service can support the passed payment option
|
|
if (paymentOptionEnum.parse(paymentOption) !== "HOLD") {
|
|
throw new Error("Payment option is not compatible with create method");
|
|
}
|
|
try {
|
|
const booking = await prisma.booking.findFirst({
|
|
select: {
|
|
uid: true,
|
|
title: true,
|
|
},
|
|
where: {
|
|
id: bookingId,
|
|
},
|
|
});
|
|
if (!booking || !this.credentials) {
|
|
throw new Error();
|
|
}
|
|
|
|
const uid = uuidv4();
|
|
|
|
const paypalClient = new Paypal({
|
|
clientId: this.credentials.client_id,
|
|
secretKey: this.credentials.secret_key,
|
|
});
|
|
const preference = await paypalClient.createOrder({
|
|
referenceId: uid,
|
|
amount: payment.amount,
|
|
currency: payment.currency,
|
|
returnUrl: `${WEBAPP_URL}/booking/${booking.uid}`,
|
|
cancelUrl: `${WEBAPP_URL}/payment/${uid}`,
|
|
intent: "AUTHORIZE",
|
|
});
|
|
|
|
const paymentData = await prisma.payment.create({
|
|
data: {
|
|
uid,
|
|
app: {
|
|
connect: {
|
|
slug: "paypal",
|
|
},
|
|
},
|
|
booking: {
|
|
connect: {
|
|
id: bookingId,
|
|
},
|
|
},
|
|
amount: payment.amount,
|
|
currency: payment.currency,
|
|
data: Object.assign({}, preference) as unknown as Prisma.InputJsonValue,
|
|
fee: 0,
|
|
refunded: false,
|
|
success: false,
|
|
paymentOption: paymentOption || "ON_BOOKING",
|
|
externalId: preference?.id,
|
|
},
|
|
});
|
|
|
|
if (!paymentData) {
|
|
throw new Error();
|
|
}
|
|
return paymentData;
|
|
} catch (error) {
|
|
log.error("Paypal: Payment method could not be collected for bookingId", bookingId);
|
|
throw new Error("Paypal: Payment method could not be collected");
|
|
}
|
|
}
|
|
chargeCard(
|
|
payment: Pick<Prisma.PaymentUncheckedCreateInput, "amount" | "currency">,
|
|
bookingId: number
|
|
): Promise<Payment> {
|
|
throw new Error("Method not implemented.");
|
|
}
|
|
getPaymentPaidStatus(): Promise<string> {
|
|
throw new Error("Method not implemented.");
|
|
}
|
|
getPaymentDetails(): Promise<Payment> {
|
|
throw new Error("Method not implemented.");
|
|
}
|
|
afterPayment(
|
|
event: CalendarEvent,
|
|
booking: {
|
|
user: { email: string | null; name: string | null; timeZone: string } | null;
|
|
id: number;
|
|
startTime: { toISOString: () => string };
|
|
uid: string;
|
|
},
|
|
paymentData: Payment
|
|
): Promise<void> {
|
|
return Promise.resolve();
|
|
}
|
|
deletePayment(paymentId: number): Promise<boolean> {
|
|
return Promise.resolve(false);
|
|
}
|
|
|
|
isSetupAlready(): boolean {
|
|
return !!this.credentials;
|
|
}
|
|
}
|