Files
calendar/packages/features/bookings/lib/payment/processNoShowFeeOnCancellation.test.ts
Benny JooandGitHub ff38d6c7db refactor: Remove circular deps between @calcom/lib and @calcom/features [1] (#24399)
* add eslint config

* migrate autoLock to features

* migrate teamService to features

* migrate userCreationService

* migrate insights services to features

* migrate ProfileRepository

* update imports

* migrate filter segmen tests

* migrate filter segment repository

* migrate getBusyTimes

* migrate getLocaleFromRequest

* refactor csvUtils

* make filename clearer

* migrate getLuckyUser integration test

* migrate autoLock test to features

* wip

* refactors

* migrate useBookerUrl

* migrate more

* wip

* Migrate eventTypeRepository

* membership repository

* update imports

* update imports

* migrate

* move organization repository

* update imports

* update imports

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix tests

* fix type checks

* fix

* fix

* migrate

* update imports

* fix tests

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix
2025-10-13 12:01:02 -03:00

510 lines
14 KiB
TypeScript

import { describe, expect, it, vi, beforeEach } from "vitest";
import { MembershipRepository } from "@calcom/features/membership/repositories/MembershipRepository";
import type { Payment } from "@calcom/prisma/client";
import { handleNoShowFee } from "./handleNoShowFee";
import { processNoShowFeeOnCancellation } from "./processNoShowFeeOnCancellation";
import { shouldChargeNoShowCancellationFee } from "./shouldChargeNoShowCancellationFee";
vi.mock("@calcom/features/membership/repositories/MembershipRepository", () => ({
MembershipRepository: {
findUniqueByUserIdAndTeamId: vi.fn(),
},
}));
vi.mock("./handleNoShowFee", () => ({
handleNoShowFee: vi.fn(),
}));
vi.mock("./shouldChargeNoShowCancellationFee", () => ({
shouldChargeNoShowCancellationFee: vi.fn(),
}));
describe("processNoShowFeeOnCancellation", () => {
const mockBooking = {
id: 1,
uid: "booking-123",
title: "Test Meeting",
startTime: new Date("2024-09-01T10:00:00Z"),
endTime: new Date("2024-09-01T11:00:00Z"),
userPrimaryEmail: "[email protected]",
userId: 1,
user: {
email: "[email protected]",
name: "John Organizer",
locale: "en",
timeZone: "UTC",
},
eventType: {
title: "Test Event Type",
hideOrganizerEmail: false,
teamId: null,
metadata: {},
},
attendees: [
{
name: "Jane Attendee",
email: "[email protected]",
timeZone: "UTC",
locale: "en",
},
],
};
const mockHoldPayment: Payment = {
id: 1,
uid: "payment-123",
appId: "stripe",
bookingId: 1,
amount: 5000,
fee: 0,
currency: "USD",
success: false,
refunded: false,
data: {},
externalId: "ext_123",
paymentOption: "HOLD",
};
const mockSuccessfulPayment: Payment = {
...mockHoldPayment,
id: 2,
success: true,
paymentOption: "ON_BOOKING",
};
beforeEach(() => {
vi.clearAllMocks();
});
describe("successful scenarios", () => {
it("should successfully process no-show fee when conditions are met", async () => {
vi.mocked(shouldChargeNoShowCancellationFee).mockReturnValue(true);
vi.mocked(handleNoShowFee).mockResolvedValue({
id: 999,
uid: "payment-charged-123",
appId: "stripe",
bookingId: 1,
amount: 5000,
fee: 0,
currency: "USD",
success: true,
refunded: false,
data: {},
externalId: "ext_charged_123",
paymentOption: "HOLD",
});
await processNoShowFeeOnCancellation({
booking: mockBooking,
payments: [mockHoldPayment],
cancelledByUserId: 999,
});
expect(shouldChargeNoShowCancellationFee).toHaveBeenCalledWith({
booking: mockBooking,
eventTypeMetadata: {},
payment: mockHoldPayment,
});
expect(handleNoShowFee).toHaveBeenCalledWith({
booking: mockBooking,
payment: mockHoldPayment,
});
});
it("should process no-show fee when cancelled by attendee (no cancelledByUserId)", async () => {
vi.mocked(shouldChargeNoShowCancellationFee).mockReturnValue(true);
vi.mocked(handleNoShowFee).mockResolvedValue({
id: 999,
uid: "payment-charged-123",
appId: "stripe",
bookingId: 1,
amount: 5000,
fee: 0,
currency: "USD",
success: true,
refunded: false,
data: {},
externalId: "ext_charged_123",
paymentOption: "HOLD",
});
await processNoShowFeeOnCancellation({
booking: mockBooking,
payments: [mockHoldPayment],
});
expect(handleNoShowFee).toHaveBeenCalledWith({
booking: mockBooking,
payment: mockHoldPayment,
});
});
});
describe("skip scenarios - organizer cancellation", () => {
it("should skip no-show fee when cancelled by organizer", async () => {
await processNoShowFeeOnCancellation({
booking: mockBooking,
payments: [mockHoldPayment],
cancelledByUserId: 1,
});
expect(shouldChargeNoShowCancellationFee).not.toHaveBeenCalled();
expect(handleNoShowFee).not.toHaveBeenCalled();
});
});
describe("skip scenarios - team admin cancellation", () => {
it("should skip no-show fee when cancelled by team admin", async () => {
const teamBooking = {
...mockBooking,
eventType: {
...mockBooking.eventType,
teamId: 1,
},
};
vi.mocked(MembershipRepository.findUniqueByUserIdAndTeamId).mockResolvedValue({
id: 1,
userId: 999,
teamId: 1,
role: "ADMIN",
accepted: true,
createdAt: new Date(),
updatedAt: new Date(),
disableImpersonation: false,
customRoleId: null,
});
await processNoShowFeeOnCancellation({
booking: teamBooking,
payments: [mockHoldPayment],
cancelledByUserId: 999,
});
expect(MembershipRepository.findUniqueByUserIdAndTeamId).toHaveBeenCalledWith({
userId: 999,
teamId: 1,
});
expect(shouldChargeNoShowCancellationFee).not.toHaveBeenCalled();
expect(handleNoShowFee).not.toHaveBeenCalled();
});
it("should skip no-show fee when cancelled by team owner", async () => {
const teamBooking = {
...mockBooking,
eventType: {
...mockBooking.eventType,
teamId: 1,
},
};
vi.mocked(MembershipRepository.findUniqueByUserIdAndTeamId).mockResolvedValue({
id: 1,
userId: 999,
teamId: 1,
role: "OWNER",
accepted: true,
createdAt: new Date(),
updatedAt: new Date(),
disableImpersonation: false,
customRoleId: null,
});
await processNoShowFeeOnCancellation({
booking: teamBooking,
payments: [mockHoldPayment],
cancelledByUserId: 999,
});
expect(shouldChargeNoShowCancellationFee).not.toHaveBeenCalled();
expect(handleNoShowFee).not.toHaveBeenCalled();
});
it("should process no-show fee when cancelled by team member (not admin/owner)", async () => {
const teamBooking = {
...mockBooking,
eventType: {
...mockBooking.eventType,
teamId: 1,
},
};
vi.mocked(MembershipRepository.findUniqueByUserIdAndTeamId).mockResolvedValue({
id: 1,
userId: 999,
teamId: 1,
role: "MEMBER",
accepted: true,
createdAt: new Date(),
updatedAt: new Date(),
disableImpersonation: false,
customRoleId: null,
});
vi.mocked(shouldChargeNoShowCancellationFee).mockReturnValue(true);
vi.mocked(handleNoShowFee).mockResolvedValue({
id: 999,
uid: "payment-charged-123",
appId: "stripe",
bookingId: 1,
amount: 5000,
fee: 0,
currency: "USD",
success: true,
refunded: false,
data: {},
externalId: "ext_charged_123",
paymentOption: "HOLD",
});
await processNoShowFeeOnCancellation({
booking: teamBooking,
payments: [mockHoldPayment],
cancelledByUserId: 999,
});
expect(MembershipRepository.findUniqueByUserIdAndTeamId).toHaveBeenCalledWith({
userId: 999,
teamId: 1,
});
expect(shouldChargeNoShowCancellationFee).toHaveBeenCalled();
expect(handleNoShowFee).toHaveBeenCalled();
});
it("should process no-show fee when cancelled by non-team member", async () => {
const teamBooking = {
...mockBooking,
eventType: {
...mockBooking.eventType,
teamId: 1,
},
};
vi.mocked(MembershipRepository.findUniqueByUserIdAndTeamId).mockResolvedValue(null);
vi.mocked(shouldChargeNoShowCancellationFee).mockReturnValue(true);
vi.mocked(handleNoShowFee).mockResolvedValue({
id: 999,
uid: "payment-charged-123",
appId: "stripe",
bookingId: 1,
amount: 5000,
fee: 0,
currency: "USD",
success: true,
refunded: false,
data: {},
externalId: "ext_charged_123",
paymentOption: "HOLD",
});
await processNoShowFeeOnCancellation({
booking: teamBooking,
payments: [mockHoldPayment],
cancelledByUserId: 999,
});
expect(shouldChargeNoShowCancellationFee).toHaveBeenCalled();
expect(handleNoShowFee).toHaveBeenCalled();
});
});
describe("skip scenarios - payment conditions", () => {
it("should skip no-show fee when no HOLD payment found", async () => {
await processNoShowFeeOnCancellation({
booking: mockBooking,
payments: [mockSuccessfulPayment],
cancelledByUserId: 999,
});
expect(shouldChargeNoShowCancellationFee).not.toHaveBeenCalled();
expect(handleNoShowFee).not.toHaveBeenCalled();
});
it("should skip no-show fee when no payments provided", async () => {
await processNoShowFeeOnCancellation({
booking: mockBooking,
payments: [],
cancelledByUserId: 999,
});
expect(shouldChargeNoShowCancellationFee).not.toHaveBeenCalled();
expect(handleNoShowFee).not.toHaveBeenCalled();
});
it("should use first HOLD payment when multiple exist", async () => {
const secondHoldPayment: Payment = {
...mockHoldPayment,
id: 3,
uid: "payment-456",
};
vi.mocked(shouldChargeNoShowCancellationFee).mockReturnValue(true);
vi.mocked(handleNoShowFee).mockResolvedValue({
id: 999,
uid: "payment-charged-123",
appId: "stripe",
bookingId: 1,
amount: 5000,
fee: 0,
currency: "USD",
success: true,
refunded: false,
data: {},
externalId: "ext_charged_123",
paymentOption: "HOLD",
});
await processNoShowFeeOnCancellation({
booking: mockBooking,
payments: [mockSuccessfulPayment, mockHoldPayment, secondHoldPayment],
cancelledByUserId: 999,
});
expect(handleNoShowFee).toHaveBeenCalledWith({
booking: mockBooking,
payment: mockHoldPayment,
});
});
});
describe("skip scenarios - time threshold", () => {
it("should skip no-show fee when outside time threshold", async () => {
vi.mocked(shouldChargeNoShowCancellationFee).mockReturnValue(false);
await processNoShowFeeOnCancellation({
booking: mockBooking,
payments: [mockHoldPayment],
cancelledByUserId: 999,
});
expect(shouldChargeNoShowCancellationFee).toHaveBeenCalledWith({
booking: mockBooking,
eventTypeMetadata: {},
payment: mockHoldPayment,
});
expect(handleNoShowFee).not.toHaveBeenCalled();
});
});
describe("error scenarios", () => {
it("should throw error when handleNoShowFee fails", async () => {
const error = new Error("Payment processing failed");
vi.mocked(shouldChargeNoShowCancellationFee).mockReturnValue(true);
vi.mocked(handleNoShowFee).mockRejectedValue(error);
await expect(
processNoShowFeeOnCancellation({
booking: mockBooking,
payments: [mockHoldPayment],
cancelledByUserId: 999,
})
).rejects.toThrow("Failed to charge no-show fee with error Error: Payment processing failed");
});
});
describe("edge cases", () => {
it("should handle booking without event type", async () => {
const bookingWithoutEventType = {
...mockBooking,
eventType: null,
};
vi.mocked(shouldChargeNoShowCancellationFee).mockReturnValue(true);
vi.mocked(handleNoShowFee).mockResolvedValue({
id: 999,
uid: "payment-charged-123",
appId: "stripe",
bookingId: 1,
amount: 5000,
fee: 0,
currency: "USD",
success: true,
refunded: false,
data: {},
externalId: "ext_charged_123",
paymentOption: "HOLD",
});
await processNoShowFeeOnCancellation({
booking: bookingWithoutEventType,
payments: [mockHoldPayment],
cancelledByUserId: 999,
});
expect(shouldChargeNoShowCancellationFee).toHaveBeenCalledWith({
booking: bookingWithoutEventType,
eventTypeMetadata: {},
payment: mockHoldPayment,
});
expect(handleNoShowFee).toHaveBeenCalled();
});
it("should handle booking with complex event type metadata", async () => {
const bookingWithMetadata = {
...mockBooking,
eventType: {
...mockBooking.eventType,
metadata: {
apps: {
stripe: {
enabled: true,
price: 1000,
currency: "usd",
autoChargeNoShowFeeIfCancelled: true,
paymentOption: "HOLD",
autoChargeNoShowFeeTimeValue: 2,
autoChargeNoShowFeeTimeUnit: "hours",
},
},
},
},
};
vi.mocked(shouldChargeNoShowCancellationFee).mockReturnValue(true);
vi.mocked(handleNoShowFee).mockResolvedValue({
id: 999,
uid: "payment-charged-123",
appId: "stripe",
bookingId: 1,
amount: 5000,
fee: 0,
currency: "USD",
success: true,
refunded: false,
data: {},
externalId: "ext_charged_123",
paymentOption: "HOLD",
});
await processNoShowFeeOnCancellation({
booking: bookingWithMetadata,
payments: [mockHoldPayment],
cancelledByUserId: 999,
});
expect(shouldChargeNoShowCancellationFee).toHaveBeenCalledWith({
booking: bookingWithMetadata,
eventTypeMetadata: bookingWithMetadata.eventType.metadata,
payment: mockHoldPayment,
});
});
it("should handle successful HOLD payment (should still be processed)", async () => {
const successfulHoldPayment: Payment = {
...mockHoldPayment,
success: true,
};
await processNoShowFeeOnCancellation({
booking: mockBooking,
payments: [successfulHoldPayment],
cancelledByUserId: 999,
});
expect(shouldChargeNoShowCancellationFee).not.toHaveBeenCalled();
expect(handleNoShowFee).not.toHaveBeenCalled();
});
});
});