Skip to content

feat(billing): notify user when an automatic top-up succeeds - #3577

Open
baktun14 wants to merge 2 commits into
mainfrom
feat/billing-auto-topup-success-notification
Open

feat(billing): notify user when an automatic top-up succeeds#3577
baktun14 wants to merge 2 commits into
mainfrom
feat/billing-auto-topup-success-notification

Conversation

@baktun14

@baktun14 baktun14 commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Why

Fixes CON-793

Automatic wallet-balance top-ups are credited silently today. Users can be surprised by a charge they don't remember triggering and have no in-product record that it happened. Part of the CON-717 fixed-threshold GA rollout; the failure case is tracked separately (CON-764).

What

  • The reload job tags its PaymentIntents with an auto_topup metadata marker so the webhook settlement path can distinguish automatic top-ups from manual "Add Funds" charges. Applies to both the fixed-threshold and legacy predicted-spend paths — all automatic top-ups notify.
  • On a settlement that actually credits the wallet (the row transitions created → succeeded on this delivery), the settlement returns an AutoTopUpSucceeded descriptor that the webhook dispatcher publishes after commit, mirroring the existing first-purchase-bonus flow (never fires on a rolled-back credit).
  • A new handler emails the user the amount charged, their resulting available balance, and a link to billing.
  • Exactly-once across job/webhook retries via three layers: the Stripe idempotency key per job, the settle guard that no-ops an already-succeeded row, and a deterministic per-transaction notificationId used as the broker singletonKey. Manual top-ups and skipped checks never notify.
  • Channel: email only — the transactional notification path routes only email (Novu) today; in-app is a separate, larger workstream and out of scope here.

Tests: new handler + template specs, webhook publish coverage (auto vs manual vs replayed/no-op settle), and integration coverage of the settle-path autoTopUp assembly.

Summary by CodeRabbit

  • New Features

    • Added automatic top-up success notifications after funds are credited.
    • Notifications include the charged amount, updated wallet balance, transaction details, and a link to billing information.
    • Automatic reloads are now clearly identified during payment processing to support accurate settlement and notification delivery.
  • Bug Fixes

    • Prevented notifications when required account or wallet contact information is unavailable.
    • Billing links are presented without unnecessary tracking parameters.

Automatic wallet-balance top-ups were credited silently. Users can be
surprised by a charge they don't remember triggering and have no
in-product record of it.

Tag PaymentIntents created by the reload job with an auto_topup marker so
the webhook settlement path can tell automatic top-ups from manual "Add
Funds" charges. When such a charge actually credits the wallet (the row
transitions created -> succeeded on this delivery), the settlement
returns an AutoTopUpSucceeded descriptor that the webhook dispatcher
publishes after commit, mirroring the first-purchase-bonus flow. A new
handler emails the user the amount charged, their resulting available
balance, and a link to billing.

Exactly-once is enforced at three layers: the Stripe idempotency key per
job, the settle guard that no-ops an already-succeeded row, and a
deterministic per-transaction notificationId used as the broker
singletonKey. Manual top-ups and skipped checks never notify.

Ref CON-793
@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 35c4503a-f9ef-449d-b157-7eb884e455a4

📥 Commits

Reviewing files that changed from the base of the PR and between 44bef1f and 6b3173c.

📒 Files selected for processing (12)
  • apps/api/src/app/providers/jobs.provider.ts
  • apps/api/src/app/services/auto-top-up-succeeded/auto-top-up-succeeded.handler.spec.ts
  • apps/api/src/app/services/auto-top-up-succeeded/auto-top-up-succeeded.handler.ts
  • apps/api/src/billing/events/auto-top-up-succeeded.ts
  • apps/api/src/billing/services/stripe-transaction/stripe-transaction.service.integration.ts
  • apps/api/src/billing/services/stripe-transaction/stripe-transaction.service.ts
  • apps/api/src/billing/services/stripe-webhook/stripe-webhook.service.spec.ts
  • apps/api/src/billing/services/stripe-webhook/stripe-webhook.service.ts
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.spec.ts
  • apps/api/src/billing/services/wallet-balance-reload-check/wallet-balance-reload-check.handler.ts
  • apps/api/src/notifications/services/notification-templates/auto-top-up-succeeded-notification.spec.ts
  • apps/api/src/notifications/services/notification-templates/auto-top-up-succeeded-notification.ts

