fix(billing): mega hardening v3 — atomicity + DRY + config#3583
Conversation
Bundles 3 logical groupings on overlapping surfaces to avoid rebase hell:
ATOMICITY (🔴 + 4× 🟠 + 2× 🟡 + 🔵 + ⚪)
- markFailedAttempt: single atomic op via aggregation pipeline (close TOCTOU
on concurrent crons that double-emit billing.extras_debit.exhausted)
- incrementMeterWithOutbox: try/catch outbox create, E11000 fetch existing
- zero-cost attribute(): writes idempotency key (close double-charge replay)
- forceRotateForPlanChange: planChangeResetTriggered set BEFORE try (close
double-rotate via fallback resetWeek)
- E11000 retry: legacy consumedHistoryIds filter symmetric (close mid-migration
double-charge)
- unitsFromCosts: hard-fail null ratioVersion + non-null costs in meterMode
- BillingMeterOutbox: timestamps:true (drop manual createdAt)
- billing.policy.js: drop stale ESLint disable
DRY + lib EXTRACTION (3× 🟠 + 5× 🟡 + 3× 🔵)
- webhook.controller: responses.error() canonical envelope (3 sites)
- billing.controller: route via BillingExtraService.getOrgBalanceContext()
- _ensureStripeCustomer private: dedup find-or-create between checkout flows
- billing.processedStripeEvent.schema.js: NEW Zod schema parity
- lib/billing.isoWeek.js: NEW currentWeekKey + isoWeekKey single source
- findAllDueForReset deprecated: removed
- lib/billing.cron-utils.js: NEW applyJitter helper, applied to all 3 crons
- billing.plans.controller: unit tests added
- lib/billing.errors.js: NEW isDuplicateKeyError single source
- lib/billing.constants.js: NEW shared types/constants — kills dynamic imports
CONFIG KNOBS SURFACING (NEW)
- config.billing.{meter,outbox,crons,planChange,alerts,events}.* knobs
- All magic numbers (METER_RUN_BASE, retry limits, jitter window, threshold
percentages, fallback plan id, event names) now config-driven with safe
defaults. README documents each with downstream override examples.
|
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 (53)
✨ 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 41 minutes and 38 seconds.Comment |
Not up to standards ⛔🔴 Issues
|
| Category | Results |
|---|---|
| ErrorProne | 7 high |
| Security | 2 high |
🟢 Metrics 71 complexity · 26 duplication
Metric Results Complexity 71 Duplication 26
AI Reviewer: first review requested successfully. AI can make mistakes. Always validate suggestions.
TIP This summary will be updated as you push new changes.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #3583 +/- ##
==========================================
+ Coverage 87.03% 87.20% +0.17%
==========================================
Files 133 138 +5
Lines 3873 3917 +44
Branches 1158 1176 +18
==========================================
+ Hits 3371 3416 +45
+ Misses 388 385 -3
- Partials 114 116 +2
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.
Pull Request Overview
This PR successfully implements several core hardening requirements, such as atomic aggregation for failed attempts and idempotency keys for zero-cost attributions. However, the current implementation contains a high-severity logic flaw in the usage overflow detection: users on zero-unit plans (like 'Free' or 'Pay-as-you-go') will bypass billing for their usage because the system incorrectly skips extra calculations when the quota is zero.
Furthermore, the PR has been flagged by Codacy as 'not up to standards,' primarily due to a high volume of code clones and linting issues. Specifically, the initialization logic across multiple cron scripts is identical and should be centralized. There are also several false-positive linter warnings related to Qwik that need to be suppressed to clean up the CI output.
While the extraction of shared utilities (ISO weeks, jitter) is a step in the right direction, addressing the overflow logic and the systemic duplication is necessary before merging.
About this PR
- There is significant duplication in the boot sequence of the billing cron scripts. Consider centralizing the environment setup, dynamic imports, and jitter application into a shared 'cronBoot' utility in 'modules/billing/lib/'. This will reduce boilerplate and make the boot logic easier to maintain.
- Several Node.js service and test files are triggering false-positive 'useQwikValidLexicalScope' warnings. These should be suppressed using the biome-ignore comment to prevent noise in the quality reports.
1 comment outside of the diff
modules/billing/services/billing.usage.service.js
line 125🔴 HIGH RISK
Remove theeffectiveQuota > 0condition from the overflow detection logic. For plans with zero included units, every unit consumed should be treated as an overflow. The current code results in 0extrasConsumedfor these users, effectively giving them free usage.
Test suggestions
- Concurrent markFailedAttempt calls emit exactly one exhausted event
- Outbox creation handles E11000 by returning the existing row instead of failing
- Zero-cost attribution writes the idempotency key and blocks later non-zero replays for that step
- Boot fails when legacy consumedHistoryIds are detected in meterMode
- Handle partial refunds correctly by identifying the latest refund delta via timestamp
- Config overrides for jitter and retry intervals are correctly resolved through constants
- ISO week key handles year rollover and DST transitions correctly
TIP Improve review quality by adding custom instructions
TIP How was this review? Give us feedback
There was a problem hiding this comment.
Pull request overview
Hardens the billing module’s metering/outbox flows and surfaces configuration knobs, while reducing duplication and aligning controllers/services with the project’s layering conventions.
Changes:
- Improves outbox failure accounting + adds E11000 recovery for outbox create after meter increment.
- Extracts shared billing utilities (ISO week, jitter, duplicate-key detection, config getters) and deprecates/removes old reset query paths.
- Expands/updates unit + integration test coverage around plan-change rotation, refunds, outbox retries, and config knobs.
Reviewed changes
Copilot reviewed 53 out of 53 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| modules/billing/tests/billing.webhook.subscription.unit.tests.js | Adds plan-change fallback test coverage |
| modules/billing/tests/billing.webhook.refund.integration.tests.js | Tests “latest refund” delta selection |
| modules/billing/tests/billing.usageHeader.integration.tests.js | Updates mocks for layering change |
| modules/billing/tests/billing.usage.service.unit.tests.js | Adds outbox E11000 + threshold config tests |
| modules/billing/tests/billing.usage.repository.unit.tests.js | Adds legacy-field count test |
| modules/billing/tests/billing.usage.repository.integration.tests.js | Validates legacy-field detection post-migration |
| modules/billing/tests/billing.subscription.repository.unit.tests.js | Removes deprecated reset query tests |
| modules/billing/tests/billing.routes.integration.tests.js | Updates mocks for controller layering fix |
| modules/billing/tests/billing.reset.service.unit.tests.js | Tests preserveUsage default config |
| modules/billing/tests/billing.processedStripeEvent.unit.tests.js | Switches to shared Zod schema import |
| modules/billing/tests/billing.plans.controller.unit.tests.js | New controller unit tests |
| modules/billing/tests/billing.meter.service.unit.tests.js | Adds runBase/ratioVersion/zero-cost + fallbackPlan tests |
| modules/billing/tests/billing.meter.outbox.unit.tests.js | Updates markFailedAttempt + adds findByIdempotencyKey test |
| modules/billing/tests/billing.lifecycle.integration.tests.js | Adds concurrency + E11000 lifecycle integration tests |
| modules/billing/tests/billing.isoWeek.unit.tests.js | New ISO-week utility tests |
| modules/billing/tests/billing.init.unit.tests.js | Tests boot abort when legacy field remains |
| modules/billing/tests/billing.extras.controller.unit.tests.js | Updates mocks for layering fix |
| modules/billing/tests/billing.extra.service.unit.tests.js | Adds getOrgBalanceContext unit test |
| modules/billing/tests/billing.cron.weeklyReset.unit.tests.js | Removes deprecated repo mock usage |
| modules/billing/tests/billing.cron.retryPendingExtrasDebit.unit.tests.js | Tests configured retry limit + event name |
| modules/billing/tests/billing.cron-utils.unit.tests.js | New jitter util tests |
| modules/billing/tests/billing.controller.unit.tests.js | Updates webhook controller error envelopes |
| modules/billing/tests/billing.config-knobs.unit.tests.js | New tests for config getters + defaults |
| modules/billing/services/billing.webhook.service.js | Plan-change rotation ordering + refund delta selection |
| modules/billing/services/billing.usage.service.js | Adds config-driven thresholds + outbox E11000 recovery |
| modules/billing/services/billing.service.js | Extracts _ensureStripeCustomer helper |
| modules/billing/services/billing.reset.service.js | Uses shared isoWeek + config default + dup-key helper |
| modules/billing/services/billing.plan.service.js | Uses shared dup-key helper |
| modules/billing/services/billing.meter.service.js | Uses shared constants + enforces ratioVersion behavior + zero-cost idempotency |
| modules/billing/services/billing.meter.outbox.service.js | Makes exhausted event name + retry attempts configurable |
| modules/billing/services/billing.extra.service.js | Adds getOrgBalanceContext service method |
| modules/billing/repositories/billing.usage.repository.js | Adds legacy-field count + uses dup-key helper |
| modules/billing/repositories/billing.subscription.repository.js | Removes deprecated findAllDueForReset |
| modules/billing/repositories/billing.processedStripeEvent.repository.js | Uses dup-key helper |
| modules/billing/repositories/billing.meter.outbox.repository.js | Adds idempotency lookup + atomic failure transition |
| modules/billing/policies/billing.policy.js | Removes unused-vars suppression |
| modules/billing/models/billing.processedStripeEvent.schema.js | New Zod schema for processed events |
| modules/billing/models/billing.meter.outbox.schema.js | Adds updatedAt in schema |
| modules/billing/models/billing.meter.outbox.model.mongoose.js | Enables timestamps on outbox model |
| modules/billing/lib/events.js | Updates exhausted-event doc wording |
| modules/billing/lib/billing.isoWeek.js | New shared ISO-week key utilities |
| modules/billing/lib/billing.errors.js | New shared duplicate-key detector |
| modules/billing/lib/billing.cron-utils.js | New jitter helper |
| modules/billing/lib/billing.constants.js | New central config getters/defaults |
| modules/billing/crons/retry-pending-extras-debit.cron.js | Adds jitter + config-driven retry interval |
| modules/billing/crons/billing.extrasExpiration.js | Adds jitter before cron work |
| modules/billing/crons/billing.dunningSweep.js | Adds jitter before cron work |
| modules/billing/crons/README.md | Documents built-in jitter + knobs |
| modules/billing/controllers/billing.webhook.controller.js | Switches to canonical error envelope helper |
| modules/billing/controllers/billing.controller.js | Fixes controller→service layering for extras balance |
| modules/billing/config/billing.development.config.js | Surfaces new billing config knobs + defaults |
| modules/billing/billing.init.js | Adds boot-time legacy-field guard + clearer warnings |
| modules/billing/README.md | Documents new hardening config knobs |
Comments suppressed due to low confidence (2)
modules/billing/crons/billing.extrasExpiration.js:42
- This cron connects to Mongo without calling
mongooseService.loadModels()before importing repositories/services. Because the repositories resolve Mongoose models at module scope, this can crash withMissingSchemaErrorin standalone cron execution. CallmongooseService.loadModels()beforeconnect(), and only import Billing repositories/services after models are loaded.
try {
await applyJitter(getCronJitterMaxMs());
await mongooseService.connect();
const [{ default: BillingExtraService }, { default: BillingExtraBalanceRepository }] =
await Promise.all([
import('../services/billing.extra.service.js'),
import('../repositories/billing.extraBalance.repository.js'),
]);
modules/billing/crons/billing.dunningSweep.js:44
- This cron connects to Mongo without calling
mongooseService.loadModels()before importing repositories (and Org repository). In a standalone CronJob, importing repositories that domongoose.model(...)at module scope can throwMissingSchemaErrorif models weren’t loaded. CallmongooseService.loadModels()beforeconnect(), and only import repositories/services after models are loaded.
try {
await applyJitter(getCronJitterMaxMs());
await mongooseService.connect();
const [{ default: BillingSubscriptionRepository }, { default: OrganizationRepository }] = await Promise.all([
import('../repositories/billing.subscription.repository.js'),
import('../../organizations/repositories/organizations.repository.js'),
]);
Summary
Comprehensive billing module hardening bundling 3 logical groupings on overlapping surfaces. Goal: module the simplest, cleanest, most configurable possible.
Source: 4-agent audit 2026-05-02 found 22 backend findings (1 🔴 + 7 🟠 + 8 🟡 + 4 🔵 + 2 ⚪).
Sections
A — Atomicity hardening (🔴 + 7 issues)
markFailedAttempt: single atomic aggregation pipeline op — closes TOCTOU where 2 concurrent crons both win the$incphase then both flipstatus: failed, double-emittingbilling.extras_debit.exhaustedincrementMeterWithOutbox: try/catch + E11000 fetch for outbox create crash recovery — closes free compute leak where meter incremented but outbox row missingattribute(): now writes idempotency key even withunits=0— closes double-charge replay when later retry has non-zero costsforceRotateForPlanChange:planChangeResetTriggeredset BEFORE try — closes double-rotate via fallbackresetWeekon throwconsumedHistoryIdsfilter added symmetrically to retry branchunitsFromCosts: nullratioVersion+ non-null costs → hard-fail in meterMode, warn otherwiseBillingMeterOutbox:timestamps:true, drop manualcreatedAtbilling.policy.js: drop stale ESLintno-unused-varsdisableB — DRY + lib extraction (3 🟠 + 5 🟡 + 3 🔵)
billing.webhook.controller: all 3 rawres.status().json()→responses.error()canonical envelopebilling.controller: direct repo call →BillingExtraService.getOrgBalanceContext()(layering fix)_ensureStripeCustomer: extracted private helper deduplicating ~50 lines of find-or-create logic betweencreateCheckout+createExtrasCheckoutbilling.processedStripeEvent.schema.js: NEW — Zod schema parity with Mongoose modellib/billing.isoWeek.js: NEW —currentWeekKey()+isoWeekKey(date)single source (was duplicated in usage.service + reset.service with DST divergence risk)findAllDueForResetdeprecated: removed from repo + test mockslib/billing.cron-utils.js: NEW —applyJitter(maxMs)usingcrypto.randomInt, applied to all 3 cronsbilling.plans.controller: unit tests added (success + Stripe down paths)lib/billing.errors.js: NEW —isDuplicateKeyError(err)single source (was 3 inline duplicates)lib/billing.constants.js: NEW — all shared getters/constants, kills dynamic imports in hot pathC — Config knobs surfacing (NEW)
All magic numbers extracted to
config.billing.*with safe defaults. Backward-compat: all defaults preserve current behavior.runBaseUnitskept as deprecated alias.billing.meter.{runBase, fallbackPlanId, maxUnitsPerOperation}billing.outbox.{maxRetryAttempts, retryIntervalSec}billing.crons.jitterMaxMsbilling.planChange.preserveUsageDefaultbilling.alerts.thresholdPercentsbilling.events.extrasExhaustedREADME documents each knob with downstream override examples.
Boot migration guard (B.7)
Dual-read
consumedHistoryIdsremoved. Boot now assertscountDocuments({consumedHistoryIds: {$exists: true}}) === 0whenmeterMode: true— aborts deploy if unmigrated docs remain.Test plan
billing.lifecycle.integration.tests.js)billing.lifecycle.integration.tests.js)billing.webhook.subscription.unit.tests.js)billing.config-knobs.unit.tests.js)