Skip to content

Conversation

@alirezanamazian
Copy link

Allow Stripe Coupons on Booking Payment Page

Summary

This PR adds support for applying Stripe coupon codes directly on the booking payment page.
Users can now enter a promo code during checkout, have it validated via Stripe, and see the discounted price before confirming the booking.

Changes

1. Booking Payment Flow

File: apps/web/app/(use-page-wrapper)/payment/[uid]/PaymentPage.tsx

  • Added promo code input and “Apply” action to the payment page UI.
  • Integrated with the new /api/payment/promo-code endpoint to validate codes.
  • Updated price display to reflect discounted amounts when a valid coupon is applied.
  • Handled error states (invalid / expired / incompatible coupon).

2. Promo Code API

File: apps/web/pages/api/payment/promo-code.ts

  • New API route to validate Stripe coupon codes.

  • Receives the promo code and payment context, calls Stripe, and returns:

    • Discounted amount / percentage
    • Updated total
    • Error details when invalid
  • Designed to be reusable for future payment flows if needed.

3. App Store & Stripe Payment Configuration

Files:

  • packages/app-store/_utils/payments/getPaymentAppData.ts
  • packages/app-store/stripepayment/components/EventTypeAppSettingsInterface.tsx
  • packages/app-store/stripepayment/zod.ts

Changes:

  • Extended Stripe app configuration to optionally enable/disable coupons per event type.
  • Added settings UI so hosts can toggle coupon support for Stripe payments.
  • Updated Zod schemas to validate the new configuration fields.

4. Payment Component Logic

File: packages/features/ee/payments/components/Payment.tsx

  • Wired promo code support into the existing payment logic.

  • Ensures that:

    • The discount is applied only when validation succeeds.
    • The final amount sent to Stripe reflects the discounted price.
    • UI remains consistent for both coupon and non-coupon flows.

5. Platform Atom for Stripe Payment Form

File: packages/platform/atoms/event-types/payments/StripePaymentForm.tsx

  • Added props and UI elements to support:

    • Promo code input.
    • Loading state while validating.
    • Display of applied discount and errors.
  • Ensures a clear separation between presentation (form) and business logic (API + payment component).

6. UI Package Adjustments

File: packages/ui/package.json

  • Updated to support the new UI behavior / dependencies used in the payment experience (if applicable).

7. Localization

File: apps/web/public/static/locales/en/common.json

  • Added English translation keys for:

    • Promo code label and placeholder.
    • “Apply” button text.
    • Success and error messages for coupons.

Screenshots

feat
1765673489766

How It Works (High-Level)

  1. User lands on the booking payment page with Stripe as the payment app.

  2. User enters a promo code and clicks Apply.

  3. Frontend calls POST /api/payment/promo-code with the code and booking/payment context.

  4. The API validates the coupon with Stripe and returns:

    • Success → updated totals + discount info.
    • Failure → a user-friendly error message.
  5. The payment form updates the UI and uses the discounted amount for the final Stripe payment.

Testing

Manually tested:

  • ✅ Apply a valid coupon — total updates correctly.
  • ✅ Apply an invalid / expired coupon — shows error, no price change.
  • ✅ Complete booking with a valid coupon — booking + payment successful.
  • ✅ Booking without a coupon still works as before.
  • ✅ Localization strings load correctly in English.

Closes #12462

@alirezanamazian alirezanamazian requested review from a team as code owners December 14, 2025 01:23
@alirezanamazian alirezanamazian requested a review from a team December 14, 2025 01:23
@vercel
Copy link

vercel bot commented Dec 14, 2025

Someone is attempting to deploy a commit to the cal Team on Vercel.

A member of the Team first needs to authorize it.

@CLAassistant
Copy link

CLAassistant commented Dec 14, 2025

CLA assistant check
All committers have signed the CLA.

@graphite-app graphite-app bot added the community Created by Linear-GitHub Sync label Dec 14, 2025
@github-actions github-actions bot added Medium priority Created by Linear-GitHub Sync ✨ feature New feature or request labels Dec 14, 2025
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 9 files

Prompt for AI agents (all 1 issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="apps/web/app/(use-page-wrapper)/payment/[uid]/PaymentPage.tsx">

<violation number="1" location="apps/web/app/(use-page-wrapper)/payment/[uid]/PaymentPage.tsx:146">
P1: Using `||` instead of `??` causes incorrect price display for 100% discount coupons. When `displayAmount` is 0, it falls back to the original payment amount instead of showing the discounted price (free).</violation>
</file>

Since this is your first cubic review, here's how it works:

  • cubic automatically reviews your code and comments on bugs and improvements
  • Teach cubic by replying to its comments. cubic learns from your replies and gets better over time
  • Ask questions if you need clarification on any suggestion

Reply to cubic to teach it or ask questions. Re-run a review with @cubic-dev-ai review this PR

@diffray-bot
Copy link

diffray diffray code review

Free public review - Want AI code reviews on your PRs? Check out diffray.ai

Summary

Validated 18 issues: 13 kept (6 high severity bugs/quality issues, 7 medium/low quality improvements), 5 filtered (incorrect claims or low value)

Issues Found: 13

See 11 individual line comment(s) for details.

Full issue list (click to expand)

🟠 HIGH - Missing cleanup for requestAnimationFrame and event listener

File: apps/web/app/(use-page-wrapper)/payment/[uid]/PaymentPage.tsx:69-92

Category: bug

Description: The recursive requestAnimationFrame loop and sdkActionManager.on() event listener are never cleaned up, causing memory leaks and potential updates after component unmount

Suggestion: Return a cleanup function from useEffect that calls cancelAnimationFrame and removes the event listener

Confidence: 95%

Rule: fe_memory_leak_event_listeners


🟠 HIGH - Array access without bounds check

File: packages/platform/atoms/event-types/payments/StripePaymentForm.tsx:26

Category: bug

Description: Accessing attendees[0] without checking if the array is empty will cause undefined access if no attendees exist

Suggestion: Add a guard: const attendeeEmail = props.booking.attendees?.[0]?.email ?? null; or check array length first

Confidence: 92%

Rule: bug_array_bounds


🟠 HIGH - Race condition in concurrent promo code application

File: apps/web/pages/api/payment/promo-code.ts:214-247

Category: bug

Description: Stripe paymentIntent update and Prisma update are not atomic. Concurrent requests can read stale data and overwrite each other's changes.

Suggestion: Use Prisma transaction to wrap the database read and write operations, and consider using optimistic locking with a version field

Confidence: 82%

Rule: gen_concurrent_update_no_lock


🟠 HIGH - Stripe and Prisma updates not transactional

File: apps/web/pages/api/payment/promo-code.ts:214-247

Category: bug

Description: If the Stripe update succeeds but Prisma update fails, payment amount in Stripe will differ from database amount, leaving system in inconsistent state

Suggestion: Implement a compensation mechanism to rollback Stripe changes if Prisma update fails, or handle partial failures with retry logic and error states

Confidence: 85%

Rule: prisma_use_transactions


🟡 MEDIUM - Double type assertion bypasses type safety

File: apps/web/pages/api/payment/promo-code.ts:333

Category: quality

Description: Using 'as unknown as Record<string, unknown>' is a double assertion pattern that bypasses TypeScript's type checking

Suggestion: Add proper runtime validation for paymentData before destructuring, or use a type guard function to safely narrow the type. The paymentData is already validated as JsonObject via safeJsonObject() at line 304, so cast could be refined.

Confidence: 80%

Rule: ts_type_assertion_abuse


🟡 MEDIUM - Double type assertion bypasses type safety

File: packages/platform/atoms/event-types/payments/StripePaymentForm.tsx:90

Category: quality

Description: Using 'as unknown as PaymentPageProps' indicates potential type mismatch between Props and PaymentPageProps

Suggestion: Review if Props type can be properly extended from PaymentPageProps, or add explicit type conversion logic

Confidence: 75%

Rule: ts_type_assertion_abuse


🟡 MEDIUM - formatMoney duplicates existing formatPrice utility

File: packages/features/ee/payments/components/Payment.tsx:76-89

Category: quality

Description: New formatMoney function reimplements currency formatting already available in @calcom/lib/currencyConversions as formatPrice

Suggestion: Use existing formatPrice from @calcom/lib/currencyConversions instead: import { formatPrice } from '@calcom/lib/currencyConversions'

Confidence: 88%

Rule: quality_reinventing_wheel


🟡 MEDIUM - getStripePublishableKey duplicated within PR

File: packages/features/ee/payments/components/Payment.tsx:52-55

Category: quality

Description: getStripePublishableKey function is defined identically in both Payment.tsx and StripePaymentForm.tsx within the same PR

Suggestion: Extract getStripePublishableKey to a shared utility file and import it in both components

Confidence: 95%

Rule: cons_duplicate_utility_function


🟡 MEDIUM - CalPromotionData type duplicated across files

File: packages/features/ee/payments/components/Payment.tsx:22-32

Category: quality

Description: CalPromotionData type is defined identically in both the API file and the component, risking type drift

Suggestion: Extract CalPromotionData to a shared types file (e.g., packages/features/ee/payments/types.ts) and import in both locations

Confidence: 95%

Rule: cons_duplicate_type_definition


🔵 LOW - Hardcoded user-facing string 'mins'

File: apps/web/app/(use-page-wrapper)/payment/[uid]/PaymentPage.tsx:129

Category: quality

Description: User-facing text 'mins' is not internationalized using translation function

Suggestion: Replace 'mins' with t('minutes_short') or similar translation key, and add the key to translation files

Confidence: 92%

Rule: fe_hardcoded_strings


🔵 LOW - Hardcoded placeholder 'Price' not internationalized

File: packages/app-store/stripepayment/components/EventTypeAppSettingsInterface.tsx:110

Category: quality

Description: Placeholder text 'Price' should use the translation function for proper localization

Suggestion: Replace placeholder="Price" with placeholder={t('price')} using the existing useLocale hook at line 48

Confidence: 90%

Rule: fe_hardcoded_strings


🔵 LOW - Rate limit values are magic numbers

File: apps/web/pages/api/payment/promo-code.ts:359-362

Category: quality

Description: Rate limit values 5 and 60s are hardcoded without named constants, making their purpose unclear

Suggestion: Extract to named constants like PROMO_CODE_RATE_LIMIT = 5 and PROMO_CODE_RATE_LIMIT_WINDOW = '60s'

Confidence: 70%

Rule: qual_magic_numbers_js


🔵 LOW - Duplicate validation logic for CalPromotionData

File: packages/features/ee/payments/components/Payment.tsx:34-45

Category: quality

Description: isCalPromotionData validation reimplements the same field checks as getExistingPromotion in promo-code.ts

Suggestion: Create a shared Zod schema for CalPromotionData validation and use z.safeParse() in both locations for consistent validation

Confidence: 85%

Rule: cons_reimplemented_validation


Powered by diffray - AI Code Review Agent

@alirezanamazian alirezanamazian force-pushed the feat/stripe-allow-coupons-booking branch from 6d6b5ba to d6ecfef Compare December 14, 2025 02:21
@alirezanamazian
Copy link
Author

✅ Fixed Stripe webhook guard logic.

Previously platform events (event.account undefined) were returning 202 and not updating booking/payment state, leaving the UI stuck on “awaiting payment”.
Now only connected-account events are ignored.

Demo video attached below.

2025-12-14.13-50-06.copy.mp4

@diffray-bot
Copy link

diffray diffray code review

Free public review - Want AI code reviews on your PRs? Check out diffray.ai

Summary

Validated 14 issues: 11 kept (including 1 high-severity concurrency bug, 1 high-impact duplicate code issue, and several medium-severity quality issues), 3 filtered (low confidence or incorrect understanding)

Issues Found: 11

See 10 individual line comment(s) for details.

Full issue list (click to expand)

🟠 HIGH - Multiple duplicate utility functions between payment API files

File: apps/web/pages/api/payment/promo-code.ts:20-46

Category: quality

| Agent: Code Consistency & Reuse Check

Description: Four utility functions are duplicated between promo-code.ts and confirm-free.ts: safeJsonObject (lines 20-23), hasStringProp (lines 35-37), getStripeAccountFromPaymentData (lines 39-42), and getExistingPromotion (lines 44-46). Additionally, getExistingPromotion duplicates getCalPromotionFromPaymentData which is already available from @calcom/lib/payment/promoCode.

Suggestion: Extract all shared utilities to packages/lib/payment/promoCode.ts or create a new shared utilities module. For getExistingPromotion, simply use the already-imported getCalPromotionFromPaymentData function instead.

Why this matters: Duplicated code increases maintenance burden and bug risk. Any bug fix or enhancement must be applied in multiple places.

Confidence: 100%

Rule: cons_duplicate_utility_function


🟠 HIGH - Missing transaction/locking for free payment confirmation

File: apps/web/pages/api/payment/confirm-free.ts:137-166

Category: bug

| Agent: Bug Detection & Logic Errors

Description: The confirmFreePayment function modifies payment state without transaction or advisory lock, unlike promo-code.ts which uses pg_advisory_xact_lock. Race condition possible if multiple requests try to confirm the same free payment concurrently.

Suggestion: Wrap the Stripe operations and handlePaymentSuccess call in a prisma.$transaction with pg_advisory_xact_lock similar to promo-code.ts (line 49: lockPaymentForPromoCode) to prevent concurrent confirmations of the same free payment.

Why this matters: Lost updates cause data inconsistency - inventory overselling, counter drift, quota violations. In this case, concurrent free confirmations could lead to duplicate booking confirmations.

Confidence: 90%

Rule: gen_concurrent_update_no_lock


🟡 MEDIUM - Silent catch block ignores rollback error

File: apps/web/pages/api/payment/promo-code.ts:291-293

Category: bug

| Agent: Bug Detection & Logic Errors

Description: Rollback error is silently ignored with only a comment. If rollback fails, there's no logging to help diagnose issues where Stripe and DB may be inconsistent.

Suggestion: Add logging for the rollback error with context (payment ID, stripe intent) before ignoring it to aid debugging of inconsistent states.

Why this matters: Guarantees minimum observability of failures.

Confidence: 85%

Rule: ts_log_errors_instead_of_failing_silently


🟡 MEDIUM - Double type assertion bypasses type safety

File: packages/features/ee/payments/components/Payment.tsx:44

Category: bug

| Agent: Bug Detection & Logic Errors

Description: The pattern 'elements as unknown as Record<string, unknown>' performs a double assertion that completely bypasses TypeScript's type checking.

Suggestion: Add a runtime typeof check before accessing fetchUpdates, or use a proper type guard function that validates the shape at runtime.

Why this matters: Weak typing defeats TypeScript's safety guarantees.

Confidence: 80%

Rule: ts_avoid_unsafe_type_assertions


🟡 MEDIUM - Duplicate of existing imported function: getExistingPromotion

File: apps/web/pages/api/payment/promo-code.ts:44-46

Category: quality

| Agent: Code Consistency & Reuse Check

Description: Function duplicates getCalPromotionFromPaymentData from packages/lib/payment/promoCode.ts which is already imported at line 11.

Suggestion: Use getCalPromotionFromPaymentData from @calcom/lib/payment/promoCode instead of defining getExistingPromotion.

Why this matters: Duplicated code increases maintenance burden. This is especially problematic since the shared function is already available in scope.

Confidence: 100%

Rule: cons_duplicate_utility_function


🟡 MEDIUM - Type assertions on API response without validation

File: packages/features/ee/payments/components/Payment.tsx:299-304

Category: quality

| Agent: Code Quality & Best Practices

Description: API response is cast to PromoCodeApiResponse | ApiErrorPayload without runtime validation

Suggestion: Use zod schema validation to parse the response before casting, or implement a type guard function

Why this matters: Type assertions bypass TypeScript's safety. Malformed responses could cause runtime errors.

Confidence: 75%

Rule: ts_type_assertion_abuse


🟡 MEDIUM - Type assertions on API response without validation

File: packages/features/ee/payments/components/Payment.tsx:337-342

Category: quality

| Agent: Code Quality & Best Practices

Description: API response is cast to PromoCodeApiResponse | ApiErrorPayload without runtime validation

Suggestion: Use zod schema validation to parse the response before casting, or implement a type guard function

Why this matters: Type assertions bypass TypeScript's safety.

Confidence: 75%

Rule: ts_type_assertion_abuse


🟡 MEDIUM - Type assertion on API response without validation

File: packages/features/ee/payments/components/Payment.tsx:380

Category: quality

| Agent: Code Quality & Best Practices

Description: API response is cast to { ok?: boolean; message?: string } without runtime validation

Suggestion: Use zod schema validation to parse the response or add runtime type checking

Why this matters: Type assertions bypass TypeScript's safety.

Confidence: 75%

Rule: ts_type_assertion_abuse


🟡 MEDIUM - Prisma query fetches all fields without select

File: packages/features/ee/payments/api/webhook.ts:58-62

Category: performance

| Agent: Performance Review

Description: findFirst query fetches all Payment fields when only data, id, and bookingId are used

Suggestion: Add select clause to only fetch needed fields: select: { id: true, data: true, bookingId: true }

Why this matters: Fetching unnecessary data wastes memory and network bandwidth. Cal.com coding standards explicitly require using select over include.

Confidence: 90%

Rule: prisma_select_needed_fields


🔵 LOW - Type assertion after Stripe API call

File: apps/web/pages/api/payment/promo-code.ts:142

Category: quality

| Agent: Code Quality & Best Practices

Description: Stripe coupon is cast from the API response without additional validation

Suggestion: This is common with Stripe SDK but consider adding defensive checks for expected properties

Why this matters: Type assertions bypass TypeScript's safety.

Confidence: 65%

Rule: ts_type_assertion_abuse


🔵 LOW - Magic number for timestamp conversion

File: apps/web/pages/api/payment/promo-code.ts:148

Category: quality

| Agent: Code Quality & Best Practices

Description: The number 1000 is used for milliseconds to seconds conversion without a named constant

Suggestion: Extract to a named constant like MILLISECONDS_PER_SECOND = 1000

Why this matters: Magic numbers obscure intent and make changes error-prone.

Confidence: 60%

Rule: qual_magic_numbers_js


Powered by diffray - AI Code Review Agent

Copy link

@SofienBAT SofienBAT left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome! 👏

@github-actions
Copy link
Contributor

This PR has been marked as stale due to inactivity. If you're still working on it or need any help, please let us know or update the PR to keep it active.

@github-actions github-actions bot added the Stale label Dec 26, 2025
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 5 files (changes from recent commits).

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="apps/web/pages/api/payment/confirm-free.ts">

<violation number="1" location="apps/web/pages/api/payment/confirm-free.ts:193">
P1: The condition `!locked.ok` will never be true since the transaction always returns `{ ok: true as const, ... }`. This is dead code and also means `handlePaymentSuccess` will be called even for already-successful payments. Consider adding an `alreadyProcessed` flag when returning early for payments where `lockedPayment.success` is true, then check that flag here to skip `handlePaymentSuccess`.</violation>
</file>

Reply to cubic to teach it or ask questions. Tag @cubic-dev-ai to re-run a review.

@github-actions github-actions bot removed the Stale label Dec 27, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community Created by Linear-GitHub Sync devin-conflict-resolution ✨ feature New feature or request Medium priority Created by Linear-GitHub Sync size/XXL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[CAL-4350] Accept stripe coupons on booking payment

4 participants