-
Notifications
You must be signed in to change notification settings - Fork 12k
feat: add time-based cancellation fees for no-show fee events #23595
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
alishaz-polymath
merged 43 commits into
main
from
devin/1757001996-no-show-cancellation-fees
Sep 5, 2025
Merged
Changes from all commits
Commits
Show all changes
43 commits
Select commit
Hold shift + click to select a range
c05cb75
feat: add time-based cancellation fees for no-show fee events
devin-ai-integration[bot] 4bd2585
fix: replace any type with proper type guards for metadata
devin-ai-integration[bot] cf23e6b
fix: handle JsonValue type compatibility in shouldChargeCancellationFee
devin-ai-integration[bot] 7c3d9c6
Refactor Devin changes to Stripe options
joeauyeung 0be2fda
Undo Devin changes to advanced tab
joeauyeung 3c18561
Add translations
joeauyeung defe6c2
Pass props to CancelBooking
joeauyeung 5d9fcef
Display no show fee charge for attendee
joeauyeung c5b8471
WIP
joeauyeung 004df7f
Anstract shouldChargeNoSHowCancellationFee
joeauyeung 6b85588
Abstract `handleNoShowFee`
joeauyeung 7b23906
Add to
joeauyeung 70392ae
Refactor `chargeCard.handler`
joeauyeung f1b1d23
Remove Devin code
joeauyeung a91bb71
Type fix in `shouldChargeNoShowCancellationFee`
joeauyeung 0194d6d
Create `processNoSHowFeeOnCancellation`
joeauyeung 4d11139
Process no show fee on cancellation
joeauyeung b20e678
Type fix
joeauyeung 2522dc0
Skip processing no show fee if organizer or admin is cancelling
joeauyeung 7fd6ead
Add translation
joeauyeung 1706303
Dynamically get and in
joeauyeung c915116
Remove unused translations
joeauyeung dde41b9
Undo dev change
joeauyeung d3b0c85
Merge branch 'main' into devin/1757001996-no-show-cancellation-fees
joeauyeung 4dbe76f
Refactor logic
joeauyeung 8670a9a
Type fix
joeauyeung eba5f77
Merge branch 'main' into devin/1757001996-no-show-cancellation-fees
alishaz-polymath a2851eb
remove any
alishaz-polymath fc28e29
revert WEBAPP_URL_FOR_OAUTH
alishaz-polymath 78e8507
Clean up console.log remnants
alishaz-polymath 5cda957
test: add comprehensive tests for time-based cancellation fees
devin-ai-integration[bot] 6e6b2a8
test: add comprehensive tests for time-based cancellation fees
devin-ai-integration[bot] 3a40b01
test: add comprehensive tests for time-based cancellation fees
devin-ai-integration[bot] 7cd8b47
test: add comprehensive unit and E2E tests for time-based cancellatio…
devin-ai-integration[bot] 2cb59da
Revert dev change
joeauyeung fb46587
Add missing translation
joeauyeung 236b461
Remove Devin code
joeauyeung 2fa873d
Revert Devin changes
joeauyeung cad3731
test: fix attendee cancellation test logic and ensure comprehensive t…
devin-ai-integration[bot] 8c760ac
Fix tests
joeauyeung 5c15052
Merge branch 'main' into devin/1757001996-no-show-cancellation-fees
joeauyeung 47c7170
Remove reverted PR translations
joeauyeung 7180f87
Merge branch 'main' into devin/1757001996-no-show-cancellation-fees
joeauyeung File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
257 changes: 257 additions & 0 deletions
257
apps/web/components/booking/__tests__/CancelBooking.cancellationFee.test.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,257 @@ | ||
| import { render, screen } from "@testing-library/react"; | ||
| import * as React from "react"; | ||
| import { describe, expect, it, vi, beforeAll } from "vitest"; | ||
|
|
||
| import * as shouldChargeModule from "@calcom/lib/payment/shouldChargeNoShowCancellationFee"; | ||
|
|
||
| import CancelBooking from "../CancelBooking"; | ||
|
|
||
| beforeAll(() => { | ||
| Element.prototype.scrollIntoView = vi.fn(); | ||
| }); | ||
|
|
||
| vi.mock("@calcom/trpc", () => ({ | ||
| trpc: { | ||
| viewer: { | ||
| bookings: { | ||
| requestReschedule: { | ||
| useMutation: () => ({ | ||
| mutate: vi.fn(), | ||
| isLoading: false, | ||
| }), | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| })); | ||
|
|
||
| vi.mock("next-i18next", () => ({ | ||
| useTranslation: () => ({ | ||
| t: (key: string, options?: Record<string, unknown>) => { | ||
| if (key === "cancellation_fee_warning_cancel") { | ||
| const opts = options as { | ||
| time?: string; | ||
| unit?: string; | ||
| amount?: number; | ||
| formatParams?: { amount?: { currency?: string } }; | ||
| }; | ||
| return `Cancelling within ${opts?.time} ${opts?.unit} will result in a ${opts?.amount} ${opts?.formatParams?.amount?.currency} cancellation fee being charged to your card.`; | ||
| } | ||
| return key; | ||
| }, | ||
| }), | ||
| })); | ||
|
|
||
| vi.mock("@calcom/lib/hooks/useLocale", () => ({ | ||
| useLocale: () => ({ | ||
| t: (key: string, options?: Record<string, unknown>) => { | ||
| if (key === "cancel_booking_acknowledge_no_show_fee") { | ||
| const opts = options as { | ||
| timeValue?: number; | ||
| timeUnit?: string; | ||
| amount?: number; | ||
| formatParams?: { amount?: { currency?: string } }; | ||
| }; | ||
| return `I acknowledge that cancelling within ${opts?.timeValue} ${opts?.timeUnit} will result in a ${opts?.amount} ${opts?.formatParams?.amount?.currency} cancellation fee being charged to my card.`; | ||
| } | ||
| return key; | ||
| }, | ||
| }), | ||
| })); | ||
|
|
||
| vi.mock("@calcom/lib/hooks/useTelemetry", () => ({ | ||
| useTelemetry: () => ({ | ||
| event: vi.fn(), | ||
| }), | ||
| })); | ||
|
|
||
| vi.mock("@calcom/lib/hooks/useRefreshData", () => ({ | ||
| useRefreshData: () => vi.fn(), | ||
| })); | ||
|
|
||
| vi.mock("next/router", () => ({ | ||
| useRouter: () => ({ | ||
| push: vi.fn(), | ||
| query: {}, | ||
| }), | ||
| })); | ||
|
|
||
| vi.mock("@calcom/lib/payment/shouldChargeNoShowCancellationFee", () => ({ | ||
| shouldChargeNoShowCancellationFee: vi.fn(), | ||
| })); | ||
|
|
||
| const mockBookingWithCancellationFee = { | ||
| uid: "test-booking-uid", | ||
| title: "Test Meeting", | ||
| id: 123, | ||
| startTime: new Date(Date.now() + 30 * 60 * 1000), | ||
| payment: { | ||
| amount: 1000, | ||
| currency: "usd", | ||
| appId: "stripe", | ||
| }, | ||
| }; | ||
|
|
||
| const mockEventTypeMetadataWithFee = { | ||
| apps: { | ||
| stripe: { | ||
| autoChargeNoShowFeeIfCancelled: true, | ||
| autoChargeNoShowFeeTimeValue: 1, | ||
| autoChargeNoShowFeeTimeUnit: "hours" as const, | ||
| paymentOption: "HOLD" as const, | ||
| }, | ||
| }, | ||
| }; | ||
|
|
||
| const mockEventTypeMetadataWithoutFee = { | ||
| apps: { | ||
| stripe: { | ||
| autoChargeNoShowFeeIfCancelled: true, | ||
| autoChargeNoShowFeeTimeValue: 1, | ||
| autoChargeNoShowFeeTimeUnit: "hours" as const, | ||
| paymentOption: "HOLD" as const, | ||
| }, | ||
| }, | ||
| }; | ||
|
|
||
| const mockProps = { | ||
| recurringEvent: null, | ||
| setIsCancellationMode: vi.fn(), | ||
| theme: "light", | ||
| allRemainingBookings: false, | ||
| seatReferenceUid: undefined, | ||
| currentUserEmail: "test@example.com", | ||
| bookingCancelledEventProps: { | ||
| booking: {}, | ||
| organizer: { | ||
| name: "Test Organizer", | ||
| email: "organizer@example.com", | ||
| timeZone: "UTC", | ||
| }, | ||
| eventType: {}, | ||
| }, | ||
| internalNotePresets: [], | ||
| }; | ||
|
|
||
| const mockBookingWithoutCancellationFee = { | ||
| uid: "test-booking-uid-2", | ||
| title: "Test Meeting 2", | ||
| id: 124, | ||
| startTime: new Date(Date.now() + 2 * 60 * 60 * 1000), | ||
| payment: { | ||
| amount: 1000, | ||
| currency: "usd", | ||
| appId: "stripe", | ||
| }, | ||
| }; | ||
|
|
||
| describe("CancelBooking Cancellation Fee Warning", () => { | ||
| it("should show cancellation fee warning when booking is within time threshold", () => { | ||
| vi.mocked(shouldChargeModule.shouldChargeNoShowCancellationFee).mockReturnValue(true); | ||
|
|
||
| render( | ||
| <CancelBooking | ||
| booking={mockBookingWithCancellationFee} | ||
| profile={{ name: "Test User", slug: "test-user" }} | ||
| team={null} | ||
| isHost={false} | ||
| eventTypeMetadata={mockEventTypeMetadataWithFee} | ||
| {...mockProps} | ||
| /> | ||
| ); | ||
|
|
||
| expect( | ||
| screen.getByText(/I acknowledge that cancelling within 1 hours will result in a/) | ||
| ).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it("should not show cancellation fee warning when booking is outside time threshold", () => { | ||
| vi.mocked(shouldChargeModule.shouldChargeNoShowCancellationFee).mockReturnValue(false); | ||
|
|
||
| render( | ||
| <CancelBooking | ||
| booking={mockBookingWithoutCancellationFee} | ||
| profile={{ name: "Test User", slug: "test-user" }} | ||
| team={null} | ||
| isHost={false} | ||
| eventTypeMetadata={mockEventTypeMetadataWithoutFee} | ||
| {...mockProps} | ||
| /> | ||
| ); | ||
|
|
||
| expect(screen.queryByText(/I acknowledge that cancelling within/)).not.toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it("should not show cancellation fee warning when user is host", () => { | ||
| vi.mocked(shouldChargeModule.shouldChargeNoShowCancellationFee).mockReturnValue(false); | ||
|
|
||
| render( | ||
| <CancelBooking | ||
| booking={mockBookingWithCancellationFee} | ||
| profile={{ name: "Test User", slug: "test-user" }} | ||
| team={null} | ||
| isHost={true} | ||
| eventTypeMetadata={mockEventTypeMetadataWithFee} | ||
| {...mockProps} | ||
| /> | ||
| ); | ||
|
|
||
| expect(screen.queryByText(/I acknowledge that cancelling within/)).not.toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it("should not show cancellation fee warning when cancellation fee is disabled", () => { | ||
| const eventTypeMetadataWithDisabledFee = { | ||
| apps: { | ||
| stripe: { | ||
| autoChargeNoShowFeeIfCancelled: false, | ||
| autoChargeNoShowFeeTimeValue: 1, | ||
| autoChargeNoShowFeeTimeUnit: "hours" as const, | ||
| paymentOption: "HOLD" as const, | ||
| }, | ||
| }, | ||
| }; | ||
|
|
||
| vi.mocked(shouldChargeModule.shouldChargeNoShowCancellationFee).mockReturnValue(false); | ||
|
|
||
| render( | ||
| <CancelBooking | ||
| booking={mockBookingWithCancellationFee} | ||
| profile={{ name: "Test User", slug: "test-user" }} | ||
| team={null} | ||
| isHost={false} | ||
| eventTypeMetadata={eventTypeMetadataWithDisabledFee} | ||
| {...mockProps} | ||
| /> | ||
| ); | ||
|
|
||
| expect(screen.queryByText(/I acknowledge that cancelling within/)).not.toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it("should not show cancellation fee warning when payment option is not HOLD", () => { | ||
| const eventTypeMetadataWithOnBookingPayment = { | ||
| apps: { | ||
| stripe: { | ||
| autoChargeNoShowFeeIfCancelled: true, | ||
| autoChargeNoShowFeeTimeValue: 1, | ||
| autoChargeNoShowFeeTimeUnit: "hours" as const, | ||
| paymentOption: "ON_BOOKING" as const, | ||
| }, | ||
| }, | ||
| }; | ||
|
|
||
| vi.mocked(shouldChargeModule.shouldChargeNoShowCancellationFee).mockReturnValue(false); | ||
|
|
||
| render( | ||
| <CancelBooking | ||
| booking={mockBookingWithCancellationFee} | ||
| profile={{ name: "Test User", slug: "test-user" }} | ||
| team={null} | ||
| isHost={false} | ||
| eventTypeMetadata={eventTypeMetadataWithOnBookingPayment} | ||
| {...mockProps} | ||
| /> | ||
| ); | ||
|
|
||
| expect(screen.queryByText(/I acknowledge that cancelling within/)).not.toBeInTheDocument(); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are the two conditions where we would disable the cancel submit button