fix(billing): v4 hardening — finish v3 oversights + cohérence API#3584
Conversation
…stants - weeklyReset cron: applyJitter (les 3 autres l'ont) - constants extraction: dollarsToUnitRatio + maxUnitsPerOperation + getDefaultPlanId - reset.service: drop isoWeekKey re-export (lib leak) - usage.service: 80% threshold no longer skipped on >100% jump (drop break) - usage.service + init: thresholdFields config validation at boot (meterMode-gated) - README: full config knobs table - nits: cron-utils fractional-input guard restored, Date.now comment, errors.js extension comment - alertCrossed JSDoc: document last-threshold-emitted semantics on multi-crossing
|
Warning Rate limit exceeded
To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (10)
WalkthroughThis PR consolidates billing configuration reading into centralized utility functions, adds startup validation for alert thresholds, introduces jitter to the weekly reset cron, strengthens ChangesConfiguration Consolidation & Hardening
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Review rate limit: 0/1 reviews remaining, refill in 35 minutes and 51 seconds.Comment |
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 34 |
| Duplication | 7 |
AI Reviewer: first review requested successfully. AI can make mistakes. Always validate suggestions.
TIP This summary will be updated as you push new changes.
There was a problem hiding this comment.
Pull Request Overview
The PR successfully implements critical hardening for the billing module, specifically addressing cron jitter and threshold event logic. Codacy results indicate the PR is up to standards, though there is a noticeable increase in code duplication across cron scripts.
One significant logic issue remains: while the removal of the loop break in billing.usage.service.js correctly allows multiple threshold events to fire, the function's return value logic is flawed. When multiple thresholds are crossed (e.g., jumping from 0% to 150%), the current implementation returns the lowest threshold (80%) rather than the most critical one (100%). This should be corrected before merging to ensure calling services receive the correct status signal.
1 comment outside of the diff
modules/billing/services/billing.usage.service.js
line 154🟡 MEDIUM RISK
The removal of thebreakstatement correctly ensures that all crossed thresholds (e.g., 80% and 100%) are processed. However, because the thresholds are sorted descending, thealertCrossedvariable is being overwritten by lower values in subsequent iterations. It should be locked to the highest (first) threshold crossed to ensure the caller receives the most urgent signal.if (marked && !alertCrossed) { alertCrossed = String(threshold); }
Test suggestions
- Weekly reset cron correctly imports and executes jitter logic
- Usage attribution jump from 0% to 150% triggers both 80% and 100% threshold events sequentially
- Module initialization warns when alert thresholds contain unsupported values like 75% while meterMode is enabled
- Billing constants (dollarsToUnitRatio, maxUnitsPerOperation, defaultPlan) resolve with correct defaults when config is missing
- isoWeekKey computes correct ISO week strings for Monday, Sunday, and year-start boundaries
Low confidence findings
- Systemic pattern: The import and initialization block for cron entry points is duplicated across
weeklyReset,dunningSweep, andextrasExpiration. Consider centralizing this 'boot' logic into a reusable utility (e.g.,modules/billing/lib/billing-cron-init.js) to simplify future updates to module structure or core services.
TIP Improve review quality by adding custom instructions
TIP How was this review? Give us feedback
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #3584 +/- ##
==========================================
+ Coverage 87.20% 87.26% +0.05%
==========================================
Files 138 138
Lines 3917 3933 +16
Branches 1176 1181 +5
==========================================
+ Hits 3416 3432 +16
Misses 385 385
Partials 116 116
Flags with carried forward coverage won't be shown. Click here to find out more. Continue to review full report in Codecov by Sentry.
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
modules/billing/lib/billing.errors.js (1)
3-11:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winPotential bug:
isDuplicateKeyErrordoesn’t detect raw string errors.Your doc says it should work for “string-only shapes”, but the implementation only checks
err?.message. If a caller ever passes a raw string (e.g.,isDuplicateKeyError("E11000 ...")), it will returnfalse.✅ Proposed fix
export const isDuplicateKeyError = (err) => - err?.code === 11000 || (typeof err?.message === 'string' && err.message.includes('E11000')); + err?.code === 11000 || + (typeof err === 'string' && err.includes('E11000')) || + (typeof err?.message === 'string' && err.message.includes('E11000'));🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@modules/billing/lib/billing.errors.js` around lines 3 - 11, isDuplicateKeyError currently only checks err?.code and err?.message which misses callers that pass a raw string; update the function isDuplicateKeyError to also handle when err itself is a string by checking typeof err === 'string' and searching for 'E11000' (in addition to the existing err?.message check) while preserving the numeric code check (err?.code === 11000) and safe null/undefined handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@modules/billing/lib/billing.constants.js`:
- Around line 92-109: Validate and sanitize the billing getters before returning
config values: in getDollarsToUnitRatio ensure the returned ratio is a positive
finite number (fallback to 1000 if value is 0, negative, NaN, or not present),
in getMaxUnitsPerOperation ensure the value is either positive finite or
Infinity (fallback to Infinity on 0, negative, NaN, or missing), and in
getDefaultPlanId ensure the plan id is a non-empty string (fallback to 'free' if
empty, not a string, or missing); implement these checks inside
getDollarsToUnitRatio, getMaxUnitsPerOperation, and getDefaultPlanId
respectively so callers always receive safe, expected types and ranges.
In `@modules/billing/README.md`:
- Line 125: The README incorrectly points only to
modules/billing/config/billing.development.config.js as the source of defaults;
update the sentence to reference modules/billing/lib/billing.constants.js as the
source for defaults resolved there (specifically DEFAULT_CRON_JITTER_MAX_MS,
DEFAULT_ALERT_THRESHOLD_PERCENTS, getDollarsToUnitRatio(),
getMaxUnitsPerOperation(), getDefaultPlanId()) and note that downstream projects
can still override via modules/billing/config/billing.development.config.js so
readers see both the canonical default implementations and the override
location.
In `@modules/billing/services/billing.meter.service.js`:
- Around line 44-45: The code reads numeric knobs like dollarsToUnitRatio (from
getDollarsToUnitRatio()) and maxUnitsPerOperation and uses them directly for
billing math, which can produce 0/NaN/negative results; add validation after
reading these resolver values to fail fast: validate dollarsToUnitRatio is a
finite positive number and maxUnitsPerOperation is a finite positive integer (or
at least > 0), throw or log an error and abort processing if checks fail, and
sanitize values before calling incrementMeterWithOutbox() (e.g., clamp or reject
invalid inputs) so invalid configs cannot produce zero/NaN units or propagate
bad values into incrementMeterWithOutbox.
In `@modules/billing/services/billing.service.js`:
- Around line 196-197: The idempotencyKey currently uses Date.now() which can
differ across rapid retries; replace it with a stable purchase-intent identifier
so Stripe can deduplicate retries. Change the idempotencyKey construction
(variable idempotencyKey in billing.service.js) to use a caller-provided
intent/request id (e.g., request.intentId) or a server-issued nonce that is
persisted and returned to the caller and reused on retry, keeping the
organization._id and packId context in the key; ensure whatever function that
calls Stripe (the extras checkout flow around idempotencyKey) reads this stable
identifier and uses it in the Stripe request instead of Date.now().
In `@modules/billing/tests/billing.cron.weeklyReset.unit.tests.js`:
- Around line 14-24: Change the tests to exercise the actual cron entrypoint in
billing.weeklyReset.js: import the module (e.g. await
import('../lib/billing.weeklyReset.js')) and mock the helper functions and
mongoose connect so you can assert call order; specifically spy/mock
billing.cron-utils.applyJitter to return a resolved promise, mock
billing.constants.getCronJitterMaxMs to return a fixed number, and mock
mongooseService.connect to be a jest mock that records when it was called, then
invoke the cron entrypoint and assert that applyJitter was awaited (called
before mongooseService.connect) by checking the call order or timestamps (i.e.,
expect(applyJitter).toHaveBeenCalled();
expect(mongooseService.connect).toHaveBeenCalled();
expect(applyJitter).toHaveBeenCalledBefore(mongooseService.connect) or assert
call indices).
In `@modules/billing/tests/billing.usage.service.unit.tests.js`:
- Around line 418-441: Add assertions that the service actually emitted two
meter.threshold_crossed events when jumping from 0%→150%; after calling
BillingUsageService.incrementMeter, assert the event emitter was called twice
(expect(mockEventEmitter.emit).toHaveBeenCalledTimes(2)) and that the calls
include meter.threshold_crossed with the 100 threshold payload first and then 80
(use expect(mockEventEmitter.emit).toHaveBeenNthCalledWith(1,
'meter.threshold_crossed', expect.objectContaining({ threshold: '100' })) and
NthCalledWith(2, 'meter.threshold_crossed', expect.objectContaining({ threshold:
'80' }))). Also ensure you reference the same mockEventEmitter used by the
service and keep the existing markThreshold assertions for coverage.
---
Outside diff comments:
In `@modules/billing/lib/billing.errors.js`:
- Around line 3-11: isDuplicateKeyError currently only checks err?.code and
err?.message which misses callers that pass a raw string; update the function
isDuplicateKeyError to also handle when err itself is a string by checking
typeof err === 'string' and searching for 'E11000' (in addition to the existing
err?.message check) while preserving the numeric code check (err?.code ===
11000) and safe null/undefined handling.
🪄 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
Run ID: bfab9ba4-6c14-4ffc-a872-c799bf57a9a2
📒 Files selected for processing (17)
modules/billing/README.mdmodules/billing/billing.init.jsmodules/billing/crons/billing.weeklyReset.jsmodules/billing/lib/billing.constants.jsmodules/billing/lib/billing.cron-utils.jsmodules/billing/lib/billing.errors.jsmodules/billing/services/billing.meter.service.jsmodules/billing/services/billing.reset.service.jsmodules/billing/services/billing.service.jsmodules/billing/services/billing.usage.service.jsmodules/billing/tests/billing.config-knobs.unit.tests.jsmodules/billing/tests/billing.cron.weeklyReset.unit.tests.jsmodules/billing/tests/billing.init.unit.tests.jsmodules/billing/tests/billing.lifecycle.integration.tests.jsmodules/billing/tests/billing.meter.service.unit.tests.jsmodules/billing/tests/billing.reset.service.unit.tests.jsmodules/billing/tests/billing.usage.service.unit.tests.js
There was a problem hiding this comment.
Pull request overview
This PR applies another round of billing v4 hardening to finish post-v3 cleanup: it tightens meter-threshold behavior, centralizes a few remaining billing config lookups, adds weekly-reset cron jitter, and updates tests/docs to match the intended API surface.
Changes:
- Adds/finishes billing config helpers (
getDefaultPlanId,getDollarsToUnitRatio,getMaxUnitsPerOperation) and switches services to use them. - Adjusts metering/reset behavior around threshold emission and
isoWeekKeyusage/export boundaries. - Expands unit/integration coverage and updates billing docs/config references.
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
modules/billing/tests/billing.usage.service.unit.tests.js |
Adds regression coverage for multi-threshold emission in one meter jump. |
modules/billing/tests/billing.reset.service.unit.tests.js |
Updates reset-service tests to import isoWeekKey from the dedicated lib. |
modules/billing/tests/billing.meter.service.unit.tests.js |
Adds coverage for dollarsToUnitRatio helper usage in unit conversion. |
modules/billing/tests/billing.lifecycle.integration.tests.js |
Switches integration test week-key lookup to the shared iso-week helper. |
modules/billing/tests/billing.init.unit.tests.js |
Adds boot-time validation tests for threshold config warnings. |
modules/billing/tests/billing.cron.weeklyReset.unit.tests.js |
Adds basic parity tests around weekly-reset jitter dependencies. |
modules/billing/tests/billing.config-knobs.unit.tests.js |
Adds helper tests for new billing config accessors. |
modules/billing/services/billing.usage.service.js |
Uses getDefaultPlanId, documents alertCrossed, and emits all crossed thresholds. |
modules/billing/services/billing.service.js |
Clarifies the current extras checkout idempotency-key behavior in comments. |
modules/billing/services/billing.reset.service.js |
Uses getDefaultPlanId and stops re-exporting isoWeekKey. |
modules/billing/services/billing.meter.service.js |
Replaces inline config reads with billing constant helpers. |
modules/billing/lib/billing.errors.js |
Adds a small extension-point comment for future billing error helpers. |
modules/billing/lib/billing.cron-utils.js |
Restores/comment-documents the fractional jitter guard. |
modules/billing/lib/billing.constants.js |
Adds helper accessors for remaining billing config knobs. |
modules/billing/crons/billing.weeklyReset.js |
Adds startup jitter wiring to the weekly reset cron entrypoint. |
modules/billing/billing.init.js |
Adds meter-threshold config validation during billing module init. |
modules/billing/README.md |
Expands the billing config-knobs documentation table. |
- middleware: consolidate plan fallback via getDefaultPlanId() - usage.service: invariant comment on threshold loop ordering - usage.service: stale-memory comment on updatedDoc[field] - README: thresholdPercents notes complete - errors.js: drop YAGNI extension comment
- constants: add type guards to getDollarsToUnitRatio (0/NaN → 1000), getMaxUnitsPerOperation (invalid → Infinity), getDefaultPlanId (empty → 'free') - constants: guard getAlertThresholdPercents against string env-override (e.g. DEVKIT_NODE_billing_alerts_thresholdPercents=80 — coerce to [80]) - weeklyReset cron: add mongooseService.loadModels() before connect() to match retry-pending-extras-debit.cron.js pattern (Copilot LYE-) - service: add random suffix to extras_checkout idempotency key to reduce collision risk on concurrent clicks (Codacy LXAx / CodeRabbit LXwS) - README: point defaults source to billing.constants.js, fix maxUnitsPerOperation default (10000 from dev config, Infinity from constant fallback), note ratioVersion is not wrapped in a constant (Copilot LYFL, CodeRabbit LXwQ) - tests: guard tests for new constant fallback behaviours (0/NaN/empty) - tests: add getAlertThresholdPercents string coercion test - tests: weeklyReset cron — replace export parity checks with contract test (applyJitter(getCronJitterMaxMs()) produces valid delay in [0, maxMs)) - tests: 0%→150% regression — add event emit assertions (LXwU)
Math.random() in the idempotency key was flagged by Codacy as insecure random in a security-sensitive context. Switch to node:crypto randomBytes(4) which produces a cryptographically secure 8-char hex suffix.
Summary
8 v4 polish items finishing what v3 mega started. 100% backward compat. Source: audit V3 post-mega 4-agents (2026-05-03).
Changes
Test plan
Summary by CodeRabbit
Documentation
Tests
Chores