Skip to content

feat(billing): admin toolkit + operational foundations (Étape 3)#3612

Merged
PierreBrisorgueil merged 3 commits intomasterfrom
feat/billing-admin-toolkit-foundations
May 6, 2026
Merged

feat(billing): admin toolkit + operational foundations (Étape 3)#3612
PierreBrisorgueil merged 3 commits intomasterfrom
feat/billing-admin-toolkit-foundations

Conversation

@PierreBrisorgueil
Copy link
Copy Markdown
Contributor

@PierreBrisorgueil PierreBrisorgueil commented May 6, 2026

Summary

  • A — Compound indexes on Subscription model: { status, lastResetAt } (weeklyReset scan) and { status, pastDueSince } (dunningSweep scan)
  • B — 6 admin toolkit endpoints (all behind JWT + policy.isAllowed admin guard):
    • GET /api/admin/billing/customer/:orgId — Stripe + DB side-by-side diagnostic
    • POST /api/admin/billing/sync/:orgId — Force Stripe→DB sync
    • POST /api/admin/billing/webhook/replay — Re-fetch Stripe event + re-dispatch
    • GET /api/admin/billing/dead-letters — Paginated dead-letter list
    • DELETE /api/admin/billing/dead-letters/:eventId — Purge post-investigation
    • POST /api/admin/billing/cancel/:orgId — Cancel + Decision adapting to front #5 (cancel→retrieve-verify→DB write)
    • CASL: 5 new path subjects mapped to BillingAdminOps (admins already have can('manage', 'all'))
  • C — Reconciliation cron (billing.reconcile.js, Sunday 03h): paginates active|past_due subs, fetches live Stripe status, emits billing.reconciliation.divergence. LOG-ONLY — no auto-fix.
  • D — Tests reliability: syncIndexes() in integration test beforeAll (prevents E11000 on new compound indexes); refund edge-case tests (refund > original → 422, multi-refund dedup, charge_already_refunded → 422); StripeInvalidRequestError / invalid_request_error error types now map to 422 in adminRefundCharge.

Test plan

  • Lint: 0 errors, 0 warnings
  • Unit tests: 1218/1218 pass (88 suites)
  • Integration tests: 366/367 pass (1 pre-existing auth flake on socket hang up — passes on re-run)
  • New test files: billing.admin.service.unit.tests.js (28 tests), billing.admin.toolkit.integration.tests.js (18 tests), billing.reconcile.service.unit.tests.js (8 tests), billing.processedStripeEvent.repository.unit.tests.js (+12 tests for listDeadLetters + purgeDeadLetterByEventId)
  • CI green on push
  • CodeRabbit review

Summary by CodeRabbit

  • New Features

    • Admin billing toolkit with endpoints for customer status lookup, Stripe sync, webhook replay, dead-letter management, and subscription cancellation.
    • Weekly automated reconciliation sweep between Stripe subscriptions and database (log-only, no auto-fix).
  • Bug Fixes

    • Enhanced error handling to distinguish invalid arguments from Stripe-specific errors (422 vs 502 responses).
  • Tests

    • Comprehensive unit and integration tests for admin toolkit, reconciliation service, and dead-letter operations.
  • Chores

    • Added database indexes and schema updates to support admin operations.

A — Compound indexes on Subscription for weeklyReset + dunningSweep scans.
B — 6 admin endpoints: customer status, Stripe→DB sync, webhook replay,
    dead-letter list + purge, subscription cancel (Decision #5 verify).
    CASL: BillingAdminOps subject on 5 routes.
C — Reconciliation cron (billing.reconcile.js, weekly Sunday 03h).
    LOG-ONLY policy — emits billing.reconciliation.divergence events.
D — syncIndexes in integration test beforeAll (E11000 flake fix).
    Refund edge-case tests: refund>original→422, multi-refund cumul.
    Stripe invalid_request_error/StripeInvalidRequestError→422 mapping.
Copilot AI review requested due to automatic review settings May 6, 2026 18:10
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 6, 2026

Warning

Rate limit exceeded

@PierreBrisorgueil has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 43 minutes and 48 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: 606a5969-3938-48fc-881f-28e6eda0d33a

📥 Commits

Reviewing files that changed from the base of the PR and between 34dadb2 and 64951e7.

📒 Files selected for processing (17)
  • modules/billing/controllers/billing.admin.controller.js
  • modules/billing/crons/billing.reconcile.js
  • modules/billing/models/billing.subscription.model.mongoose.js
  • modules/billing/models/billing.subscription.schema.js
  • modules/billing/policies/billing.policy.js
  • modules/billing/repositories/billing.processedStripeEvent.repository.js
  • modules/billing/routes/billing.admin.routes.js
  • modules/billing/services/billing.admin.service.js
  • modules/billing/services/billing.reconcile.service.js
  • modules/billing/tests/billing.admin.integration.tests.js
  • modules/billing/tests/billing.admin.service.unit.tests.js
  • modules/billing/tests/billing.admin.toolkit.integration.tests.js
  • modules/billing/tests/billing.lifecycle.integration.tests.js
  • modules/billing/tests/billing.processedStripeEvent.repository.unit.tests.js
  • modules/billing/tests/billing.reconcile.service.unit.tests.js
  • modules/billing/tests/billing.refund.service.unit.tests.js
  • modules/billing/tests/billing.usage.concurrent-attribution.integration.tests.js

Walkthrough

This PR introduces a comprehensive billing admin toolkit for managing Stripe operations (customer status, synchronization, webhook replay, dead-letter event management, subscription cancellation) alongside a separate weekly reconciliation cron that detects divergences between Stripe subscriptions and the database without mutating state. Both features are backed by new services, repository functions, endpoints, schemas, and extensive test coverage.

Changes

Billing Admin Toolkit

Layer / File(s) Summary
Schema Definitions
modules/billing/models/billing.subscription.schema.js
New schemas AdminWebhookReplayRequest and AdminDeadLettersQuery define request shapes for webhook replay (eventId) and dead-letter queries (page, limit).
Repository Functions
modules/billing/repositories/billing.processedStripeEvent.repository.js
New listDeadLetters() provides paginated retrieval of dead-lettered events with safe defaults; purgeDeadLetterByEventId() removes dead-lettered events with validation.
Admin Service
modules/billing/services/billing.admin.service.js
Core service wiring Stripe and DB repositories to expose six operations: getCustomerStatus(), syncOrgFromStripe(), replayWebhookEvent(), listDeadLetters(), purgeDeadLetter(), and cancelSubscription(), with validation, error handling, and event dispatch.
Controller Endpoints
modules/billing/controllers/billing.admin.controller.js
Six new endpoints route to the admin service; expanded error classification in adminRefundCharge() distinguishes invalid arguments (422) from Stripe errors (502).
Routes & Access Control
modules/billing/routes/billing.admin.routes.js, modules/billing/policies/billing.policy.js
Admin toolkit endpoints registered with authentication and authorization checks; policy grants BillingAdminOps access to customer, sync, webhook/replay, dead-letters, and cancel routes.
Tests
modules/billing/tests/billing.admin.service.unit.tests.js, modules/billing/tests/billing.admin.toolkit.integration.tests.js, modules/billing/tests/billing.admin.integration.tests.js, modules/billing/tests/billing.refund.service.unit.tests.js
Comprehensive unit tests for all admin service methods; integration tests validate routing, authentication, schema validation, and error scenarios; refund tests verify 422 mapping for Stripe invalid-request errors and idempotency behavior; admin service mocking prevents deep imports in refund tests.

Billing Reconciliation

Layer / File(s) Summary
Data Shape
modules/billing/models/billing.subscription.model.mongoose.js
Two compound indexes added: (status, lastResetAt) for weeklyReset scans and (status, pastDueSince) for dunningSweep scans, with descriptive comments.
Reconciliation Service
modules/billing/services/billing.reconcile.service.js
runReconciliation() paginates subscriptions, fetches live Stripe data, detects status/plan divergences, and emits events without mutating DB state (LOG-ONLY policy); includes pagination helpers and error handling.
Cron Script
modules/billing/crons/billing.reconcile.js
Weekly Kubernetes CronJob that loads config and services via dynamic imports, applies jitter, connects to MongoDB, executes reconciliation via BillingReconcileService.runReconciliation(), logs results, and exits with code indicating errors.
Tests
modules/billing/tests/billing.reconcile.service.unit.tests.js, modules/billing/tests/billing.lifecycle.integration.tests.js, modules/billing/tests/billing.usage.concurrent-attribution.integration.tests.js
Unit tests exercise meterMode gating, Stripe configuration checks, pagination, divergence detection (status and plan), error handling, and verify LOG-ONLY policy prevents DB writes; integration tests synchronize compound indexes via Subscription.syncIndexes() to prevent flakes.

Sequence Diagram(s)

sequenceDiagram
    participant Admin as Admin User
    participant Controller as Admin Controller
    participant AdminService as Admin Service
    participant Stripe as Stripe API
    participant DB as Database
    
    Admin->>Controller: POST /api/admin/billing/sync/:orgId
    Controller->>AdminService: syncOrgFromStripe(orgId)
    AdminService->>DB: findByOrganization(orgId)
    DB-->>AdminService: subscription with stripeSubscriptionId
    AdminService->>Stripe: retrieveSubscription(stripeSubscriptionId)
    Stripe-->>AdminService: live Stripe subscription
    AdminService->>AdminService: resolvePlan(stripeSubscription)
    AdminService->>DB: updateSubscription with new plan/status
    AdminService->>DB: updateOrganization plan
    AdminService-->>Controller: { status, plan, ... }
    Controller-->>Admin: 200 { status, plan, ... }
Loading
sequenceDiagram
    participant ReconcileCron as Reconcile Cron
    participant ReconcileService as Reconcile Service
    participant DB as Database
    participant Stripe as Stripe API
    participant EventBus as Event Bus
    
    ReconcileCron->>ReconcileService: runReconciliation()
    ReconcileService->>DB: find subscriptions by status
    DB-->>ReconcileService: paginated active/past-due subs
    loop For each subscription
        ReconcileService->>Stripe: retrieveSubscription(stripeId)
        Stripe-->>ReconcileService: live Stripe subscription
        ReconcileService->>ReconcileService: compareStatus and plan
        alt Divergence detected
            ReconcileService->>EventBus: emit billing.reconciliation.divergence
        end
    end
    ReconcileService-->>ReconcileCron: { checked, divergences, errors }
    ReconcileCron->>ReconcileCron: log summary and set exitCode
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • pierreb-devkit/Node#3537: Related through ProcessedStripeEvent repository and webhook idempotency/replay/dead-letter tooling extensions alongside refund error mapping.
  • pierreb-devkit/Node#3611: Related through billing admin surface extensions (controller endpoints, routes, repositories) for admin operations.
  • pierreb-devkit/Node#3535: Related through ProcessedStripeEvent dead-letter APIs, subscription indexes, and shared meterMode config toggle used by the reconciliation cron.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main feature: admin toolkit and operational foundations for the billing module, with a stage indicator.
Description check ✅ Passed The description covers summary, scope, validation, guardrails, and test results. All major areas are addressed with clear, organized information.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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 feat/billing-admin-toolkit-foundations

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 6, 2026

Codecov Report

❌ Patch coverage is 94.79554% with 14 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.11%. Comparing base (34dadb2) to head (64951e7).

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #3612      +/-   ##
==========================================
+ Coverage   87.68%   88.11%   +0.43%     
==========================================
  Files         132      134       +2     
  Lines        4149     4417     +268     
  Branches     1290     1364      +74     
==========================================
+ Hits         3638     3892     +254     
- Misses        398      412      +14     
  Partials      113      113              
Flag Coverage Δ
integration 59.85% <27.88%> (-2.09%) ⬇️
unit 62.01% <76.95%> (+1.03%) ⬆️

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 34dadb2...64951e7. 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.

…patch, controller error paths

Add tests for: multi-page pagination in reconcile service (page += 1 branch),
billingEvents listener error (non-fatal swallow), all 10 dispatchWebhookEvent
switch cases via test.each, and controller catch paths for sync/replay/dead-letters
(422/502/500).

Fixes codecov/patch failure on PR #3612.
- HIGH: remove dead .catch() on deleteByEventId — repo returns
  {deleted:false} on not-found, infra errors now propagate correctly
- MEDIUM 1: wire AdminDeadLettersQuery.safeParse in controller —
  model.isValid is body-only (POST/PUT), query validation in controller
- MEDIUM 2: admin has can('manage','all') — BillingAdminOps covered
- MEDIUM 3: add comment clarifying full-sync vs adminUpdatePlanOnly
- test: replace swallow-error test with infra-error-propagates test
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.

1 participant