Skip to content

chore(billing): batch 4 — RUNBOOKS accuracy, params validation, cursor pagination, log levels#3623

Merged
PierreBrisorgueil merged 3 commits intomasterfrom
fix/billing-batch4-runbooks-cleanup
May 7, 2026
Merged

chore(billing): batch 4 — RUNBOOKS accuracy, params validation, cursor pagination, log levels#3623
PierreBrisorgueil merged 3 commits intomasterfrom
fix/billing-batch4-runbooks-cleanup

Conversation

@PierreBrisorgueil
Copy link
Copy Markdown
Contributor

@PierreBrisorgueil PierreBrisorgueil commented May 7, 2026

Summary

Final batch (4/4) of the post-V3 billing audit fix plan. 6 items:

  • RUNBOOKS.md accuracy: 17-roles #2 dead-letter retry count corrected 3→5 (matches BILLING_WEBHOOK_MAX_ATTEMPTS = 5); 17 roles #3 plans/bump example body drops reason field (absent from AdminBumpPlanRequest schema, was yielding 422 in production)
  • Zod path param validation on 4 admin endpoints: AdminOrgIdParam (ObjectId regex) + AdminEventIdParam (evt_* regex) wired via safeParse in adminGetCustomerStatus, adminSyncFromStripe, adminCancelSubscription, adminPurgeDeadLetter → returns 422 on invalid input; test coverage added
  • Cursor-based pagination in reconcile service: replaces skip+limit with _id > lastSeenId cursor in _fetchPage / findPageForReconciliation. Stable across new subscription inserts mid-run (skip offsets shift silently). Tests for first-page null cursor, second-page cursor, and 3-page mid-run stability
  • Log level fix: handleChargeDisputeFundsReinstatedlogger.warnlogger.error (action-required state: ops must apply manual ledger credit)
  • creditPack observability: log result.applied = false at debug level in handleCheckoutPaymentCompleted to surface duplicate Stripe session redeliveries
  • console.warn → logger.warn in billing.init.js (6 occurrences); init tests updated to assert on mockLogger.warn instead of console.warn spy

Test plan

  • npm run lint — ESLint clean
  • npm run test:unit — 1321 passed (all green)
  • npm run test:integration — 391 passed; 3 pre-existing failures in billing.extraBalance.listLedger.perf (unrelated, confirmed on master)
  • CI green
  • CodeRabbit 0 threads

Summary by CodeRabbit

  • Bug Fixes

    • Updated dead-letter webhook processing threshold to 5+ failure attempts for improved detection criteria.
    • Enhanced admin endpoint parameter validation with strict error handling.
  • Improvements

    • Improved webhook idempotency tracking and dispute handling logging.
    • Refined subscription reconciliation process for enhanced stability during batch operations.

…r pagination, log levels

1. RUNBOOKS.md: #2 dead-letter retry count 3→5 (matches BILLING_WEBHOOK_MAX_ATTEMPTS=5);
   #3 plans/bump body: drop 'reason' field (not in AdminBumpPlanRequest schema, was 422)

2. Zod params validation on admin endpoints: AdminOrgIdParam (ObjectId regex) +
   AdminEventIdParam (evt_* regex) wired via safeParse in adminGetCustomerStatus,
   adminSyncFromStripe, adminCancelSubscription, adminPurgeDeadLetter → 422 on invalid input

3. Cursor-based pagination in reconcile service: replace skip+limit with
   _id > lastSeenId cursor in _fetchPage / findPageForReconciliation.
   Stable across new subscription inserts mid-run. Tests for multi-page + cursor stability.

4. dispute.funds_reinstated log level: logger.warn → logger.error in
   handleChargeDisputeFundsReinstated (action-required state, not warn-noise)

5. creditPack duplicate observability: log result.applied=false at debug level
   in handleCheckoutPaymentCompleted to surface Stripe redelivery phantom calls

6. billing.init.js console.warn → logger.warn (6 occurrences)
Copilot AI review requested due to automatic review settings May 7, 2026 08:54
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 7, 2026

