Files
cal.diy/packages/app-store/paypal/lib/PaymentService.ts
T
Udit Takkar 371a0f7245 feat: booking errors logging (#12325)
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).
2023-11-15 12:52:19 -07:00

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;
}
}