Skip to content

fix(billing): keep historical usage priced after price removal - #1702

Merged
ctkm-aelf merged 1 commit into
rollup-2026-09-29-ctkm-1from
fix-missing-usage-costs
Sep 30, 2026
Merged

ctkm-aelf merged 1 commit into
rollup-2026-09-29-ctkm-1from
fix-missing-usage-costs

Conversation

@ctkm-aelf

Copy link
Copy Markdown
Collaborator

Summary

  • Cause of the "Unavailable" cost: removing or re-authoring a service price deleted the retired Lago code's billing_rate_cache row (pricing.rs: complete_price_removal, the lane-cleanup branch, cleanup_components). Historical usage_meter rows recorded under that code could then no longer be priced by GET /api/v1/billing/usage (estimated_credits: null), and the Usage page turned any group or total containing a null into "Unavailable" — for Chrono LLM and for the whole Spend card. Grants/free credits were never the cause; grant funding stayed known from the rows' own consumption records.
  • Backend: price removal now marks the row retired_at instead of deleting it — fresh_rate refuses it for new reservations, every historical reader keeps using it, and a re-synced price replaces the row. get_usage prices historical groups by grant-settled derivation (exact) → cached rate → exact per-row reservation rates (rate × quantity in Decimal128 credits, as exact settlement computes) → unknown, with grant credits still reported.
  • Frontend: totals become lower bounds (≥ n plus an unpriced-record count) when only some records are unpriced; "Unavailable" appears only when no charged record is priced; groups sort by their known sum.
  • Docs: glossary precedence and the admin-usage limitation (admin usage keeps only the cached-rate rule; codes whose rows were deleted before this change stay unknown there), LAGO_SETUP, PLATFORM_KEYS_AND_INFERENCE, USAGE_BILLING_LAGO_SPEC (retired_at, rollout note), ENV, CLAUDE.md rule 5.

Based on rollup-2026-09-29-ctkm-1 (b98949e1), re-implemented on #1692's exact Credits model. Rollout: upgrade all backend replicas before removing prices — old replicas ignore retired_at until the 900 s rate TTL expires. The provenance row for docs/rollups/rollup-2026-09-29-ctkm-1.md and #1700 will be added when this is squash-landed.

Test Plan

  • Existing tests pass — targeted backend suites (billing_integration_tests::usage, services::billing::{pricing,reservation,tests,funding,reconcile,meter,exact_tests}, handlers::services::tests) on a MongoDB 8.0 replica set: 141 passed, 0 failed; cargo clippy -p nyxid --all-targets -- -D warnings clean; cargo fmt clean; frontend npm run lint 0 errors, vitest 66/66 (billing + credits files), npm run build (type-check) passes.
  • New tests: per-row reservation pricing (mixed pico/micro rates, exact allowance = rate × units, a funding: None row makes the group unknown), grant-settled derivation (exact result beats a cached estimate; pre-cutover Int64/amount_micros row; a wallet debit > 0 stays unknown), a retired rate still prices history, a retired rate fails the reservation gate closed, removal retires and re-sync un-retires the rate, frontend lower-bound / Unavailable / free-row rules.
  • Manually tested the affected flows — not exercised against a live Lago; the Usage page behaviour is covered by the page tests.

Review: reviewed by Fable 5.1 (personal pass) plus an Opus adversarial review whose findings were all resolved; GPT-6-Astra sign-off in progress and will be posted here.

Checklist

  • Code follows the project's architecture rules
  • No hardcoded secrets or credentials
  • Error messages do not leak internal details
  • Documentation updated (if applicable)

🤖 Generated with Claude Code

Re-authoring a service price deleted the retired Lago code's
billing_rate_cache row, so every historical usage_meter row recorded
under that code lost its only price and GET /billing/usage returned
null costs; the Usage page then showed Unavailable for the service and
for the Spend total. Grants were never the cause.

- Price removal marks the rate row retired_at instead of deleting it;
  new reservations refuse retired rates, historical pricing keeps
  reading them, and a re-synced price clears the mark.
- get_usage prices historical groups by grant-settled derivation, then
  the cached rate, then per-row reservation rates (exact Decimal128
  credits, as exact settlement does), else null with grant credits
  still known.
- The Usage page shows lower bounds (≥) with an unpriced-record count
  instead of Unavailable when only some records are unpriced.
- Docs: glossary precedence, admin usage limitation, rollout note.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

📊 Code coverage

