Skip to content

improvement(billing): immediately charge for plan upgrades#3664

Merged
icecrasher321 merged 3 commits intostagingfrom
improvement/billing-upgrades
Mar 19, 2026
Merged

improvement(billing): immediately charge for plan upgrades#3664
icecrasher321 merged 3 commits intostagingfrom
improvement/billing-upgrades

Conversation

@icecrasher321
Copy link
Collaborator

Summary

Right now we create a proration and charge at next invoice. We should just charge immediately.

Type of Change

  • Other: UX Improvement

Testing

Tested manually

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

@vercel
Copy link

vercel bot commented Mar 19, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Mar 19, 2026 4:01am

Request Review

@cursor
Copy link

cursor bot commented Mar 19, 2026

PR Summary

Medium Risk
Changes Stripe proration behavior to immediately invoice on subscription updates and adjusts webhook side effects (unblocking/resetting usage) based on proration invoices, which could affect customer access and billing state if misclassified.

Overview
Switches plan and seat-change subscription updates to use Stripe proration_behavior: 'always_invoice' so upgrades/seat changes are charged immediately instead of waiting for the next renewal.

Updates the invoice payment-succeeded webhook to treat subscription_update (proration) invoices specially: it only unblocks users when a non-zero amount was actually paid, and it avoids resetting usage on proration invoices to prevent mid-cycle usage resets.

Written by Cursor Bugbot for commit b4371a7. Configure here.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 19, 2026

Greptile Summary

This PR changes the Stripe proration_behavior from create_prorations to always_invoice across the switch-plan and seat-update APIs, so mid-cycle upgrades and seat additions are charged immediately rather than deferred to the next billing cycle. A corresponding guard was added to handleInvoicePaymentSucceeded to prevent zero-amount proration credit invoices (e.g. downgrades) from unblocking users and wiping mid-cycle usage.

Key changes and findings:

  • switch-plan/route.ts: Single-line change to always_invoice — clean and correct for the immediate-charge goal.
  • organizations/[id]/seats/route.ts: Same change; also removes a previously incorrect inline comment claiming create_prorations charged immediately. The success message for seat reductions ("You'll receive a prorated credit") may mislead users into expecting a card refund rather than a Stripe account-balance credit.
  • invoices.ts — unblock logic: The new shouldUnblock guard correctly prevents $0 credit invoices from triggering an unblock. However, the amount_paid > 0 branch allows a successful upgrade proration payment to unblock a user who may still have an unresolved prior subscription invoice failure. Only subscription_cycle invoices represent settlement of the period's subscription debt and should trigger an unblock.
  • invoices.ts — logger refactor: The logger.error call in handleInvoicePaymentFailed was correctly moved inside the records.length > 0 guard, avoiding a misleading log entry when no subscription record is found.

Confidence Score: 2/5

  • The API changes are correct, but the webhook unblock logic introduces a new edge case where a successful upgrade payment can lift a billing block that was placed for a different, still-unpaid invoice.
  • The proration_behavior change itself is straightforward and correct. The webhook guard for zero-amount invoices is a good addition. However, the shouldUnblock = !isProrationInvoice || (amount_paid > 0) condition allows a paid upgrade invoice to clear a block that was set because of an entirely separate failed subscription payment — the two invoices are independent debts. A blocked user could upgrade, pay the proration, and regain access while their original subscription invoice is still outstanding. This is a meaningful billing integrity issue in a payment-critical path.
  • apps/sim/lib/billing/webhooks/invoices.ts — specifically the shouldUnblock condition at lines 509–510

Important Files Changed