Review Change Stack

Warning

Rate limit exceeded

@PierreBrisorgueil has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 48 minutes and 41 seconds before requesting another review.

To continue reviewing without waiting, purchase usage credits in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: f48d324c-d78e-4d07-ba88-68e3bf11272e

📥 Commits

Reviewing files that changed from the base of the PR and between c69a34a and 76ef319.

📒 Files selected for processing (2)
  • modules/billing/controllers/billing.admin.controller.js
  • modules/billing/tests/billing.admin.integration.tests.js

Walkthrough

This PR consolidates multiple billing module improvements: parameter validation for admin routes via new Zod schemas, cursor-based pagination in reconciliation for stability, migration of startup warnings to structured logging, and refinements to webhook idempotency handling and logging levels. All changes maintain backward compatibility and existing behavior while improving observability, robustness, and type safety.

Changes

Billing Module Enhancements

Layer / File(s) Summary
Path-Parameter Schemas
modules/billing/models/billing.subscription.schema.js
Added AdminOrgIdParam (MongoDB ObjectId orgId) and AdminEventIdParam (Stripe event eventId with evt_ prefix) Zod schemas; exported and added to default export.
Admin Controller Validation
modules/billing/controllers/billing.admin.controller.js
Integrated AdminOrgIdParam and AdminEventIdParam into four endpoints (adminGetCustomerStatus, adminSyncFromStripe, adminPurgeDeadLetter, adminCancelSubscription); each validates req.params and returns 422 on failure.
Cursor-Based Pagination
modules/billing/repositories/billing.subscription.repository.js, modules/billing/services/billing.reconcile.service.js
Replaced skip-based offset pagination with stable cursor-based pagination using MongoDB _id; findPageForReconciliation now takes (statuses, lastSeenId, limit) and runReconciliation tracks cursor across pages.
Startup Logging Migration
modules/billing/billing.init.js
Migrated four boot-time warnings from console.warn to structured logger.warn: missing pack priceUsd, unsupported alert thresholdPercents, analytics groupIdentify failures, and Subscription.plan validation exceptions.
Webhook Event Improvements
modules/billing/services/billing.webhook.service.js
In handleCheckoutPaymentCompleted, captures BillingExtraService.creditPack result and logs idempotency on duplicate. In handleChargeDisputeFundsReinstated, elevates operational alert from warn to error.
Parameter Validation Tests
modules/billing/tests/billing.admin.integration.tests.js, modules/billing/tests/billing.refund.service.unit.tests.js
Added integration tests for admin routes validating path parameters; 422 on invalid inputs, 200 on valid inputs; added module mocks for parameter schemas.
Pagination Tests
modules/billing/tests/billing.reconcile.service.unit.tests.js
Added cursor-based pagination test suite validating first-page null cursor, second-page cursor from last _id, and multi-page stability under mid-run insertion.
Logger Assertion Tests
modules/billing/tests/billing.init.unit.tests.js
Replaced console.warn spies with mockLogger.warn call assertions; updated orphaned plan, threshold validation, and suppression tests.
Runbook Documentation
modules/billing/RUNBOOKS.md
Updated dead-letter investigation threshold in Runbook #2 from 3+ to 5+ processing failures.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • pierreb-devkit/Node#3619: Both modify billing initialization logging and tests, migrating warnings to structured logger with aligned unit test assertions.
  • pierreb-devkit/Node#3612: Both introduce path-parameter validation schemas and update the admin billing controller with Zod validation for routes.
  • pierreb-devkit/Node#3620: Both modify the billing reconciliation pagination architecture (skip-based to cursor-based) and adjust webhook dispute handling and runbook documentation.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly and specifically summarizes all four main changes: RUNBOOKS accuracy fixes, parameter validation additions, cursor-based pagination implementation, and log-level adjustments.
Description check ✅ Passed The PR description is comprehensive and well-structured, covering all six items with clear explanations of what changed and why, along with test coverage validation.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/billing-batch4-runbooks-cleanup

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 and usage tips.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 7, 2026

