+2



![coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>](/assets/img/avatar_default.png)


![coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>](/assets/img/avatar_default.png)


Anwar Sadath
GitHub
coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Omar López
amrit
coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Kartik Saini
Anik Dhabal Babu
Devanshu Sharma
Benny Joo
Peer Richelsen
Volnei Munhoz
55e6682832
* feat: Addons to bookings * Add tests * Add tests * Fix price field not shown immediately after enabling payment for an event * Add support for all currencies in addons and fix type errors * Address review comments * Add tests to cover all input types * Address cubic-dev-ai comments * Remove openapi.json from git tracking * Revert changes * Revert unrelated linter/formatter changes * Address cubic-dev-ai comments * Addres cubic-dev-ai comment * Update to use test id instead of hardcoded label Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Address coderabbits review comments * Fix failing unit test * some fixes... * Fix tests * Fix import * Move handlePayment test file to appropriate location * Fix test --------- Co-authored-by: Omar López <zomars@me.com> Co-authored-by: amrit <iamamrit27@gmail.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Kartik Saini <41051387+kart1ka@users.noreply.github.com> Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com> Co-authored-by: Devanshu Sharma <devanshusharma658@gmail.com> Co-authored-by: Benny Joo <sldisek783@gmail.com> Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: Volnei Munhoz <volnei.munhoz@gmail.com>
201 lines
6.4 KiB
TypeScript
201 lines
6.4 KiB
TypeScript
import { PaymentServiceMap } from "@calcom/app-store/payment.services.generated";
|
|
import type { EventTypeAppsList } from "@calcom/app-store/utils";
|
|
import { eventTypeMetaDataSchemaWithTypedApps } from "@calcom/app-store/zod-utils";
|
|
import type { AppCategories, Prisma, EventType } from "@calcom/prisma/client";
|
|
import { convertToSmallestCurrencyUnit } from "@calcom/app-store/_utils/payments/currencyConversions";
|
|
import type { Fields } from "@calcom/features/bookings/lib/getBookingFields";
|
|
import { fieldTypesConfigMap } from "@calcom/features/form-builder/fieldTypes";
|
|
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 handlePayment = async ({
|
|
evt,
|
|
selectedEventType,
|
|
paymentAppCredentials,
|
|
booking,
|
|
bookerName,
|
|
bookerEmail,
|
|
bookerPhoneNumber,
|
|
isDryRun = false,
|
|
bookingFields,
|
|
locale = "en",
|
|
}: {
|
|
evt: CalendarEvent;
|
|
selectedEventType: Pick<EventType, "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;
|
|
bookingFields?: Fields;
|
|
locale?: string;
|
|
}) => {
|
|
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 = eventTypeMetaDataSchemaWithTypedApps.parse(selectedEventType?.metadata)?.apps;
|
|
|
|
const paymentOption = apps?.[paymentAppCredentials.appId].paymentOption || "ON_BOOKING";
|
|
const paymentCurrency = apps?.[paymentAppCredentials.appId].currency;
|
|
// Ensure we have a valid currency - fallback to USD if undefined
|
|
const currency = paymentCurrency || "USD";
|
|
|
|
let totalAmount = apps?.[paymentAppCredentials.appId].price || 0;
|
|
|
|
if ((bookingFields || [])?.length > 0) {
|
|
let addonsPrice = 0;
|
|
|
|
// Process each booking field
|
|
(bookingFields || []).forEach((field) => {
|
|
// Skip fields that don't support pricing
|
|
if (
|
|
!fieldTypesConfigMap[field.type]?.supportsPricing &&
|
|
!fieldTypesConfigMap[field.type]?.optionsSupportPricing
|
|
) {
|
|
return;
|
|
}
|
|
|
|
// Type assertion for price-related fields
|
|
const typedInput = field as { price?: number; options?: { value: string; price?: number }[] };
|
|
const response = evt.responses?.[field.name];
|
|
|
|
// Skip if no response for this field
|
|
if (!response) return;
|
|
|
|
switch (field.type) {
|
|
case "number": {
|
|
// Multiply the numeric value by the field's price
|
|
const parsedValue = Number((response.value ?? "").toString().trim());
|
|
const safeValue = Number.isFinite(parsedValue) ? parsedValue : 0;
|
|
addonsPrice += safeValue * (typedInput.price || 0);
|
|
break;
|
|
}
|
|
|
|
case "boolean": {
|
|
// Add price if boolean field is true
|
|
if (response.value) {
|
|
addonsPrice += typedInput.price || 0;
|
|
}
|
|
break;
|
|
}
|
|
|
|
case "select":
|
|
case "radio": {
|
|
// For select and radio, find the selected option and add its price
|
|
const selectedValue = response.value;
|
|
let selectedOption;
|
|
|
|
if (field.type === "radio") {
|
|
// For radio, the value coming is the label itself (formatted with price)
|
|
selectedOption = typedInput.options?.find((opt) => {
|
|
const formattedValue = `${opt.value} (${Intl.NumberFormat(locale, {
|
|
style: "currency",
|
|
currency: currency,
|
|
}).format(opt.price || 0)})`;
|
|
return formattedValue === selectedValue;
|
|
});
|
|
} else {
|
|
// For select, match by direct value
|
|
selectedOption = typedInput.options?.find((opt) => opt.value === selectedValue);
|
|
}
|
|
|
|
addonsPrice += selectedOption?.price || 0;
|
|
break;
|
|
}
|
|
|
|
case "checkbox":
|
|
case "multiselect": {
|
|
// For checkbox and multiselect, add prices of all selected options
|
|
const responseValue = response.value;
|
|
const selectedValues = Array.isArray(responseValue)
|
|
? responseValue
|
|
: responseValue
|
|
? [responseValue]
|
|
: [];
|
|
|
|
selectedValues.forEach((value) => {
|
|
const option = typedInput.options?.find((opt) => opt.value === value);
|
|
addonsPrice += option?.price || 0;
|
|
});
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
|
|
totalAmount += convertToSmallestCurrencyUnit(addonsPrice, currency);
|
|
}
|
|
|
|
const paymentPriceAndCurrency = {
|
|
amount: totalAmount,
|
|
currency: currency,
|
|
};
|
|
|
|
let paymentData;
|
|
if (paymentOption === "HOLD") {
|
|
paymentData = await paymentInstance.collectCard(
|
|
paymentPriceAndCurrency,
|
|
booking.id,
|
|
paymentOption,
|
|
bookerEmail,
|
|
bookerPhoneNumber
|
|
);
|
|
} else {
|
|
paymentData = await paymentInstance.create(
|
|
paymentPriceAndCurrency,
|
|
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 };
|