Skip to content

fix(billing): report checkout top-ups fully - #3475

Merged
steebchen merged 1 commit into
mainfrom
claude/stripe-topup-webhook-discord-0yp3dr
Aug 8, 2026
Merged

steebchen merged 1 commit into
mainfrom
claude/stripe-topup-webhook-discord-0yp3dr

Conversation

@steebchen

@steebchen steebchen commented Aug 7, 2026 •

Copy link
Copy Markdown
Member

Problem

Credit top-ups paid through Stripe Checkout on the credits page are credited and recorded as a credit_topup transaction correctly, but two things around them were incomplete:

  1. The Stripe charge has no metadata. create-checkout-session set metadata on the Checkout Session only — deliberately, because adding payment_intent_data.metadata used to make handlePaymentIntentSucceeded fulfil the same payment a second time. The consequence is that the PaymentIntent and its charge show up bare in the Stripe dashboard: no organization id, no credit amount, no gross, no fee. Every other top-up path (saved card, auto top-up) carries that metadata.
  2. The Discord notification was silently skipped whenever the session metadata carried no userEmail — notifyCreditsPurchased was gated behind if (userEmail). And when it did fire it only reported email, name and credits, so a top-up was indistinguishable from any other and carried none of the amounts.

Approach

Stripe metadata. The Checkout Session and its PaymentIntent now get the same metadata (organizationId, type, baseAmount, platformFee, internationalFee, totalAmount, userEmail, userId), plus a description on the PaymentIntent matching the saved-card path. The PaymentIntent copy is marked source: "stripe_checkout", which replaces the old "just don't set any metadata" trick as the double-credit guard:

  • handlePaymentIntentSucceeded returns early on that marker, so checkout.session.completed remains the single fulfilment path.
  • handlePaymentIntentFailed skips the failed-credit_topup-row branch for the same marker. The Stripe-hosted page lets a customer retry a declined card on the same PaymentIntent, so recording a row per decline would spam the billing history, and no transaction exists yet at that point. The paymentFailure row and dunning email still fire, exactly as before.

internationalFee is always 0 on this path — the checkout page collects the card itself, so the international-card fee is unknown up front and is not charged. It is included anyway so the metadata shape matches the other paths.

Discord. notifyCreditsPurchased takes an options object and now reports credits, bonus (when non-zero), gross, fee (gross − credits), source and organization. Both call sites pass the organization's billing email as a fallback so a missing userEmail no longer swallows the notification. Source is reported as Stripe Checkout, Saved card or Auto top-up, distinguishing the three ways credits get bought.

totalAmount and type: "credit_topup" were also added to the saved-card and auto top-up PaymentIntent metadata so all three paths carry an identical shape.

Verification

New apps/api/src/credit-topup-checkout.spec.ts drives real webhook events through the /webhook route and covers: the checkout top-up credits the org and posts a Discord embed with the right credits/gross/fee/source/organization; the notification still fires with the billing email when the session has no userEmail; a following payment_intent.succeeded carrying the checkout metadata does not credit twice or re-notify; a declined checkout attempt records a paymentFailure but no failed transaction row.

pnpm format, pnpm build, and the full apps/api (849) and apps/worker (106) unit suites all pass.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Ku5DD6b3KNZQk8AC2Y7vQi


Generated by Claude Code

Summary by CodeRabbit

  • New Features

    • Credit top-ups now retain detailed payment information, including fees, totals, organization, and purchase source.
    • Purchase notifications include richer details such as credits, bonuses, charges, currency, and organization information.
    • Automatic top-ups now record the total amount charged.
  • Bug Fixes

    • Prevented duplicate crediting and duplicate failure records for Stripe Checkout payments.
    • Improved handling of declined payments while preserving payment-failure tracking.
    • Added billing-email fallback support for credit top-ups.

Credit top-ups paid through Stripe Checkout set metadata only on the
Checkout Session, so the PaymentIntent and its charge showed up bare in
the Stripe dashboard — no organization id, credit amount, gross or fee.
Set the same metadata on the PaymentIntent via payment_intent_data and
mark it `source: "stripe_checkout"`, which the payment_intent.succeeded
handler now skips so the top-up is still credited exactly once (and the
payment_intent.payment_failed handler skips as well, since retries on the
Stripe-hosted page share one PaymentIntent).