Codecov Report

❌ Patch coverage is 84.61538% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.82%. Comparing base (60f723b) to head (76ef319).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #3623      +/-   ##
==========================================
- Coverage   88.83%   88.82%   -0.02%     
==========================================
  Files         134      134              
  Lines        4595     4616      +21     
  Branches     1416     1423       +7     
==========================================
+ Hits         4082     4100      +18     
- Misses        400      403       +3     
  Partials      113      113              
Flag Coverage Δ
integration 59.31% <61.53%> (+0.09%) ⬆️
unit 62.84% <30.76%> (-0.23%) ⬇️

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


Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 60f723b...76ef319. 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.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR is the final batch of the post‑V3 billing audit fix plan, focusing on operational correctness (runbooks/logging) and hardening billing admin + reconciliation flows (validation + stable pagination).

Changes:

  • Added Zod-based path param validation for several billing admin endpoints (422 on invalid orgId / eventId) with added/updated tests.
  • Replaced skip/limit pagination in billing reconciliation with cursor-based pagination (_id > lastSeenId) and added unit tests for cursor behavior.
  • Improved operational observability: corrected runbook examples, adjusted log levels, added debug logging for idempotent duplicate Stripe deliveries, and replaced console.warn with logger.warn in billing init (tests updated accordingly).

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated no comments.

Show a summary per file
File Description
modules/billing/tests/billing.refund.service.unit.tests.js Updates schema module mock to include newly imported param validators.
modules/billing/tests/billing.reconcile.service.unit.tests.js Adds unit tests verifying cursor pagination semantics and stability.
modules/billing/tests/billing.init.unit.tests.js Updates tests to assert logger.warn instead of console.warn.
modules/billing/tests/billing.admin.integration.tests.js Adds integration coverage for 422 behavior on invalid path params and “reaches service” on valid params.
modules/billing/services/billing.webhook.service.js Adds debug log for duplicate extras creditPack; elevates dispute funds reinstated log to error level.
modules/billing/services/billing.reconcile.service.js Switches reconciliation pagination from page index to _id cursor (lastSeenId).
modules/billing/RUNBOOKS.md Fixes runbook attempt count and corrects bump-plan example body.
modules/billing/repositories/billing.subscription.repository.js Implements cursor-based reconciliation page fetch using _id > lastSeenId and removes skip.
modules/billing/models/billing.subscription.schema.js Introduces AdminOrgIdParam and AdminEventIdParam Zod schemas for path params.
modules/billing/controllers/billing.admin.controller.js Wires new param schemas via safeParse for specific admin endpoints and returns 422 on invalid path params.
modules/billing/billing.init.js Replaces console.warn with logger.warn for boot-time warnings and non-fatal analytics errors.

coderabbitai[bot]
coderabbitai Bot previously requested changes May 7, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
modules/billing/controllers/billing.admin.controller.js (1)

249-260: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

adminDisputeCredit is the only :orgId route that skips AdminOrgIdParam validation.

Line 252 reads req.params.orgId raw while every other orgId-bearing route in this PR now calls AdminOrgIdParam.safeParse(req.params) first. With an invalid orgId, execution falls through to BillingAdminService.creditDisputeReinstated, which eventually hits Mongoose's ObjectId.isValid guard and returns null — likely surfacing as an opaque 404 or 500 instead of the expected 422 "Invalid path parameters".