📝 Walkthrough

Walkthrough

The PR marks automatic top-ups in Stripe metadata, propagates structured settlement outcomes, publishes AutoTopUpSucceeded, and processes the event through a registered job handler that creates a balance and billing notification.

Changes

Automatic top-up settlement and notification flow

Layer / File(s) Summary
Settlement contracts and metadata
apps/api/src/billing/events/auto-top-up-succeeded.ts, apps/api/src/billing/services/stripe-transaction/..., apps/api/src/billing/services/wallet-balance-reload-check/...
Adds the automatic-top-up event and settlement outcome types. Reload payment intents include the automatic-top-up metadata marker.
Stripe settlement outcome propagation
apps/api/src/billing/services/stripe-transaction/stripe-transaction.service.ts, apps/api/src/billing/services/stripe-transaction/stripe-transaction.service.integration.ts
Settlement methods return structured outcomes. Automatic-top-up success data is returned only for newly settled marked transactions.
Webhook event publication
apps/api/src/billing/services/stripe-webhook/stripe-webhook.service.ts, apps/api/src/billing/services/stripe-webhook/stripe-webhook.service.spec.ts
The webhook service publishes AutoTopUpSucceeded and preserves first-purchase bonus publication from settlement outcomes.
Notification processing and registration
apps/api/src/app/services/auto-top-up-succeeded/..., apps/api/src/notifications/services/notification-templates/..., apps/api/src/app/providers/jobs.provider.ts
Adds the job handler and notification template. The handler validates required data, retrieves the wallet balance, normalizes the billing URL, and creates the notification. Registers the handler with the job queue.

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

Possibly related PRs

Suggested reviewers: ygrishajev

✨ 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 feat/billing-auto-topup-success-notification

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

apps/api/src/app/providers/jobs.provider.ts

ESLint skipped: missing config or dependency (missing-dependency). The ESLint configuration references a package that is not available in the sandbox.

apps/api/src/app/services/auto-top-up-succeeded/auto-top-up-succeeded.handler.spec.ts

ESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox.

apps/api/src/app/services/auto-top-up-succeeded/auto-top-up-succeeded.handler.ts

ESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox.

  • 9 others

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

@codecov

codecov Bot commented Aug 9, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.18182% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 75.55%. Comparing base (44bef1f) to head (6b3173c).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
...s/stripe-transaction/stripe-transaction.service.ts 93.33% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3577      +/-   ##
==========================================
- Coverage   76.33%   75.55%   -0.78%     
==========================================
  Files        1134     1047      -87     
  Lines       29598    27288    -2310     
  Branches     7352     6894     -458     
==========================================
- Hits        22593    20617    -1976     
+ Misses       6174     5868     -306     
+ Partials      831      803      -28     
Flag Coverage Δ *Carryforward flag
api 88.91% <98.18%> (-0.02%) ⬇️
deploy-web 66.19% <ø> (ø) Carriedforward from 54fd1dc
log-collector ?
notifications 93.84% <ø> (ø) Carriedforward from 54fd1dc
provider-console 81.38% <ø> (ø) Carriedforward from 54fd1dc
provider-inventory ?
provider-proxy 88.17% <ø> (ø) Carriedforward from 54fd1dc
tx-signer ?

*This pull request uses carry forward flags. Click here to find out more.

Files with missing lines Coverage Δ
apps/api/src/app/providers/jobs.provider.ts 0.00% <ø> (ø)
...-top-up-succeeded/auto-top-up-succeeded.handler.ts 100.00% <100.00%> (ø)
...ps/api/src/billing/events/auto-top-up-succeeded.ts 100.00% <100.00%> (ø)
.../services/stripe-webhook/stripe-webhook.service.ts 100.00% <100.00%> (ø)
...eload-check/wallet-balance-reload-check.handler.ts 100.00% <ø> (ø)
...on-templates/auto-top-up-succeeded-notification.ts 100.00% <100.00%> (ø)
...s/stripe-transaction/stripe-transaction.service.ts 89.32% <93.33%> (+0.12%) ⬆️