Filename Overview
apps/sim/app/api/billing/switch-plan/route.ts Single-line change from create_prorations to always_invoice so plan switches immediately charge rather than wait for the next billing cycle. Logic is straightforward; the main risk lives in the webhook handler rather than here.
apps/sim/app/api/organizations/[id]/seats/route.ts Changes proration_behavior to always_invoice for seat count changes. Also removes an incorrect inline comment that claimed create_prorations charges immediately (it doesn't). The response message for seat reductions may mislead users into expecting a card refund rather than a Stripe account credit.
apps/sim/lib/billing/webhooks/invoices.ts Adds guards to handleInvoicePaymentSucceeded to skip unblocking and usage resets for zero-amount proration invoices. The zero-amount guard is correct for downgrade credits, but the positive-amount case (amount_paid > 0) allows a successful upgrade payment to unblock a user who may still have a prior unresolved failed subscription invoice.

Sequence Diagram

sequenceDiagram
    participant User
    participant API as Switch-Plan / Seats API
    participant Stripe
    participant DB as Database
    participant Webhook as Invoice Webhook

    User->>API: POST /api/billing/switch-plan (or PUT seats)
    API->>Stripe: subscriptions.update(proration_behavior: always_invoice)
    Note over Stripe: Previously: proration added to next invoice<br/>Now: immediate invoice created & charged
    Stripe-->>API: Updated subscription
    API->>DB: Update plan / seat count
    API-->>User: 200 OK

    Stripe->>Webhook: invoice.payment_succeeded (billing_reason=subscription_update)
    alt amount_paid > 0 (upgrade)
        Webhook->>DB: Unblock user (shouldUnblock=true)
        Note over Webhook: ⚠️ May unblock user with a prior failed subscription invoice
    else amount_paid == 0 (downgrade credit)
        Webhook->>Webhook: Skip unblock (log info)
    end
    Note over Webhook: Usage reset always skipped for proration invoices

    Stripe->>Webhook: invoice.payment_failed (billing_reason=subscription_update)
    Note over Webhook: ⚠️ Failures still block users (no guard on failure path)<br/>But prior thread: upgrade failures may leave plan upgraded in DB
Loading

Comments Outside Diff (2)

  1. apps/sim/lib/billing/webhooks/invoices.ts, line 509-510 (link)

    Upgrade proration success can unblock users with prior failed payments

    With always_invoice, when a user upgrades their plan mid-cycle, Stripe immediately charges for the proration. If that charge succeeds, isProrationInvoice = true and amount_paid > 0, so shouldUnblock = true. This can unblock a user who was previously blocked for a different failed invoice (e.g., their regular subscription renewal).

    The scenario:

    1. User's monthly subscription charge fails → user is blocked (billingBlockedReason = 'payment_failed')
    2. Stripe keeps the subscription active during the retry window
    3. User calls /api/billing/switch-plan (no blocked-user guard in that route)
    4. Upgrade proration invoice is generated and paid → invoice.payment_succeeded fires
    5. amount_paid > 0shouldUnblock = true → user is unblocked
    6. The original subscription invoice is still unpaid

    Unblocking should only be done in response to a subscription_cycle invoice, which represents the actual period payment. Proration invoices reflect mid-cycle pricing adjustments, not settlement of a previously failed debt:

  2. apps/sim/app/api/organizations/[id]/seats/route.ts, line 222-225 (link)

    Misleading success message for seat reductions

    With always_invoice, Stripe generates a negative-amount proration invoice and applies it as a credit to the customer's Stripe account balance — not as a refund to the original payment method. The current message "You'll receive a prorated credit" may lead users to expect money back on their card, which typically won't happen unless the balance is later applied to a future invoice.

    Consider clarifying the credit mechanism:

Last reviewed commit: "address bugbot comme..."

@icecrasher321
Copy link
Collaborator Author

bugbot run

@icecrasher321
Copy link
Collaborator Author

@greptile

@icecrasher321
Copy link
Collaborator Author

bugbot run

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

@icecrasher321
Copy link
Collaborator Author

@greptile

@icecrasher321 icecrasher321 merged commit 1809b38 into staging Mar 19, 2026
12 checks passed
@icecrasher321 icecrasher321 changed the title improvement(billing): immediately charge for billing upgrades improvement(billing): immediately charge for plan upgrades Mar 19, 2026
@waleedlatif1 waleedlatif1 deleted the improvement/billing-upgrades branch March 19, 2026 20:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant