mirror of
https://github.com/hanzoai/calendar.git
synced 2026-10-30 14:46:43 +00:00
* 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>
512 lines
14 KiB
TypeScript
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();
|
|
});
|
|
});
|
|
});
|