Skip to content

feat(usage/billing): identities as first-class objects + billing clarity (Phases 1–2) - #347

Merged
eliteprox merged 5 commits into
mainfrom
feat/usage-billing-phase1-identities
Jul 31, 2026
Merged

feat(usage/billing): identities as first-class objects + billing clarity (Phases 1–2)#347
eliteprox merged 5 commits into
mainfrom
feat/usage-billing-phase1-identities

Conversation

@eliteprox

@eliteprox eliteprox commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Phases 1 and 2 of the Usage & Billing overhaul. One commit per phase; Phases 3–5 (usage page fixes, Payments tab, cross-cutting consistency) are not in this PR.

Phase 1 — Identity as a first-class object

An M2M identity is a billable entity that had no home in the IA: there was no way to answer "which identity burned my allowance?"

No metering/backend work was required. All five OpenMeter meters already group by client_id + external_user_id — the identity data was fetched and then discarded in the UI layer.

  • aggregateIdentityTotals — per-identity cycle fee, requests, billable_secs, last active.
  • aggregateDailyUserRows — app × identity chart dimension, computed from the DAY-window rows the dashboard already fetches (METER_GROUP_BY_DETAIL includes external_user_id), so the new dimension costs zero additional meter queries.
  • identity-rollup — joins meter identities with app_users provisioning and api_keys. Unions in both directions: metered-but-unprovisioned identities stay visible (they are billable) and provisioned-but-idle ones stay visible (they are still identities).
  • New /apps/:id/identities table, default sorted by network fee desc, with a per-identity drill-down (usage chart + request log).
  • Identity filter and a pipeline/identity chart toggle on both usage pages; identity column on the requests table; cross-links request → identity → API key → app.

Authorization note

The identity request log is served by a new app-scoped endpoint, not /api/v1/me/usage/requests — that route deliberately rejects externalUserId so a viewer cannot read another identity's log. App ownership is a different and correct authorization basis for the same data, so the existing guard is left intact rather than relaxed.

Phase 2 — Billing clarity

$25.00 / $25.00 remaining, $0.655482 accrued and 13% of allowance used appeared side by side with no way to reconcile them, and the consumption order lived only in scattered prose.

  • CostWaterfall renders the real settlement order (plan allowance → prepaid credits → card). The three steps always sum to the cycle total, so figures reconcile by construction rather than by prose. Reused on /billing and the usage-page subscription card; the prose it replaces is deleted.
  • Transactions ledger — previously missing entirely. Grants and invoices are real billing records; credit consumption is synthesized from daily metered spend walked against the plan allowance, because OpenMeter exposes only a scalar consumed total. Running balances are assigned backward from the live balance, so the newest row always equals the balance shown elsewhere and the ledger cannot drift from the rest of the page. Derived rows are labelled.
  • Platform invoices — human labels (Usage overage · Jul 2026) with raw identifiers moved to an expandable details row; date issued / period covered / amount / status columns; $0 invoices behind a toggle (default off); pagination.
  • Payment methods — removing the only method is now blocked in the UI and the API, so overage invoices always have something to charge. The Remove confirmation dialog already existed and is unchanged.

formatUsdMicrosSummary rounds to 2dp (the existing display formatter truncates, turning $0.655482 into $0.65); full precision stays available on hover. Phase 5 rolls it out to remaining surfaces.

Invoice links

OpenMeter stores only the Stripe invoice id (externalIds.invoicing) and hosted URLs are signed and unguessable. Rather than N+1 Stripe calls at page load, links resolve on click via a new endpoint that re-resolves the invoice from the caller's own wallet — an invoice id cannot be used to read someone else's.

Known limitation

The waterfall caps credits-applied at the current balance. That is exact when credits covered the overage, but understates what credits absorbed if they were fully exhausted mid-cycle — OpenMeter gives no cycle-scoped consumption figure to do better. The three steps still sum to the total either way, so nothing on the page contradicts anything else. Exact mid-cycle attribution would need a real ledger table.

Verification

  • Tests: 608 pass, 1 fail, 97 skipped. The single failure is a pre-existing local Postgres connection refusal (ECONNREFUSED 127.0.0.1:5432), identical to the pre-change baseline. +46 tests added, covering waterfall reconciliation, ledger balance anchoring, allowance splitting, identity rollup/aggregation, and money formatting — including precision beyond Number.MAX_SAFE_INTEGER.
  • tsc --noEmit clean; eslint 0 errors (2 pre-existing warnings in an untouched file); next build compiles with all new routes registered.

Existing shared components were extended rather than forked: AppFilterDropdown, UsageBreakdownChart, RequestTable (now exported), and AppSectionBreadcrumb (was dead code, generalized with a section prop).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added app identity listings with usage metrics, API-key status, sorting, and identity detail pages.
    • Added identity-level usage charts, filtering, request history, and linked identity navigation.
    • Added billing cost breakdowns showing plan allowance, credits, and card payments.
    • Added transaction ledgers and improved platform invoice tables with hosted invoice and PDF links.
    • Added invoice hosted URLs and safer payment-method removal.
  • Bug Fixes
    • Improved validation, authorization, unavailable-service handling, and billing fallback behavior.
  • Documentation
    • Added coverage for billing, duration formatting, ledgers, cost allocation, and identity usage.

eliteprox and others added 2 commits July 31, 2026 18:01
M2M identities are billable entities with no home in the IA — there was no
way to answer "which identity burned my allowance?". The meters already
carry the data: all five meters group by client_id + external_user_id, so
this is a UI and query-shaping change with no ingest or meter migration.

- usage-read: aggregateIdentityTotals rolls up per-identity cycle fee,
  request count, billable_secs and last-active; aggregateDailyUserRows
  adds the app x identity chart dimension by re-reading the DAY-window
  rows the dashboard already fetches (no extra meter query).
- identity-rollup: joins meter identities with app_users provisioning and
  api_keys. Union in both directions — metered-but-unprovisioned
  identities stay visible (they are billable) and provisioned-but-idle
  ones stay visible (they are still identities).
- Identities table at /apps/:id/identities, default sorted by fee desc,
  with a per-identity drill-down (usage chart + request log).
- Identity filter and a pipeline/identity chart toggle on both usage
  pages; identity column on the requests table; cross-links from request
  row to identity, identity to API key, and key to app.

The identity request log is served by a new app-scoped endpoint rather
than /api/v1/me/usage/requests, which deliberately rejects
externalUserId to stop viewers reading other identities. App ownership
is a different and correct authorization basis for the same data.

Reuses AppFilterDropdown, UsageBreakdownChart and the shared RequestTable
(now exported) instead of forking new table/filter implementations.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The billing page showed "$25.00 / $25.00 remaining", "$0.655482 accrued" and
"13% of allowance used" side by side with no way to reconcile them, and the
consumption order was only explained in scattered prose.

- CostWaterfall renders the actual settlement order (plan allowance, then
  prepaid credits, then card). The three steps always sum to the cycle
  total, so the figures reconcile by construction rather than by prose.
  Reused on /billing and the usage-page subscription card; the prose
  explanations it replaces are deleted.
- Transactions ledger: credit grants and invoices come from billing
  records; credit consumption is synthesized from daily metered spend
  walked against the plan allowance, because OpenMeter exposes only a
  scalar consumed total. Running balances are assigned backward from the
  live balance so the newest row always equals the balance shown
  elsewhere. Derived rows are labelled as such.
- Platform invoices: human labels ("Usage overage · Jul 2026") with raw
  identifiers moved to an expandable details row; date issued, period
  covered, amount and status columns; $0 invoices hidden behind a toggle
  (default off); pagination.
- Removing the only payment method is now blocked in the UI and in the
  API, so overage invoices always have something to charge. The Remove
  confirmation dialog already existed and is unchanged.

formatUsdMicrosSummary rounds to 2dp (the existing display formatter
truncates, turning $0.655482 into $0.65); full precision stays available
on hover. Phase 5 rolls it out to the remaining surfaces.

Invoice links needed a new path: OpenMeter stores only the Stripe invoice
id and hosted URLs are signed, so they are resolved on click through a new
endpoint rather than N+1 Stripe calls at page load.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@eliteprox, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 36 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c0c5077c-b9df-4edc-80f8-0d97d852eaa0

📥 Commits

Reviewing files that changed from the base of the PR and between 8c83b9a and 5684f59.