Component Lines Threshold Status Δ vs base
Backend (nyxid) 87.41% 73% ✅ 🔺 +0.02
Frontend (vitest) 71.18% 15% ✅ 🔺 +0.01

Gate: line coverage must stay at or above the threshold. Ratchet plan (W21): Backend → 55%, CLI → 50%, Frontend → 30% by quarter end.

@ctkm-aelf
ctkm-aelf merged commit c3d4970 into rollup-2026-09-29-ctkm-1 Sep 30, 2026
34 checks passed
@ctkm-aelf
ctkm-aelf deleted the fix-missing-usage-costs branch September 30, 2026 06:40
ctkm-aelf added a commit that referenced this pull request Sep 30, 2026
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ctkm-aelf added a commit that referenced this pull request Sep 30, 2026
* feat(auth): gate signup invitation codes behind default-on feature flag (#1690)

Signup invitation codes are now an operator choice. The requirement is behind
the global, default-on `auth:invitation-code` feature flag, which replaces the
`INVITE_CODE_REQUIRED` environment variable and can be toggled in
Admin > Feature Flags without a restart.

- Enabled (default): email signup requires a valid code; browser social signup
  can redeem one; native social token exchange still rejects first-time signup.
- Disabled: email and first-time social signup succeed without a code.
- Flag is global only; scoped overrides are rejected. The backend resolves it on
  every signup attempt and /api/v1/public/config reports the effective state.
- Invitation-code admin page, API and CLI are retained; the page states when
  codes are not required. Organization invitations are unchanged.
- Rollout: deploy backend before frontend. Deployments with
  INVITE_CODE_REQUIRED=false must disable the flag to keep signup open.

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

* docs(rollup): record 2026-09-29 ctkm-1 rollup

Records #1690 (default-on auth:invitation-code signup gate), its reviewed
head and squash commit, the main sync to e96a507, rollout notes and
verification.

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

* feat(oauth): redesign consent screen to match connection flows (#1701)

Rework the OAuth consent page into the centered card used by connect links
and channel-bot connections: circular NyxID mark, "Authorize application"
heading with the requesting app highlighted, plain-language permission rows,
and a trust note above Allow/Decline. Client ID, redirect and raw scopes move
under App details. The Low/Medium/High scope badges are removed; they were
client-side labels, not server decisions.

Service access shows each service's catalog description (two lines max) in a
scrollable list with edge fades. Customize toggles a picker that closes via
Done or Save selection. GET /user-services list responses now include
catalog_service_description. Form fields posted to
/oauth/authorize/decision are unchanged.

Adds a dev-only /oauth-consent-preview route with sample data and disabled
decisions.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* test(nyxbot): wait for relayed message instead of a fixed sleep

the_same_question_is_not_worked_on_twice slept 300 ms after a Lark relay
callback that returns 202 before recording the message. The instrumented
coverage build on rollup PR #1700 took longer, so the reply target was not
yet recorded. Poll the home thread (bounded to 10 s) until it is, keeping
the assertions unchanged. Record the follow-up in the rollup notes.

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

* docs(rollup): record #1701 in the 2026-09-29 ctkm-1 rollup

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

* test(auth): allow the code-free registration test 5 s under coverage

The #1690 test 'allows email registration without a code when the flag is
disabled' failed once in the rollup's frontend coverage run: the register
call had not happened within waitFor's default 1 s. Use a 5 s timeout, as
other slow frontend waits do, and record the follow-up in the rollup notes.

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

* fix(billing): keep historical usage priced after price removal (#1702)

Re-authoring a service price deleted the retired Lago code's
billing_rate_cache row, so every historical usage_meter row recorded
under that code lost its only price and GET /billing/usage returned
null costs; the Usage page then showed Unavailable for the service and
for the Spend total. Grants were never the cause.

- Price removal marks the rate row retired_at instead of deleting it;
  new reservations refuse retired rates, historical pricing keeps
  reading them, and a re-synced price clears the mark.
- get_usage prices historical groups by grant-settled derivation, then
  the cached rate, then per-row reservation rates (exact Decimal128
  credits, as exact settlement does), else null with grant credits
  still known.
- The Usage page shows lower bounds (≥) with an unpriced-record count
  instead of Unavailable when only some records are unpriced.
- Docs: glossary precedence, admin usage limitation, rollout note.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* docs(rollup): record #1702 in the 2026-09-29 ctkm-1 rollup

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* ci: build backend test binaries with line-table debuginfo

Backend Test and Backend Billing Smoke died five times out of seven
attempts on the 2026-09-29 rollup head with exit 143 ("The runner has
received a shutdown signal") four to five minutes into compiling the
nyxid test binary, before any test ran. The full-DWARF test binary
exhausts the hosted runner's memory during codegen and linking. Both
jobs now set CARGO_PROFILE_TEST_DEBUG=line-tables-only, which keeps
panic locations and backtraces and changes nothing for local builds or
the coverage jobs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* feat(chat): open connector and channel setup in reusable overlays (#1707)

* feat(chat): open connector and channel setup in reusable overlays

* fix(chat): harden setup popup lifecycle and restore link focus

* feat(channels): add tracked bot setup links and signed callbacks (#1705)

* feat(channels): add tracked bot setup links and signed callbacks

* fix(channels): preserve bot creation calls in production builds

* fix(cli): save bot webhook signing secrets to private files

* ci: bound backend coverage debug information

* docs: record bot-link callbacks and rollup verification

* fix(oauth): make Google Workspace grants optional (#1706)

* fix(oauth): make Google Workspace grants optional

Accept partial Google consent, remove seeded required-scope metadata across Workspace products, and show last reported grants in service details.

* test(oauth): match optional Workspace seed wording

* feat(oauth): show grants for every Google Workspace product

* test(oauth): retain prior scope evidence when response omits scopes

* ci: bound backend coverage debug info for hosted runners

* docs: record Google OAuth grant fix in rollup

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
ctkm-aelf added a commit that referenced this pull request Sep 30, 2026
* rollup: 2026-09-29 ctkm-1 integration into main (#1700)

* feat(auth): gate signup invitation codes behind default-on feature flag (#1690)

Signup invitation codes are now an operator choice. The requirement is behind
the global, default-on `auth:invitation-code` feature flag, which replaces the
`INVITE_CODE_REQUIRED` environment variable and can be toggled in
Admin > Feature Flags without a restart.

- Enabled (default): email signup requires a valid code; browser social signup
  can redeem one; native social token exchange still rejects first-time signup.
- Disabled: email and first-time social signup succeed without a code.
- Flag is global only; scoped overrides are rejected. The backend resolves it on
  every signup attempt and /api/v1/public/config reports the effective state.
- Invitation-code admin page, API and CLI are retained; the page states when
  codes are not required. Organization invitations are unchanged.
- Rollout: deploy backend before frontend. Deployments with
  INVITE_CODE_REQUIRED=false must disable the flag to keep signup open.

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

* docs(rollup): record 2026-09-29 ctkm-1 rollup

Records #1690 (default-on auth:invitation-code signup gate), its reviewed
head and squash commit, the main sync to e96a507, rollout notes and
verification.

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

* feat(oauth): redesign consent screen to match connection flows (#1701)

Rework the OAuth consent page into the centered card used by connect links
and channel-bot connections: circular NyxID mark, "Authorize application"
heading with the requesting app highlighted, plain-language permission rows,
and a trust note above Allow/Decline. Client ID, redirect and raw scopes move
under App details. The Low/Medium/High scope badges are removed; they were
client-side labels, not server decisions.

Service access shows each service's catalog description (two lines max) in a
scrollable list with edge fades. Customize toggles a picker that closes via
Done or Save selection. GET /user-services list responses now include
catalog_service_description. Form fields posted to
/oauth/authorize/decision are unchanged.

Adds a dev-only /oauth-consent-preview route with sample data and disabled
decisions.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* test(nyxbot): wait for relayed message instead of a fixed sleep

the_same_question_is_not_worked_on_twice slept 300 ms after a Lark relay
callback that returns 202 before recording the message. The instrumented
coverage build on rollup PR #1700 took longer, so the reply target was not
yet recorded. Poll the home thread (bounded to 10 s) until it is, keeping
the assertions unchanged. Record the follow-up in the rollup notes.

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

* docs(rollup): record #1701 in the 2026-09-29 ctkm-1 rollup

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

* test(auth): allow the code-free registration test 5 s under coverage

The #1690 test 'allows email registration without a code when the flag is
disabled' failed once in the rollup's frontend coverage run: the register
call had not happened within waitFor's default 1 s. Use a 5 s timeout, as
other slow frontend waits do, and record the follow-up in the rollup notes.

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

* fix(billing): keep historical usage priced after price removal (#1702)

Re-authoring a service price deleted the retired Lago code's
billing_rate_cache row, so every historical usage_meter row recorded
under that code lost its only price and GET /billing/usage returned
null costs; the Usage page then showed Unavailable for the service and
for the Spend total. Grants were never the cause.

- Price removal marks the rate row retired_at instead of deleting it;
  new reservations refuse retired rates, historical pricing keeps
  reading them, and a re-synced price clears the mark.
- get_usage prices historical groups by grant-settled derivation, then
  the cached rate, then per-row reservation rates (exact Decimal128
  credits, as exact settlement does), else null with grant credits
  still known.
- The Usage page shows lower bounds (≥) with an unpriced-record count
  instead of Unavailable when only some records are unpriced.
- Docs: glossary precedence, admin usage limitation, rollout note.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* docs(rollup): record #1702 in the 2026-09-29 ctkm-1 rollup

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* ci: build backend test binaries with line-table debuginfo

Backend Test and Backend Billing Smoke died five times out of seven
attempts on the 2026-09-29 rollup head with exit 143 ("The runner has
received a shutdown signal") four to five minutes into compiling the
nyxid test binary, before any test ran. The full-DWARF test binary
exhausts the hosted runner's memory during codegen and linking. Both
jobs now set CARGO_PROFILE_TEST_DEBUG=line-tables-only, which keeps
panic locations and backtraces and changes nothing for local builds or
the coverage jobs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* feat(chat): open connector and channel setup in reusable overlays (#1707)

* feat(chat): open connector and channel setup in reusable overlays

* fix(chat): harden setup popup lifecycle and restore link focus

* feat(channels): add tracked bot setup links and signed callbacks (#1705)

* feat(channels): add tracked bot setup links and signed callbacks

* fix(channels): preserve bot creation calls in production builds

* fix(cli): save bot webhook signing secrets to private files

* ci: bound backend coverage debug information

* docs: record bot-link callbacks and rollup verification

* fix(oauth): make Google Workspace grants optional (#1706)

* fix(oauth): make Google Workspace grants optional

Accept partial Google consent, remove seeded required-scope metadata across Workspace products, and show last reported grants in service details.

* test(oauth): match optional Workspace seed wording

* feat(oauth): show grants for every Google Workspace product

* test(oauth): retain prior scope evidence when response omits scopes

* ci: bound backend coverage debug info for hosted runners

* docs: record Google OAuth grant fix in rollup

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* feat: NyxBot guests use specialists' services at owner-set levels; Telegram bot creators are owners; Lark groups answer only bot mentions (0.38.0) (#1712)

* fix: a Telegram bot created through NyxID knows its owner: the creating account is verified on link or first message, and Start greets instead of refusing

* fix: only a creator bound by the owner's own setup challenge is trusted, in private chats; link-time grants are audited and NyxBot keeps the verify link as a fallback

* chore: bump version to 0.37.2

* fix: guests use a specialist's services except deleting; Lark groups count only mentions of the bot itself

* fix: guests are refused calls that look like deleting by what they send, and never raise approvals; Lark bot ids cached per app

* fix: guest delete checks cover GETs, body method overrides, unparsable JSON and code fields; guests never run on the owner's approval grants

* feat: owners set what guests may do with each of a specialist's services (read, use, all), from the agent page or by asking NyxBot; destructive operations come from spec markers, not word lists

* test: agent fixtures carry guest_access

* fix: guest access judges every requested method, keeps levels beside grants for rolling deploys, honours read-only specs and catalog markers on mounted specs; Drive trash and content replace are destructive

* fix: grants form builds its request without an unused binding

* fix: guest calls never carry method overrides; Calendar event and Bitable record updates are destructive; a re-granted service starts at the default guest level

* fix: guests' default use is reading, creating and acting (no PUT/PATCH/DELETE); Aevatar's destructive markers stay unchanged and NyxID marks POST edits itself; only stored catalog contracts widen reads; override keys read as PHP reads them

* fix: NyxID's per-operation x-nyxid-changes-existing (true for POST edits, false for PUT actions); override checks follow the body's content type; hosted overlays mounted by URL are catalog contracts; all-level grants say they include deleting

* fix: only a catalog contract may say an operation only acts; method fields count when they name another changing verb, in JSON bodies too; ; separates fields; hosted overlays share one check

* fix: override checks skip empty, text and binary bodies; hosted overlay lookups share NyxID's compiled copy without cloning

* chore: bump version to 0.38.0

---------

Co-authored-by: chrono-kw <chrono-kw@users.noreply.github.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: chronoai-kai <kaiweichronoai@gmail.com>
Co-authored-by: chrono-kw <chrono-kw@users.noreply.github.com>
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