... and 92 files with indirect coverage changes

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@claude claude Bot 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.

Beyond the reported inline findings, I also checked two other candidate issues and ruled them out:

  • Stale balance in the email: topUpWallet awaits the on-chain authorizeSpending call and commits before settleSucceededTransaction returns, and the AutoTopUpSucceeded event is only published after that commit — so by the time the notification handler reads getDeploymentBalanceInFiat, the credited balance is already reflected, not stale.
  • Bonus-publish failure dropping the auto-top-up event: DomainEventsService.publish() catches its own enqueue errors internally and returns null rather than throwing, so a failed FirstPurchaseBonusGranted publish does not prevent the subsequent AutoTopUpSucceeded publish in stripe-webhook.service.ts.
Extended reasoning...

Verified two candidate issues raised by finder agents against the actual code paths (RefillService.topUpWallet ordering relative to commit/publish, and DomainEventsService.publish's internal try/catch) and confirmed neither is a real bug. Recording this so it isn't re-explored in a future pass.

- Link the success email to plain billing instead of CONSOLE_WEB_PAYMENT_LINK,
  whose openPayment=true query auto-opens the manual Add-Funds modal — wrong for
  an informational "you were already charged" email.
- Reuse centsToUsd for the amount conversion instead of an ad hoc /100.
- Move the notificationId dedup note to a JSDoc block (no inline comments).

@claude claude Bot 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.

Reviewed and no bugs found this run. Since my last pass on this PR, the auto-top-up email link now strips the ?openPayment=true param before use and the two style nits (inline comment, cents→USD reuse) have both been addressed.

What was reviewed: the auto_topup metadata marker threading through both reload paths, the webhook settlement's SettlementOutcome (bonusGrant/autoTopUp) publish-after-commit flow, and the new notification handler/template. Checked exactly-once guarantees (idempotency key, settle-guard no-op, deterministic notificationId singletonKey) — logic holds. Two candidate issues (fragile billing-URL string split; a bonus-publish failure dropping the auto-top-up notification on the same delivery) were flagged by the bug-hunting system and ruled out.

Given this touches Stripe webhook settlement and billing-notification logic — a financial critical path — a human look is still worthwhile even with no findings.

Extended reasoning...

This PR adds an "auto top-up succeeded" email notification: it tags reload-job PaymentIntents with an auto_topup metadata marker, has the webhook settlement path return that as part of a new SettlementOutcome (alongside the existing first-purchase-bonus grant), and publishes an AutoTopUpSucceeded domain event after commit that a new job handler turns into an email via Novu.

I reviewed this PR previously and flagged one real bug (the email linked to CONSOLE_WEB_PAYMENT_LINK, which carries ?openPayment=true and auto-opens the manual Add-Funds modal) plus two style nits (an inline // comment that should be JSDoc, and re-deriving cents→USD instead of reusing centsToUsd). All three have since been addressed in the current diff: #billingUrl() now strips the query string, the singletonKey rationale is now a JSDoc block, and the template now calls centsToUsd.

This run's bug-hunting system found no new bugs. It raised and ruled out two candidates: the fragility of the .split("?")[0] fix for the billing link, and whether a bonus-publish failure could permanently drop the auto-top-up notification on retry (since a retried webhook delivery finds the transaction already succeeded and short-circuits before recomputing the outcome). Neither was confirmed as a real, actionable bug.

Security risk is limited: no new user input is parsed, the Stripe webhook signature verification path is unchanged, and the notification only echoes server-computed amounts/balances. The main risk surface is correctness of the exactly-once notification guarantees, which the PR addresses with three layers (Stripe idempotency key, settle-guard no-op, deterministic notificationId singletonKey) and covers with dedicated tests (98% patch coverage, new unit/integration specs for the settle-path autoTopUp assembly and webhook dispatch).

Given the change sits in Stripe webhook settlement and billing-notification code — a financial-critical path, even though this specific change is additive and well-tested — I think a human review is still the right call rather than an automated shadow-approval, despite the review turning up nothing further this run.

@stalniy

stalniy commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

I don't like that this PR mixes 2 different terms "auto top up" which deployment escrow balance autopup and auto reload/auto charge which is charge of user credit card.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants