Skip to content

feat(usage): add metering ledger#947

Closed
law-chain-hot wants to merge 1 commit into
mainfrom
codex/billing-pr1-metering
Closed

feat(usage): add metering ledger#947
law-chain-hot wants to merge 1 commit into
mainfrom
codex/billing-pr1-metering

Conversation

@law-chain-hot

@law-chain-hot law-chain-hot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add usage_period and usage_period_archive metering ledger tables
  • wire real box lifecycle into open/close usage periods
  • add a temporary metering inspection page for local verification
  • keep money, wallet, Stripe, and rating out of PR1

Reviewed architecture topology

+-------------------------+       React Query        +------------------------------+
| Dashboard /metering UI  | -----------------------> | BillingApiClient             |
| Metering.tsx            |                          | getOrganizationMetering()    |
+-------------------------+                          +---------------+--------------+
                                                                    |
                                                                    v
                                                       GET /organization/:orgId/metering
                                                                    |
                                                                    v
+-------------------------+    box.* events     +-------------------+-------------------+
| Box lifecycle writers   | ------------------> | UsageController + UsageService       |
| BoxService/BoxRepository|                     | auth/owner guard, transition math    |
| created/state/desired/  |                     | per-box Redis lock                   |
| organization updates    |                     +-------------------+-------------------+
+-------------------------+                                         |
                                                                    v
                                                       +------------+-------------+
                                                       | usage_period             |
                                                       | open/current ledger rows |
                                                       +------------+-------------+
                                                                    |
                                      rollover cron (>24h) closes/reopens rows
                                      archive cron moves closed rows atomically
                                                                    |
                                                                    v
                                                       +------------+-------------+
                                                       | usage_period_archive     |
                                                       | closed ledger history    |
                                                       +--------------------------+

Testing

  • yarn nx test api --testPathPatterns=usage --skip-nx-cache --output-style=stream
  • yarn nx build api --configuration=development --skip-nx-cache --output-style=stream
  • yarn nx lint api --skip-nx-cache --output-style=stream
  • local-infra metering flow verified against dashboard/metering

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6a2add8b-596e-437b-b4e0-03d6163aaebd

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/billing-pr1-metering

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.

}

function hashApiKey(value) {
return crypto.createHash('sha256').update(value).digest('hex')
@DorianZheng

Copy link
Copy Markdown
Member

@boxlite-agent review

@law-chain-hot law-chain-hot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Reviewed PR #947. I found one ledger completeness risk before this is treated as production-ready metering. I did not duplicate the existing CodeQL SHA-256 warning on the local E2E helper.

private readonly boxRepository: BoxRepository,
) {}

@OnAsyncEvent({ event: BoxEvents.CREATED })

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The ledger currently starts only from future box events, and later recordLifecycleTransition() logs-and-drops listener failures. That leaves two permanent accounting gaps: boxes already STARTED/STOPPED when this deploy lands will have no open usage_period until their next lifecycle event, and any transient DB/Redis failure during a listener run loses that period transition forever. Please add a bootstrap/reconciliation path (or durable retry/outbox) that scans current non-destroyed boxes and ensures exactly one open period per box before relying on this ledger.

@boxlite-ai boxlite-ai deleted a comment from boxlite-agent Bot Jul 8, 2026
@boxlite-ai boxlite-ai deleted a comment from boxlite-agent Bot Jul 8, 2026

@boxlite-agent boxlite-agent 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.

📦 BoxLite review — 1 issues

to,
activePeriods,
archivedPeriods,
totals: aggregateUsagePeriods([...activeRows, ...archivedRows], from, to),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Totals are paginated
Totals are computed from rows already capped by take: limit, so /metering underreports whenever overlaps exceed limit/default; verified code path at lines 150-174, migration lint passed, tests blocked by yarn install ENOSPC.

@boxlite-agent

boxlite-agent Bot commented Jul 8, 2026

Copy link
Copy Markdown

📦 BoxLite review — looks good · 67b70c0

