Files
calendar/packages/features/bookings/lib/handlePayment.test.ts
T
+2
Anwar SadathGitHubcoderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>Omar Lópezamritcoderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>Kartik SainiAnik Dhabal BabuDevanshu SharmaBenny JooPeer RichelsenVolnei Munhoz
55e6682832 feat: Custom Fields OR Add-Ons with a fee associated (#19997)
* 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>
2025-09-16 21:33:23 +09:00

301 lines
7.4 KiB
TypeScript

import { describe, expect, it, vi, beforeEach } from "vitest";
import { handlePayment } from "./handlePayment";
vi.mock("@calcom/app-store/zod-utils", () => ({
eventTypeMetaDataSchemaWithTypedApps: {
parse: vi.fn((data) => data),
},
}));
vi.mock("@calcom/app-store/payment.services.generated", () => ({
PaymentServiceMap: {
stripepayment: Promise.resolve({
PaymentService: class MockPaymentService {
async create(paymentData: { amount: number; currency: string }) {
return {
...paymentData,
id: "mock-payment-id",
externalId: "mock-external-id",
success: true,
};
}
async collectCard(paymentData: { amount: number; currency: string }) {
return {
...paymentData,
id: "mock-payment-id",
externalId: "mock-external-id",
success: true,
};
}
async afterPayment() {
return true;
}
},
}),
},
}));
describe("handlePayment", () => {
const mockBooking = {
id: 123,
userId: 456,
startTime: { toISOString: () => "2025-03-12T10:00:00Z" },
uid: "test-uid",
user: {
email: "user@example.com",
name: "Test User",
timeZone: "UTC",
username: "testuser",
},
};
const mockEventType = {
title: "Test Event",
metadata: {
apps: {
stripe: {
enabled: true,
price: 1000,
currency: "USD",
paymentOption: "ON_BOOKING",
},
},
},
};
const mockPaymentCredentials = {
key: {},
appId: "stripe" as const,
app: {
dirName: "stripepayment",
categories: ["payment"],
},
};
const mockEvent = {
type: "testEvent",
title: "Test Event",
startTime: "2025-03-12T10:00:00Z",
endTime: "2025-03-12T11:00:00Z",
organizer: {
name: "Test Organizer",
email: "organizer@example.com",
timeZone: "UTC",
language: { locale: "en" },
},
attendees: [],
responses: {},
};
beforeEach(() => {
vi.clearAllMocks();
});
it("should handle payment on dry run", async () => {
const result = await handlePayment({
evt: mockEvent,
selectedEventType: mockEventType,
paymentAppCredentials: mockPaymentCredentials,
booking: mockBooking,
bookerName: "John Doe",
bookerEmail: "john@example.com",
bookingFields: [],
isDryRun: true,
});
expect(result).toBeNull();
});
it("should calculate total amount with no addons", async () => {
const result = await handlePayment({
evt: mockEvent,
selectedEventType: mockEventType,
paymentAppCredentials: mockPaymentCredentials,
booking: mockBooking,
bookerName: "John Doe",
bookerEmail: "john@example.com",
bookingFields: [],
});
expect(result?.amount).toBe(1000);
expect(result?.currency).toBe("USD");
});
it("should calculate total amount with number field addon", async () => {
const mockEventWithResponse = {
...mockEvent,
responses: {
quantity: { value: "2" },
},
};
const bookingFields = [
{
name: "quantity",
type: "number" as const,
price: 5,
required: false,
},
];
const result = await handlePayment({
evt: mockEventWithResponse,
selectedEventType: mockEventType,
paymentAppCredentials: mockPaymentCredentials,
booking: mockBooking,
bookerName: "John Doe",
bookerEmail: "john@example.com",
bookingFields,
});
expect(result?.amount).toBe(2000); // Base 1000 cents + (2 * $5 * 100 cents)
});
it("should calculate total amount with boolean field addon", async () => {
const mockEventWithResponse = {
...mockEvent,
responses: {
extraService: { value: true },
},
};
const bookingFields = [
{
name: "extraService",
type: "boolean" as const,
price: 15,
required: false,
},
];
const result = await handlePayment({
evt: mockEventWithResponse,
selectedEventType: mockEventType,
paymentAppCredentials: mockPaymentCredentials,
booking: mockBooking,
bookerName: "John Doe",
bookerEmail: "john@example.com",
bookingFields,
});
expect(result?.amount).toBe(2500); // Base 1000 cents + ($15 * 100 cents)
});
it("should calculate total amount with select field addon", async () => {
const mockEventWithResponse = {
...mockEvent,
responses: {
package: { value: "premium" },
},
};
const bookingFields = [
{
name: "package",
type: "select" as const,
required: false,
options: [
{ value: "basic", label: "Basic Package", price: 10 },
{ value: "premium", label: "Premium Package", price: 20 },
],
},
];
const result = await handlePayment({
evt: mockEventWithResponse,
selectedEventType: mockEventType,
paymentAppCredentials: mockPaymentCredentials,
booking: mockBooking,
bookerName: "John Doe",
bookerEmail: "john@example.com",
bookingFields,
});
expect(result?.amount).toBe(3000); // Base 1000 cents + ($20 * 100 cents)
});
it("should calculate total amount with multiple addons", async () => {
const mockEventWithResponse = {
...mockEvent,
responses: {
quantity: { value: "2" },
extraService: { value: true },
package: { value: "premium" },
},
};
const bookingFields = [
{
name: "quantity",
type: "number" as const,
price: 5,
required: false,
},
{
name: "extraService",
type: "boolean" as const,
price: 15,
required: false,
},
{
name: "package",
type: "select" as const,
required: false,
options: [
{ value: "basic", label: "Basic Package", price: 10 },
{ value: "premium", label: "Premium Package", price: 20 },
],
},
];
const result = await handlePayment({
evt: mockEventWithResponse,
selectedEventType: mockEventType,
paymentAppCredentials: mockPaymentCredentials,
booking: mockBooking,
bookerName: "John Doe",
bookerEmail: "john@example.com",
bookingFields,
});
expect(result?.amount).toBe(5500); // Base 1000 cents + (2 * $5 * 100 cents) + ($15 * 100 cents) + ($20 * 100 cents)
});
it("should calculate total amount with radio field addon using locale", async () => {
const mockEventWithResponse = {
...mockEvent,
responses: {
mealChoice: { value: "premium ($20.00)" }, // Formatted with price
},
};
const bookingFields = [
{
name: "mealChoice",
type: "radio" as const,
required: false,
options: [
{ value: "basic", label: "Basic Meal", price: 10 },
{ value: "premium", label: "Premium Meal", price: 20 },
],
},
];
const result = await handlePayment({
evt: mockEventWithResponse,
selectedEventType: mockEventType,
paymentAppCredentials: mockPaymentCredentials,
booking: mockBooking,
bookerName: "John Doe",
bookerEmail: "john@example.com",
bookingFields,
locale: "en",
});
expect(result?.amount).toBe(3000); // Base 1000 cents + ($20 * 100 cents)
});
});