📒 Files selected for processing (42)
  • src/app/api/v1/apps/[id]/identities/[externalUserId]/requests/route.ts
  • src/app/api/v1/apps/[id]/identities/route.ts
  • src/app/api/v1/apps/[id]/usage/balance/route.ts
  • src/app/api/v1/apps/[id]/usage/route.ts
  • src/app/api/v1/billing/invoices/[invoiceId]/hosted-url/route.ts
  • src/app/api/v1/me/billing/payment-method/route.ts
  • src/app/api/v1/me/credits/route.ts
  • src/app/api/v1/user/usage/balance/route.test.ts
  • src/app/api/v1/user/usage/balance/route.ts
  • src/app/api/v1/user/usage/requests/route.ts
  • src/app/api/v1/user/usage/route.test.ts
  • src/app/api/v1/user/usage/route.ts
  • src/app/apps/[id]/identities/[externalUserId]/page.tsx
  • src/app/apps/[id]/identities/page.tsx
  • src/components/BillingUsageDashboard.helpers.tsx
  • src/components/BillingUsageDashboard.tsx
  • src/components/OwnerBillingView.tsx
  • src/components/SidebarCreditPreview.tsx
  • src/components/billing/CostWaterfall.tsx
  • src/components/billing/PlatformInvoicesTable.tsx
  • src/components/billing/TransactionsLedger.tsx
  • src/components/identities/IdentitiesTable.tsx
  • src/lib/app-api-keys.ts
  • src/lib/auth/end-user.test.ts
  • src/lib/auth/end-user.ts
  • src/lib/billing-format.ts
  • src/lib/billing-usage-dashboard-data.ts
  • src/lib/billing-utils.ts
  • src/lib/billing/cost-waterfall.test.ts
  • src/lib/billing/cost-waterfall.ts
  • src/lib/billing/owner-billing-pressure.test.ts
  • src/lib/billing/owner-billing-pressure.ts
  • src/lib/billing/transactions-ledger.test.ts
  • src/lib/oidc/mint-user-signer-token.test.ts
  • src/lib/oidc/mint-user-signer-token.ts
  • src/lib/openmeter/billing-consistency.test.ts
  • src/lib/openmeter/billing-consistency.ts
  • src/lib/openmeter/invoices.ts
  • src/lib/openmeter/konnect-credits.ts
  • src/lib/openmeter/owner-payment-method.ts
  • src/lib/openmeter/usage-read.ts
  • src/lib/usage/app-usage-handlers.ts
📝 Walkthrough

Walkthrough

Adds identity-level usage APIs, dashboard pages, request history, and filtering. Extends billing with prepaid-credit allocation, transaction ledgers, invoice access, identity charts, and payment-method safeguards. Adds OpenMeter aggregation, Stripe link retrieval, formatting helpers, and tests.

Changes

Identity analytics