Review evidence

  • git diff --numstat origin/main...HEAD && git diff origin/main...HEAD — 25 files, +2258/-24, read all changed files
  • node_modules/.bin/jest --config api/jest.config.ts --rootDir api src/usage — 23/23 tests pass (service, math, controller)
  • vitest run dashboard/src/pages/Metering.test.tsx — 1/1 test passes
  • tsc -p apps/api/tsconfig.app.json --noEmit — no errors in api
  • tsc -p apps/dashboard/tsconfig.app.json --noEmit — pre-existing errors only (missing generated api-client pkg), none in PR files
  • eslint api/src/usage + dashboard metering files — no lint errors, only nx cache warnings
  • psql migration dry-run — no postgres/docker available in sandbox; verified SQL by reading + tsc compile

Risk notes

  • concurrency: usage period lifecycle — per-box redis lock (waitForLock) serializes applyTransition; stale-event guard compares now vs open.startAt to drop out-of-order events when an open period exists; verified via unit tests incl. resize/resource-change cases
  • archiver cron (every 5s, no age filter) — archiveUsagePeriods moves any closed period to archive immediately; global redis lock prevents concurrent runs; safe since it only touches endAt IS NOT NULL rows, never conflicts with open-period lookups
  • dashboard billing API base URL fallback change — apiClient.ts fallback changed window.location.origin -> config.apiUrl; verified all other billing queries (wallet/tiers/invoices) are gated behind config.billingApiUrl truthy so the fallback is unreachable for them; only the new ungated metering query relies on it
  • authz — UsageController requires CombinedAuthGuard + OrganizationActionGuard + OWNER role, matching pattern used by other org-scoped controllers; boxId filter can't leak cross-org data since organizationId is always ANDed in
  • tests — good coverage: lifecycle transitions, resize/resource-change reopen, archive rollover, controller param parsing, aggregate math incl. clipped/open periods, dashboard render smoke test
apps/api/src/usage/usage.service.ts
  UsageService  +376/-0  lifecycle event handlers, cron rollover/archive
apps/api/src/usage/metering/usage-period-math.ts
  planUsageTransition/aggregateUsagePeriods  +93/-0  pure period state-machine + billing math
apps/api/src/usage/entities/usage-period.entity.ts
  UsagePeriod  +59/-0  open-ledger table, unique-open-per-box index
apps/api/src/usage/entities/usage-period-archive.entity.ts
  UsagePeriodArchive  +87/-0  closed-ledger table
apps/api/src/usage/usage.controller.ts
  UsageController.getOrganizationMetering  +64/-0  new metering endpoint, owner-gated
apps/api/src/migrations/pre-deploy/1782700000000-migration.ts
  Migration1782700000000  +71/-0  creates usage_period(_archive) tables
apps/dashboard/src/pages/Metering.tsx
  Metering  +233/-0  new dashboard usage page
apps/dashboard/src/api/apiClient.ts
  ApiClient ctor  +1/-1  billing base URL fallback changed
apps/scripts/metering-local-e2e.mjs
  main script  +390/-0  dev-only local e2e harness

reviewed 67b70c0 in a BoxLite microVM · @boxlite-agent review to re-run · powered by BoxLite

@DorianZheng

Copy link
Copy Markdown
Member

@Boxlite review

@boxlite-agent boxlite-agent 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.

📦 BoxLite review — 2 issues

export class Migration1782700000000 implements MigrationInterface {
name = 'Migration1782700000000'

public async up(queryRunner: QueryRunner): Promise<void> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛑 Existing boxes never meter
The migration creates empty usage tables and UsageService only opens periods from future events, so boxes already running/stopped at deploy have no usage_period until a later lifecycle/resource/org event.

Comment on lines +150 to +160
const [activeRows, archivedRows] = await Promise.all([
this.periods.find({
where: activeWhere,
order: { startAt: 'DESC' },
take: limit,
}),
this.archives.find({
where: archivedWhere,
order: { startAt: 'DESC' },
take: limit,
}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Totals ignore limited rows
getOrganizationMeteringView applies take: limit to both queries, then line 174 aggregates only those rows; orgs with more than limit overlapping periods get undercounted totals.

@DorianZheng

Copy link
Copy Markdown
Member

@boxlite-agent review

@law-chain-hot

Copy link
Copy Markdown
Contributor Author

Superseded by #968, which keeps the Usage lifecycle implementation aligned with the selected upstream design and uses the final BoxLite table names.

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.

3 participants