🛡️ Proposed fix
 const adminDisputeCredit = async (req, res) => {
   try {
-    const { chargeId, amountCents, reason, refundRequestId } = req.body;
-    const { orgId } = req.params;
+    const parsedParams = AdminOrgIdParam.safeParse(req.params);
+    if (!parsedParams.success) {
+      return responses.error(res, 422, 'Unprocessable Entity', 'Invalid path parameters')(parsedParams.error);
+    }
+    const { orgId } = parsedParams.data;
+    const { chargeId, amountCents, reason, refundRequestId } = req.body;
🤖 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 `@modules/billing/controllers/billing.admin.controller.js` around lines 249 -
260, The adminDisputeCredit handler reads req.params.orgId directly and skips
the AdminOrgIdParam validation; update adminDisputeCredit to call
AdminOrgIdParam.safeParse(req.params) (as other routes do), return a 422 error
when safeParse fails, and then use the validated orgId value when calling
BillingAdminService.creditDisputeReinstated (and any subsequent calls) so
invalid path params are rejected early with the proper 422 instead of allowing
Mongoose/ObjectId errors to surface.
🤖 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.admin.integration.tests.js`:
- Around line 535-550: Add an assertion that the mocked service method
BillingAdminService.purgeDeadLetter was invoked with the eventId parameter used
in the test ('evt_1ABC'); after runHandlers completes, add
expect(BillingAdminService.purgeDeadLetter).toHaveBeenCalledWith('evt_1ABC') (or
the equivalent Jest matcher) to ensure the DELETE /dead-letters/:eventId route
wires the eventId through to purgeDeadLetter.
- Around line 476-491: The test currently only asserts res.status(200) but
should also assert that the controller called the service; add an assertion that
BillingAdminService.getCustomerStatus was called once with the expected orgId
string (e.g. '507f1f77bcf86cd799439011') after runHandlers completes,
referencing the mocked BillingAdminService.getCustomerStatus to verify
controller→service wiring.

---

Outside diff comments:
In `@modules/billing/controllers/billing.admin.controller.js`:
- Around line 249-260: The adminDisputeCredit handler reads req.params.orgId
directly and skips the AdminOrgIdParam validation; update adminDisputeCredit to
call AdminOrgIdParam.safeParse(req.params) (as other routes do), return a 422
error when safeParse fails, and then use the validated orgId value when calling
BillingAdminService.creditDisputeReinstated (and any subsequent calls) so
invalid path params are rejected early with the proper 422 instead of allowing
Mongoose/ObjectId errors to surface.
🪄 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: e7264ea4-157f-47cf-bc1a-c9e191ec4aab

📥 Commits

Reviewing files that changed from the base of the PR and between 60f723b and c69a34a.

📒 Files selected for processing (11)
  • modules/billing/RUNBOOKS.md
  • modules/billing/billing.init.js
  • modules/billing/controllers/billing.admin.controller.js
  • modules/billing/models/billing.subscription.schema.js
  • modules/billing/repositories/billing.subscription.repository.js
  • modules/billing/services/billing.reconcile.service.js
  • modules/billing/services/billing.webhook.service.js
  • modules/billing/tests/billing.admin.integration.tests.js
  • modules/billing/tests/billing.init.unit.tests.js
  • modules/billing/tests/billing.reconcile.service.unit.tests.js
  • modules/billing/tests/billing.refund.service.unit.tests.js

Comment thread modules/billing/tests/billing.admin.integration.tests.js
Comment thread modules/billing/tests/billing.admin.integration.tests.js
…th param

Completes the Batch 4 admin endpoint param coverage — adminDisputeCredit
/:orgId was missed in the initial pass. Now all 5 :orgId admin endpoints
validate with AdminOrgIdParam.safeParse → 422 on invalid ObjectId.
…lidation tests

CodeRabbit nit: tests asserting "reaches the service" now also verify the
controller passed the validated param to the service method (getCustomerStatus
called with orgId, purgeDeadLetter called with eventId).
@PierreBrisorgueil PierreBrisorgueil dismissed coderabbitai[bot]’s stale review May 7, 2026 09:03

All findings addressed: adminDisputeCredit orgId validation added (2 commits), 2 nitpick threads resolved.

@PierreBrisorgueil PierreBrisorgueil merged commit ddb0285 into master May 7, 2026
4 checks passed
@PierreBrisorgueil PierreBrisorgueil deleted the fix/billing-batch4-runbooks-cleanup branch May 7, 2026 09:05
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.

2 participants