Skip to content

fix(billing): rotate week-quota snapshot on mid-week plan activation - #3989

Merged
PierreBrisorgueil merged 4 commits into
masterfrom
fix/3988-billing-quota-rotation
Jul 25, 2026
Merged

fix(billing): rotate week-quota snapshot on mid-week plan activation#3989
PierreBrisorgueil merged 4 commits into
masterfrom
fix/3988-billing-quota-rotation

Conversation

@PierreBrisorgueil

@PierreBrisorgueil PierreBrisorgueil commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Summary

  • What changed: wires the existing BillingResetService.forceRotateForPlanChange week-quota snapshot rotation into handleCheckoutCompleted and both branches of handleSubscriptionCreated (billing.webhook.service.js), mirroring the call already present in handleSubscriptionUpdated's plan-change block. Also makes billing.usage.service.js incrementMeter read the live plan quota (already fetched for the snapshot write) instead of the potentially-stale updatedDoc.meterQuota for overflow decisions.
  • Why: forceRotateForPlanChange was wired only into handleSubscriptionUpdated. Checkout activation and subscription-created (both the "new row" and "existing row" branches) never rotated the current-week quota snapshot after a mid-week plan activation — an upgraded org kept the previous plan's weekly quota (often 0 on free→paid) until the next weekly reset. This is a money bug: the org does not get its new quota immediately after upgrading.
  • Related issues: Closes 🐛 Billing: mid-week plan activation misses week-quota snapshot rotation #3988

Scope

  • Module(s) impacted: billing
  • Cross-module impact: none
  • Risk level: low — additive, non-fatal (logged, try/catch) call at three existing call sites; mirrors an already-proven pattern in the same file (handleSubscriptionUpdated's plan-change block, handleSubscriptionDeleted, handleCustomerDeleted).

Validation

  • npm run lint
  • npm test (full suite: npm run test:coverage — 213 suites / 2770 tests passing, coverage thresholds met, none lowered)
  • Manual checks done (2 new integration tests exercise the checkout-completed and subscription-created activation paths end-to-end against a real MongoDB, asserting the rotated snapshot + immediate new-plan quota)

Guardrails check

  • No secrets or credentials introduced (.env*, secrets/**, keys, tokens)
  • No risky rename/move of core stack paths
  • Changes remain merge-friendly for downstream projects
  • Tests added or updated when behavior changed

Notes for reviewers

  • Security considerations: none — no auth/authz surface touched.
  • Mergeability considerations: pure additive fix inside modules/billing/; downstream projects absorb via /update-stack.
  • Follow-up tasks (optional): none currently tracked.

Summary by CodeRabbit

  • Bug Fixes

    • Fixed plan activation during checkout and subscription creation so the current billing-period quota reflects the new plan.
    • Updated usage tracking to evaluate overflow against the active plan’s quota, preventing incorrect extras balances after mid-period plan changes.
    • Billing webhook processing now continues safely if quota refresh fails.
  • Tests

    • Added coverage for checkout upgrades and subscription updates during an active billing period.

forceRotateForPlanChange was wired only into handleSubscriptionUpdated's
plan-change block. handleCheckoutCompleted and both branches of
handleSubscriptionCreated never rotated the current-week quota snapshot
after a mid-week plan activation, so an upgraded org kept the previous
plan's weekly quota (often 0 on free->paid) until the next weekly reset.

- Wire the same non-fatal, logged forceRotateForPlanChange call into all
  three activation call sites (preserveUsage: true, consistent with the
  existing plan-change call site).
- billing.usage.service.js incrementMeter now reads the live plan quota
  (already fetched for the snapshot write) instead of the potentially
  stale updatedDoc.meterQuota for overflow decisions.
- Integration tests for the checkout-completed and subscription-created
  (existing row) activation paths, asserting the rotated snapshot and
  immediate new-plan quota.

Closes #3988

Claude-Session: https://claude.ai/code/session_01K6NtYTXHfNcpAQ3TJXWJxL
@PierreBrisorgueil PierreBrisorgueil added Fix A bug fix billing Phase 3 — Stripe billing integration labels Jul 25, 2026
@PierreBrisorgueil PierreBrisorgueil self-assigned this Jul 25, 2026
@PierreBrisorgueil PierreBrisorgueil added Fix A bug fix billing Phase 3 — Stripe billing integration labels Jul 25, 2026
@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@PierreBrisorgueil, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 7 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 1e6e3cb3-e879-41d2-8cfc-0e1f395998e8

📥 Commits

Reviewing files that changed from the base of the PR and between 4052bc0 and 9e4432a.

📒 Files selected for processing (4)
  • modules/billing/services/billing.usage.service.js
  • modules/billing/tests/billing.usage.service.unit.tests.js
  • modules/billing/tests/billing.webhook.checkout.unit.tests.js
  • modules/billing/tests/billing.webhook.priceid-map.unit.tests.js

Walkthrough

Billing webhook activation paths now rotate current-week quota snapshots without failing webhook processing. Meter overflow decisions use the live plan quota, with integration coverage for checkout and subscription-created flows.

Changes

Billing quota activation

Layer / File(s) Summary
Rotate quota snapshots after plan activation
modules/billing/services/billing.webhook.service.js, ERRORS.md
Checkout completion and both subscription-created branches invoke forceRotateForPlanChange with usage preservation after plan synchronization; failures are logged non-fatally.
Use live quota for meter overflow
modules/billing/services/billing.usage.service.js
incrementMeter bases overflow decisions on the current plan quota instead of the potentially stale stored snapshot.
Validate activation and metering behavior
modules/billing/tests/billing.lifecycle.integration.tests.js
Tests mock Stripe retrieval and verify snapshot rotation, preserved usage, live-quota attribution, subscription updates, and no extras balance creation.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant BillingWebhook
  participant OrganizationPlan
  participant BillingResetService
  participant BillingUsageService
  BillingWebhook->>OrganizationPlan: sync activated plan
  BillingWebhook->>BillingResetService: forceRotateForPlanChange(preserveUsage: true)
  BillingResetService-->>BillingWebhook: refresh current-week quota snapshot
  BillingUsageService->>BillingUsageService: use live meterQuota for overflow decisions
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed Title clearly states the billing fix for rotating the weekly quota snapshot on mid-week activation.
Description check ✅ Passed Description matches the template and includes summary, scope, validation, guardrails, and related issue #3988.
Linked Issues check ✅ Passed Changes implement snapshot rotation, live-quota attribution, non-fatal logging, and tests for the checkout and subscription paths.
Out of Scope Changes check ✅ Passed The changes stay within billing and test coverage, with no unrelated code outside the issue scope.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/3988-billing-quota-rotation

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.

@codecov

codecov Bot commented Jul 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.30%. Comparing base (5ac7fbc) to head (9e4432a).

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #3989      +/-   ##
==========================================
+ Coverage   93.29%   93.30%   +0.01%     
==========================================
  Files         170      170              
  Lines        5653     5664      +11     
  Branches     1820     1821       +1     
==========================================
+ Hits         5274     5285      +11     
  Misses        306      306              
  Partials       73       73              
Flag Coverage Δ
integration 61.40% <53.84%> (-0.04%) ⬇️
unit 75.58% <100.00%> (+0.04%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 5ac7fbc...9e4432a. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@PierreBrisorgueil
PierreBrisorgueil marked this pull request as ready for review July 25, 2026 18:46

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🤖 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 `@modules/billing/tests/billing.lifecycle.integration.tests.js`:
- Around line 274-282: Update the subscription-created test around
BillingWebhookService.handleSubscriptionCreated to configure the appropriate
test price-ID-to-plan mapping, then replace price.metadata.planId with a
realistic price.id payload. Keep the existing upgradePlan resolution and
rotation assertions, ensuring the test validates plan resolution through the
configured mapping.
- Around line 234-292: Add a separate integration test alongside the
existing-row test for BillingWebhookService.handleSubscriptionCreated where no
Subscription matches stripeSubscriptionId but the organization is resolved
through stripeCustomerId. Assert that a new Subscription row is created with the
upgraded plan and that the current-week BillingUsage snapshot receives the
upgraded quota and planVersion while preserving existing usage.
🪄 Autofix (Beta)

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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 435106ec-1966-418b-9931-48f3809651a3

📥 Commits

Reviewing files that changed from the base of the PR and between 5ac7fbc and 4052bc0.

📒 Files selected for processing (4)
  • ERRORS.md
  • modules/billing/services/billing.usage.service.js
  • modules/billing/services/billing.webhook.service.js
  • modules/billing/tests/billing.lifecycle.integration.tests.js

Comment thread modules/billing/tests/billing.lifecycle.integration.tests.js
Comment thread modules/billing/tests/billing.lifecycle.integration.tests.js
…Created

CodeRabbit flagged missing coverage of the "new row" (Dashboard/Payment
Link) branch's forceRotateForPlanChange call added in the previous commit.

A real-DB integration test can't exercise this: `organization` is a
unique field on the Subscription schema, so `orgSub` being found via
findByStripeCustomerId always means a row already exists for that org,
and Subscription.create() for the same org always throws E11000 before
reaching the rotation call (confirmed empirically — see the existing
"race condition: E11000" test). Cover it at the unit level instead
(mocked repositories), where the success and duplicate-skip branches
can both be exercised in isolation:

- Assert forceRotateForPlanChange(orgId, { preserveUsage: true }) fires
  when create() succeeds.
- Assert it does NOT fire on the E11000 duplicate-skip path (the handler
  never wrote a row in that case).

Claude-Session: https://claude.ai/code/session_01K6NtYTXHfNcpAQ3TJXWJxL
The Codecov patch gate flagged 4 uncovered lines: the non-fatal error
branch of the forceRotateForPlanChange call added to handleCheckoutCompleted
and both handleSubscriptionCreated branches. None of the prior tests forced
the rotation call to reject, so the catch/logger.warn lines never executed.

- billing.webhook.checkout.unit.tests.js: promote the inline
  forceRotateForPlanChange stub to a named, assertable mock; add a
  happy-path assertion (preserveUsage: true) and a rejection test
  (non-fatal, logged, does not throw).
- billing.webhook.priceid-map.unit.tests.js: same pair of tests for both
  handleSubscriptionCreated branches (existing row, new row).

Claude-Session: https://claude.ai/code/session_01K6NtYTXHfNcpAQ3TJXWJxL
@PierreBrisorgueil

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 23 minutes.

/critical-review fallback finding (CodeRabbit rate-limited on this PR):
the incrementMeter replay branch (idempotencyKey already consumed)
still returned meterQuota: existing?.meterQuota ?? meterQuota — the
stored, potentially-stale snapshot — while the non-replay path 10 lines
below already reads the live quota. A replayed call after a mid-week
plan upgrade (e.g. rotation hasn't run yet, or failed non-fatally)
would report the pre-upgrade quota instead of the current one,
inconsistent with what the same call returns when it isn't a replay.

Claude-Session: https://claude.ai/code/session_01K6NtYTXHfNcpAQ3TJXWJxL
@PierreBrisorgueil
PierreBrisorgueil merged commit a6bcd98 into master Jul 25, 2026
8 checks passed
@PierreBrisorgueil
PierreBrisorgueil deleted the fix/3988-billing-quota-rotation branch July 25, 2026 19:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

billing Phase 3 — Stripe billing integration Fix A bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🐛 Billing: mid-week plan activation misses week-quota snapshot rotation

1 participant