The Discord notification was silently dropped whenever the session
carried no userEmail, and carried no top-up detail either. It now always
fires, falling back to the organization's billing email, and reports the
credits, bonus, gross, fee, source and organization.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ku5DD6b3KNZQk8AC2Y7vQi
Copilot AI lite review requested due to automatic review settings August 7, 2026 23:41
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Stripe credit top-ups now share metadata across Checkout and PaymentIntents, use structured Discord notifications, prevent duplicate webhook processing, and include integration tests for successful, duplicate, fallback, and declined-payment flows.

Changes

Credit top-up checkout

Layer / File(s) Summary
Top-up metadata and purchase notifications
apps/api/src/routes/payments.ts, apps/api/src/utils/discord.ts, apps/worker/src/worker.ts
PaymentIntents and Checkout Sessions now carry organization, amount, fee, total, user, and source metadata. Discord purchase notifications now include structured financial, organization, and source details.
Checkout webhook processing
apps/api/src/stripe.ts
Checkout completions send detailed purchase notifications and fulfill credits. Checkout PaymentIntents no longer create duplicate success or failed-credit transactions.
Checkout webhook validation
apps/api/src/credit-topup-checkout.spec.ts
Tests cover successful fulfillment, Discord reporting, billing-email fallback, duplicate success events, and declined payments.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Suggested reviewers: smakosh, ratchaw

Sequence Diagram(s)

sequenceDiagram
  participant Stripe
  participant APIWebhook
  participant Database
  participant Discord
  Stripe->>APIWebhook: checkout.session.completed
  APIWebhook->>Database: credit organization and create transaction
  APIWebhook->>Discord: notifyCreditsPurchased
  Stripe->>APIWebhook: payment_intent.succeeded
  APIWebhook-->>Database: skip duplicate Checkout processing
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: fully reporting Stripe Checkout credit top-ups.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/stripe-topup-webhook-discord-0yp3dr

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/api/src/stripe.ts`:
- Around line 2052-2062: Make credit-purchase notifications durable by replacing
the direct notifyCreditsPurchased call in apps/api/src/stripe.ts lines 2052-2062
with a deduplicated notification-job enqueue tied to the successfully persisted
top-up, so delivery and retries occur outside the webhook. Apply the same
durable enqueue path to the saved-card and auto-top-up notification calls in
apps/api/src/stripe.ts lines 2922-2932; both sites require changes before
webhook completion.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4a203755-0394-4b66-8f56-bb6b085b517b

📥 Commits

Reviewing files that changed from the base of the PR and between c4de189 and add50f2.

📒 Files selected for processing (5)
  • apps/api/src/credit-topup-checkout.spec.ts
  • apps/api/src/routes/payments.ts
  • apps/api/src/stripe.ts
  • apps/api/src/utils/discord.ts
  • apps/worker/src/worker.ts

Comment thread apps/api/src/stripe.ts
Comment on lines +2052 to +2062
await notifyCreditsPurchased({
email: userEmail ?? organization.billingEmail,
name: resolvedUser?.name,
creditAmount,
bonusAmount,
grossAmount: totalAmountInDollars,
currency: (session.currency ?? "USD").toUpperCase(),
organizationId,
organizationName: organization.name,
source: "stripe_checkout",
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Make credit-purchase notifications durable.

A Discord failure occurs after the top-up is persisted. A Stripe retry then exits at the completed-transaction guard and never re-sends the notification. Store a deduplicated notification job with the successful top-up, then deliver it with retry handling outside the webhook.

  • apps/api/src/stripe.ts#L2052-L2062: enqueue the Checkout notification before webhook completion.
  • apps/api/src/stripe.ts#L2922-L2932: enqueue saved-card and auto-top-up notifications through the same durable path.
📍 Affects 1 file
  • apps/api/src/stripe.ts#L2052-L2062 (this comment)
  • apps/api/src/stripe.ts#L2922-L2932
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/stripe.ts` around lines 2052 - 2062, Make credit-purchase
notifications durable by replacing the direct notifyCreditsPurchased call in
apps/api/src/stripe.ts lines 2052-2062 with a deduplicated notification-job
enqueue tied to the successfully persisted top-up, so delivery and retries occur
outside the webhook. Apply the same durable enqueue path to the saved-card and
auto-top-up notification calls in apps/api/src/stripe.ts lines 2922-2932; both
sites require changes before webhook completion.

@steebchen
steebchen merged commit e194ec5 into main Aug 8, 2026
12 checks passed
@steebchen
steebchen deleted the claude/stripe-topup-webhook-discord-0yp3dr branch August 8, 2026 10:49
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.

3 participants