Files
calendar/packages/features/bookings/lib/payment/processNoShowFeeOnCancellation.test.ts
Benny JooGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
ab21c7f805 refactor: Cal.diy (#28903)
* feat: Cal.diy — community-driven MIT-licensed fork of Cal.com

This squashed commit contains all Cal.diy changes applied on top of calcom/cal.com main:

- Rebrand Cal.com to Cal.diy across the entire codebase
- Remove Enterprise Edition (EE) features, license checks, and AGPL restrictions
- Switch license from AGPL-3.0 to MIT
- Remove docs/ directory (migrated to Nextra at cal.diy)
- Remove dead code: org tests, EE tips, platform nav, premium username, SAML/SSO, etc.
- Clean up .env.example for self-hosted Cal.diy
- Update Docker image references to calcom/cal.diy
- Update README, CONTRIBUTING.md, and issue templates for Cal.diy community fork
- Add PR welcome bot for Cal.diy contributors
- Fix API v2 breaking changes oasdiff ignore entries
- Replace Blacksmith CI runners with default GitHub Actions

3893 files changed, 20789 insertions(+), 411020 deletions(-)

Co-Authored-By: [email protected] <[email protected]>

* refactor: remove org-specific /organizations/:orgId endpoints from API v2 atoms controllers (#1701)

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix: revert Cal.diy Inc to Cal.com, Inc. in license files, copyright notices, and package metadata (#1702)

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* rip out org related comments in api v2

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-04-15 09:52:36 -03:00

512 lines
14 KiB
TypeScript

import type { Payment } from "@calcom/prisma/client";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { handleNoShowFee } from "./handleNoShowFee";
import { processNoShowFeeOnCancellation } from "./processNoShowFeeOnCancellation";
import { shouldChargeNoShowCancellationFee } from "./shouldChargeNoShowCancellationFee";
const { mockFindUniqueByUserIdAndTeamId, MockMembershipRepository } = vi.hoisted(() => {
const mockFindUniqueByUserIdAndTeamId = vi.fn();
class MockMembershipRepository {
findUniqueByUserIdAndTeamId = mockFindUniqueByUserIdAndTeamId;
}
return { mockFindUniqueByUserIdAndTeamId, MockMembershipRepository };
});
vi.mock("@calcom/features/membership/repositories/MembershipRepository", () => ({
MembershipRepository: MockMembershipRepository,
}));
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,
},
};
mockFindUniqueByUserIdAndTeamId.mockResolvedValue({
id: 1,
userId: 999,
teamId: 1,
role: "ADMIN",
accepted: true,
createdAt: new Date(),
updatedAt: new Date(),
customRoleId: null,
});
await processNoShowFeeOnCancellation({
booking: teamBooking,
payments: [mockHoldPayment],
cancelledByUserId: 999,
});
expect(mockFindUniqueByUserIdAndTeamId).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,
},
};
mockFindUniqueByUserIdAndTeamId.mockResolvedValue({
id: 1,
userId: 999,
teamId: 1,
role: "OWNER",
accepted: true,
createdAt: new Date(),
updatedAt: new Date(),
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,
},
};
mockFindUniqueByUserIdAndTeamId.mockResolvedValue({
id: 1,
userId: 999,
teamId: 1,
role: "MEMBER",
accepted: true,
createdAt: new Date(),
updatedAt: new Date(),
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(mockFindUniqueByUserIdAndTeamId).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,
},
};
mockFindUniqueByUserIdAndTeamId.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();
});
});
});