Layer / File(s) Summary
Identity usage aggregation
src/lib/openmeter/usage-read.ts, src/lib/usage/identity-rollup.ts, src/lib/usage/query-openmeter.ts, src/lib/openmeter/identity-usage.test.ts, src/lib/usage/identity-rollup.test.ts
Adds identity totals, daily identity usage, API-key selection, identity sorting, and OpenMeter query support.
Identity APIs and pages
src/app/api/v1/apps/..., src/app/apps/..., src/components/identities/*, src/components/SignedTicketRequestHistory.tsx, src/components/apps/AppSectionBreadcrumb.tsx
Adds authorized identity listing, identity detail pages, paginated request logs, identity tables, and identity navigation.
Identity dashboard integration
src/lib/billing-usage-dashboard-data.ts, src/components/BillingUsageDashboard.tsx, src/components/BillingUsageDashboard.helpers.tsx
Adds identity chart series, identity filtering, identity links, and prepaid-credit data in dashboard payloads.

Billing accounting and presentation

Layer / File(s) Summary
Billing ledger and allocation
src/lib/billing/*, src/lib/openmeter/credit-allowance-summary.ts, src/lib/openmeter/konnect-credits.ts, src/lib/owner-billing-data.ts
Adds plan, credit, and card allocation; chronological ledger construction; owner credit grants; invoice normalization; and ledger payload data.
Billing display and invoice access
src/components/OwnerBillingView.tsx, src/components/billing/*, src/lib/stripe/connect-accounts.ts, src/lib/openmeter/invoices.ts, src/app/api/v1/billing/invoices/[invoiceId]/hosted-url/route.ts, src/app/billing/page.tsx
Adds cost waterfalls, invoice and transaction tables, hosted invoice links, invoice metadata, and billing fallback data.
Billing safeguards and formatting
src/app/api/v1/me/billing/payment-method/route.ts, src/components/OwnerPaymentMethodsCard.tsx, src/lib/billing-format.ts, src/lib/format-usd-micros.ts, src/lib/*.test.ts
Prevents removal of the only payment method and adds tested duration and USD-micros formatting helpers.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.16% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the PR’s main changes: first-class identities and billing improvements across Phases 1–2.
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.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/usage-billing-phase1-identities

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.

Make the intentional pressure surface visible: derive solvent/blocked/
chargeable from wallet spendable + payment methods, show a hard banner
and blocked waterfall row on /billing, echo the same state on /usage and
in the sidebar, and align mint 402 copy with “Payment method required”.
@eliteprox
eliteprox temporarily deployed to vercel / preview July 31, 2026 22:11 — with GitHub Actions Inactive
Port the auth/usage core from #234 so /api/v1/user/usage* works with a
normal pmth_* Bearer (subject from the credential), without requiring
the composite app_*_* presented form. Shared OpenMeter usage handlers
keep Builder/legacy and end-user mounts in sync; composite remains
optional for signer routing.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 19

🧹 Nitpick comments (3)
src/lib/billing/transactions-ledger.test.ts (1)

195-200: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Optional: assert the to filter bound.

The tests cover types and from, but not to. The UI in TransactionsLedger.tsx appends T23:59:59.999Z to the chosen to date, and usage rows are dated at exactly T23:59:59.999Z. That inclusive boundary is easy to break. Add one assertion for it.

💚 Proposed test addition
   assert.equal(filterLedgerEntries(entries, {}).length, 2);
+  // The UI extends `to` to end-of-day; the same-day usage row must stay in.
+  assert.equal(
+    filterLedgerEntries(entries, { to: "2026-07-20T23:59:59.999Z" }).length,
+    2,
+  );
+  assert.equal(filterLedgerEntries(entries, { to: "2026-07-02" }).length, 1);
🤖 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 `@src/lib/billing/transactions-ledger.test.ts` around lines 195 - 200, Add an
assertion in the ledger filter tests around filterLedgerEntries that verifies
the `to` date is inclusive at exactly `T23:59:59.999Z`, preserving the expected
matching entry count for that boundary.
src/lib/openmeter/credit-allowance-summary.ts (1)

291-305: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Optional: extract the shared owner-wallet prelude.

Lines 291-305 repeat the guard sequence and customer-key resolution already present in getOwnerPrepaidCreditBalance (lines 229-243): admin-client check, trim, API-key read, shouldUseKonnectRoutes, buildOwnerCustomerKey. A small helper that returns { client, customerId, apiKey } | null would keep both lookups aligned when the route-mode rules change.

🤖 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 `@src/lib/openmeter/credit-allowance-summary.ts` around lines 291 - 305,
Extract the repeated owner-wallet setup from the current lookup and
getOwnerPrepaidCreditBalance into a shared helper that returns { client,
customerId, apiKey } or null. Keep the existing admin-client, trimmed owner ID,
API-key, shouldUseKonnectRoutes, and buildOwnerCustomerKey checks in that
helper, then update both callers to use its result while preserving their
existing lookup behavior.
src/components/billing/TransactionsLedger.tsx (1)

40-48: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Three independent date formatters share the same locale/timeZone gap. formatEntryDate, formatPeriodRange, and formatIssuedAt each call toLocaleDateString without a fixed locale (and two of the three without a fixed timeZone), in client components rendered under a force-dynamic SSR page. This risks hydration mismatches and, where timeZone is missing, an actual date-value shift depending on server vs. browser timezone. Consider extracting one shared helper (e.g. in src/lib/billing-format.ts, alongside formatBillableDuration) that fixes both locale and timeZone, and reuse it in all three call sites.

  • src/components/billing/TransactionsLedger.tsx#L40-L48: fix formatEntryDate to pass an explicit locale and timeZone to toLocaleDateString, or switch to the shared helper.
  • src/components/billing/PlatformInvoicesTable.tsx#L44-L64: fix formatPeriodRange to pass an explicit locale alongside its existing timeZone: "UTC", or switch to the shared helper.
  • src/components/billing/PlatformInvoicesTable.tsx#L66-L75: fix formatIssuedAt to add both timeZone: "UTC" and an explicit locale, or switch to the shared helper.
🤖 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 `@src/components/billing/TransactionsLedger.tsx` around lines 40 - 48,
Standardize the three billing date formatters on an explicit locale and UTC time
zone, preferably through one shared helper. Update formatEntryDate in
src/components/billing/TransactionsLedger.tsx lines 40-48, formatPeriodRange in
src/components/billing/PlatformInvoicesTable.tsx lines 44-64, and formatIssuedAt
in src/components/billing/PlatformInvoicesTable.tsx lines 66-75; preserve the
existing formatting and invalid-date behavior.

Source: Linters/SAST tools

🤖 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 `@src/app/api/v1/apps/`[id]/identities/[externalUserId]/requests/route.ts:
- Around line 29-33: Update the externalUserId normalization in the route
handler to avoid unguarded double decoding: prefer the already-decoded
rawExternalUserId, or perform decodeURIComponent only within try-catch if
encoded input must be supported. Preserve trimming and return the existing 400
response for empty values, while converting malformed encoding into the route’s
appropriate validation response rather than allowing an exception.

In `@src/app/api/v1/apps/`[id]/identities/route.ts:
- Around line 47-60: Update the date validation in the identities route around
startDate and endDate to reject ranges where startDate is after endDate and
reject ranges exceeding the supported maximum span before calling
listAppIdentities. Parse the dates once, apply both validity and range-width
checks, and return the existing 400 Invalid date range response for any failure.

In `@src/app/api/v1/billing/invoices/`[invoiceId]/hosted-url/route.ts:
- Around line 44-54: Update the ownership validation around
listOwnerWalletInvoices so it does not limit resolution to the first 100
invoices. Use a direct invoice lookup constrained to the owner’s customer IDs,
or paginate repeatedly until decodedId is found, while preserving the existing
404 response for invoices not owned by the user.
- Around line 39-42: Update the hosted invoice URL route to decode invoiceId
once inside its existing error-handling flow, using the decodedId value for
validation and Stripe ID matching. Catch URI decoding failures and return the
same 400 response before any invoice lookup, while preserving the existing
behavior for valid decoded IDs.

In `@src/app/api/v1/me/billing/payment-method/route.ts`:
- Around line 144-156: Update the safeguard around listOwnerPaymentMethods so an
empty result is treated as unverifiable and the removal request is rejected
rather than proceeding; preserve the existing rejection for removing the sole
known method. Also enforce the same last-method invariant inside
unlinkOwnerPaymentMethod, where payment methods are read and mutated atomically,
so concurrent deletions cannot both pass the route-level check.

In `@src/app/apps/`[id]/identities/[externalUserId]/page.tsx:
- Around line 97-104: Update listAppIdentities in
src/app/apps/[id]/identities/[externalUserId]/page.tsx lines 97-104 to catch
failures and resolve to an empty array, matching the adjacent query fallback.
Apply the same change to listAppIdentities in
src/app/apps/[id]/identities/page.tsx lines 32-38.
- Around line 83-91: Log the caught authorization-resolution error before
calling notFound() in both getAuthorizedProviderApp try/catch blocks:
src/app/apps/[id]/identities/[externalUserId]/page.tsx lines 83-91 and
src/app/apps/[id]/identities/page.tsx lines 19-27. Preserve the existing null
assignment and 404 behavior while including the caught error and clear context
in the operator-visible log.
- Around line 80-81: Update the parameter decoding in the page component around
rawExternalUserId to catch URIError from decodeURIComponent and call notFound()
when decoding fails, before the authorization flow continues. Preserve the
existing decoded externalUserId behavior for valid route segments.

In `@src/components/billing/PlatformInvoicesTable.tsx`:
- Around line 66-75: Update formatIssuedAt to pass timeZone: "UTC" in the
toLocaleDateString options, preserving the existing formatting and invalid-date
fallback.
- Around line 44-64: Update formatPeriodRange to pass an explicit locale such as
"en-US" to both toLocaleDateString calls for startLabel and endLabel, preserving
the existing formatting options and date-range behavior.

In `@src/components/billing/TransactionsLedger.tsx`:
- Around line 91-93: Update the hasDerived computation in TransactionsLedger to
inspect the full filtered collection rather than the paginated page. Keep page
for visible-row rendering, but derive the footnote condition from filtered so it
remains consistent as “Show more” changes pagination.
- Around line 40-48: Update formatEntryDate to specify an explicit timezone in
its toLocaleDateString options, using the intended application timezone or UTC,
so server and client render the same calendar day while preserving the existing
invalid-date fallback.

In `@src/components/identities/IdentitiesTable.tsx`:
- Around line 52-61: Update formatLastActive to pass an explicit stable locale
such as "en-US" to toLocaleDateString instead of undefined, while preserving the
existing UTC timezone and invalid-date handling.

In `@src/components/OwnerBillingView.tsx`:
- Around line 200-211: Update OwnerBillingView’s subscription-row rendering to
maintain a running remaining prepaid-credit balance across the .map sequence,
passing each SubscriptionCard the current remainder and decrementing it by the
credits applied for that card. Preserve the existing null/default behavior when
no allowance exists, and add a test covering multiple subscription rows sharing
a single credit balance.

In `@src/lib/billing-usage-dashboard-data.ts`:
- Around line 464-478: Cap identity series generation to the highest-request
identities before constructing or returning chartSeriesByIdentity. Update the
loop that populates identitySeriesMeta and identitySeriesDayCounts, using each
app/externalUserId pair’s total request count to rank candidates and retaining
only the configured chart-series limit, so buildChartSeries does not materialize
discarded identities.

In `@src/lib/billing/transactions-ledger.ts`:
- Around line 187-204: The invoice ledger drafts incorrectly populate
hostedInvoiceUrl from unavailable Stripe metadata; update the invoice-link flow
around the ledger draft construction and TransactionsLedger.tsx to fetch the
hosted URL on demand through /api/v1/billing/invoices/${id}/hosted-url, or
remove hostedInvoiceUrl from the ledger model and rendering if links are not
needed. Do not continue sending null-valued hostedInvoiceUrl fields.
- Around line 206-227: The ledger currently derives balances from
endingCreditBalanceUsdMicros even when input events are incomplete. In
src/lib/billing/transactions-ledger.ts lines 206-227, update buildLedgerEntries
to accept an explicit completeness state and emit null balanceUsdMicros when
completeness cannot be guaranteed; in src/lib/owner-billing-data.ts lines
740-758, track grant and daily-usage soft timeouts and pass that state instead
of a bare empty list; in src/lib/openmeter/credit-allowance-summary.ts lines
312-317, parse grant amounts independently so malformed rows do not discard
valid grants.

In `@src/lib/openmeter/credit-allowance-summary.ts`:
- Around line 288-311: Update listOwnerCreditGrants and the
listKonnectCreditGrants flow to fetch every grants page, starting with the
existing request and continuing until a response contains fewer than 100
records; accumulate all returned grants before passing them to
buildLedgerEntries, preserving the current behavior for customers with 100 or
fewer grants.

In `@src/lib/openmeter/konnect-credits.ts`:
- Around line 36-53: Update konnectGrantTimestamp to return the parsed timestamp
normalized to ISO format rather than the raw trimmed candidate. Preserve the
existing candidate order and null behavior, and ensure buildLedgerEntries
receives consistently ISO-formatted dates for sorting.

---

Nitpick comments:
In `@src/components/billing/TransactionsLedger.tsx`:
- Around line 40-48: Standardize the three billing date formatters on an
explicit locale and UTC time zone, preferably through one shared helper. Update
formatEntryDate in src/components/billing/TransactionsLedger.tsx lines 40-48,
formatPeriodRange in src/components/billing/PlatformInvoicesTable.tsx lines
44-64, and formatIssuedAt in src/components/billing/PlatformInvoicesTable.tsx
lines 66-75; preserve the existing formatting and invalid-date behavior.

In `@src/lib/billing/transactions-ledger.test.ts`:
- Around line 195-200: Add an assertion in the ledger filter tests around
filterLedgerEntries that verifies the `to` date is inclusive at exactly
`T23:59:59.999Z`, preserving the expected matching entry count for that
boundary.

In `@src/lib/openmeter/credit-allowance-summary.ts`:
- Around line 291-305: Extract the repeated owner-wallet setup from the current
lookup and getOwnerPrepaidCreditBalance into a shared helper that returns {
client, customerId, apiKey } or null. Keep the existing admin-client, trimmed
owner ID, API-key, shouldUseKonnectRoutes, and buildOwnerCustomerKey checks in
that helper, then update both callers to use its result while preserving their
existing lookup behavior.
🪄 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: CHILL

Plan: Pro Plus

Run ID: c742b2a1-5e6b-4155-84d6-efaadb370a02

📥 Commits

Reviewing files that changed from the base of the PR and between bd396eb and 8c83b9a.

📒 Files selected for processing (37)
  • src/app/api/v1/apps/[id]/identities/[externalUserId]/requests/route.ts
  • src/app/api/v1/apps/[id]/identities/route.ts
  • src/app/api/v1/billing/invoices/[invoiceId]/hosted-url/route.ts
  • src/app/api/v1/me/billing/payment-method/route.ts
  • src/app/apps/[id]/identities/[externalUserId]/page.tsx
  • src/app/apps/[id]/identities/page.tsx
  • src/app/billing/page.tsx
  • src/components/BillingUsageDashboard.helpers.tsx
  • src/components/BillingUsageDashboard.tsx
  • src/components/OwnerBillingView.tsx
  • src/components/OwnerPaymentMethodsCard.tsx
  • src/components/SignedTicketRequestHistory.tsx
  • src/components/apps/AppSectionBreadcrumb.tsx
  • src/components/billing/CostWaterfall.tsx
  • src/components/billing/PlatformInvoicesTable.tsx
  • src/components/billing/TransactionsLedger.tsx
  • src/components/identities/IdentitiesTable.tsx
  • src/components/identities/IdentityRequestLog.tsx
  • src/lib/billing-format.test.ts
  • src/lib/billing-format.ts
  • src/lib/billing-usage-dashboard-data.ts
  • src/lib/billing/cost-waterfall.test.ts
  • src/lib/billing/cost-waterfall.ts
  • src/lib/billing/transactions-ledger.test.ts
  • src/lib/billing/transactions-ledger.ts
  • src/lib/format-usd-micros.test.ts
  • src/lib/format-usd-micros.ts
  • src/lib/openmeter/credit-allowance-summary.ts
  • src/lib/openmeter/identity-usage.test.ts
  • src/lib/openmeter/invoices.ts
  • src/lib/openmeter/konnect-credits.ts
  • src/lib/openmeter/usage-read.ts
  • src/lib/owner-billing-data.ts
  • src/lib/stripe/connect-accounts.ts
  • src/lib/usage/identity-rollup.test.ts
  • src/lib/usage/identity-rollup.ts
  • src/lib/usage/query-openmeter.ts

Comment thread src/app/api/v1/apps/[id]/identities/route.ts
Comment thread src/app/api/v1/billing/invoices/[invoiceId]/hosted-url/route.ts Outdated
Comment thread src/app/api/v1/billing/invoices/[invoiceId]/hosted-url/route.ts Outdated
Comment thread src/app/api/v1/me/billing/payment-method/route.ts
Comment thread src/lib/billing-usage-dashboard-data.ts
Comment thread src/lib/billing/transactions-ledger.ts
Comment thread src/lib/billing/transactions-ledger.ts
Comment thread src/lib/openmeter/credit-allowance-summary.ts
Comment thread src/lib/openmeter/konnect-credits.ts

Copilot AI 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.

Pull request overview

Implements Phases 1–2 of the Usage & Billing overhaul by making per-app identities a first-class, explorable dimension in usage surfaces and by restructuring billing UI around reconciliation-by-construction (waterfall + ledger + invoices table).

Changes:

  • Adds identity-level aggregation/rollups and new app-scoped identities pages (table + per-identity detail with chart and request log).
  • Replaces scattered billing prose with a CostWaterfall component, introduces a transactions ledger, and upgrades the platform invoices UI (including on-demand Stripe hosted invoice link resolution).
  • Tightens and broadens end-user auth to support resolving app API keys from Bearer tokens (bare and composite), plus blocks removing the only payment method (UI + API).

Reviewed changes

Copilot reviewed 56 out of 56 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/lib/usage/query-openmeter.ts Exposes new identity totals query + aggregator exports.
src/lib/usage/identity-rollup.ts Builds per-app identity rows by unioning OpenMeter totals with app_users and api_keys.
src/lib/usage/identity-rollup.test.ts Covers identity rollup semantics, key selection, and sorting.
src/lib/usage/app-usage-handlers.ts Centralizes app usage/balance route handling and authorization.
src/lib/stripe/connect-accounts.ts Adds Stripe platform invoice link retrieval helper.
src/lib/owner-billing-data.ts Adds ledger build inputs (daily usage, grants, invoices) and emits ledger in payload.
src/lib/openmeter/usage-read.ts Adds identity daily series + identity totals aggregation + OpenMeter identity totals query/stubs.
src/lib/openmeter/konnect-credits.ts Exposes grant row type and timestamp normalization helper for ledger ordering.
src/lib/openmeter/invoices.ts Adds external Stripe invoice id to invoice DTOs.
src/lib/openmeter/identity-usage.test.ts Tests identity totals + daily identity aggregation logic.
src/lib/openmeter/credit-allowance-summary.ts Adds owner credit-grant listing for ledger inputs.
src/lib/openmeter/billing-consistency.ts Updates gate-consistency message to match new UX wording.
src/lib/openmeter/billing-consistency.test.ts Updates expected message for consistency classification.
src/lib/oidc/mint-user-signer-token.ts Updates allowance-gate message text.
src/lib/oidc/mint-user-signer-token.test.ts Updates expected allowance-gate message text.
src/lib/format-usd-micros.ts Adds summary rounding formatter + exact hover title helper.
src/lib/format-usd-micros.test.ts Tests summary rounding and hover-title formatting.
src/lib/billing/transactions-ledger.ts Implements synthesized usage burn + anchored running-balance ledger model.
src/lib/billing/transactions-ledger.test.ts Tests allowance splitting, ordering, refunds, and balance anchoring.
src/lib/billing/owner-billing-pressure.ts Encodes “solvent/blocked/chargeable” state for card-attachment pressure UI.
src/lib/billing/owner-billing-pressure.test.ts Tests billing pressure resolution and spendable computation.
src/lib/billing/cost-waterfall.ts Implements settlement-order waterfall computation (plan → credits → card).
src/lib/billing/cost-waterfall.test.ts Verifies reconciliation invariant and payment-method label formatting.
src/lib/billing-usage-dashboard-data.ts Adds identity chart series support + credit/payment-method data to payload.
src/lib/billing-format.ts Adds compact formatting for billable_secs duration display.
src/lib/billing-format.test.ts Tests billable duration formatting rules and empty states.
src/lib/auth/end-user.ts Enables Bearer API key resolution for end-user routes (bare + composite).
src/lib/auth/end-user.test.ts Adds integration tests for Bearer key resolution/auth.
src/lib/app-api-keys.ts Adds Bearer key resolver and refactors key lookup to support expected client validation.
src/components/SignedTicketRequestHistory.tsx Exports RequestTable and adds optional identity column with deep links.
src/components/SidebarCreditPreview.tsx Shows blocked “payment method required” nudge based on billing pressure.
src/components/OwnerPaymentMethodsCard.tsx Disables removing the only payment method and explains why.
src/components/OwnerBillingView.tsx Replaces prose with CostWaterfall, adds invoice table + ledger, adds blocked-state UI.
src/components/identities/IdentityRequestLog.tsx New identity-scoped request log component using exported RequestTable.
src/components/identities/IdentitiesTable.tsx New identities table with sorting and drill-down links.
src/components/BillingUsageDashboard.tsx Adds identity chart dimension toggle + identity filtering + embedded waterfall summary.
src/components/BillingUsageDashboard.helpers.tsx Adds identities cross-link and links user rows to identity drill-down.
src/components/billing/TransactionsLedger.tsx New ledger UI with filtering and pagination.
src/components/billing/PlatformInvoicesTable.tsx New invoices table with toggles, details expansion, and on-demand links.
src/components/billing/CostWaterfall.tsx New waterfall UI component rendered on billing and usage pages.
src/components/apps/AppSectionBreadcrumb.tsx Generalizes breadcrumb to support section + optional parent section.
src/app/billing/page.tsx Ensures billing payload includes ledger field in fallback state.
src/app/apps/[id]/identities/page.tsx New app identities index page.
src/app/apps/[id]/identities/[externalUserId]/page.tsx New per-identity detail page (chart + request log).
src/app/api/v1/user/usage/route.ts Refactors to shared handler and expands auth contract description.
src/app/api/v1/user/usage/route.test.ts Adds tests for Bearer key auth and user scoping.
src/app/api/v1/user/usage/requests/route.ts Updates auth contract comment to include API keys.
src/app/api/v1/user/usage/balance/route.ts Refactors to shared handler and updates param parsing.
src/app/api/v1/user/usage/balance/route.test.ts Adds bare Bearer override-rejection test coverage.
src/app/api/v1/me/credits/route.ts Returns billingPressure to support sidebar blocked-state nudge.
src/app/api/v1/me/billing/payment-method/route.ts Blocks deleting the only payment method at the API layer.
src/app/api/v1/billing/invoices/[invoiceId]/hosted-url/route.ts New on-demand Stripe hosted invoice link resolver endpoint.
src/app/api/v1/apps/[id]/usage/route.ts Refactors legacy app usage route to shared handler + shared auth.
src/app/api/v1/apps/[id]/usage/balance/route.ts Refactors legacy app balance route to shared handler + shared auth.
src/app/api/v1/apps/[id]/identities/route.ts New app identities API route with date-range validation.
src/app/api/v1/apps/[id]/identities/[externalUserId]/requests/route.ts New app-scoped identity request log endpoint (app-ownership authorized).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/app/api/v1/billing/invoices/[invoiceId]/hosted-url/route.ts Outdated
Comment thread src/components/BillingUsageDashboard.helpers.tsx Outdated
Comment thread src/lib/app-api-keys.ts
Reduce cognitive complexity on billing/usage surfaces, fail closed on
payment-method removal and invoice ownership lookup, and harden identity
routes, credit allocation, and date formatting from the latest review.
@eliteprox
eliteprox temporarily deployed to vercel / preview July 31, 2026 22:27 — with GitHub Actions Inactive
@eliteprox

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@sonarqubecloud

Copy link
Copy Markdown

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