Skip to content

fix: account for legacy spend in single-type credit balances - #1050

Open
think-in-universe wants to merge 2 commits into
mainfrom
fix/legacy-credit-remaining
Open

think-in-universe wants to merge 2 commits into
mainfrom
fix/legacy-credit-remaining

Conversation

@think-in-universe

@think-in-universe think-in-universe commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Summary

  • fold legacy unattributed spend into the per-type balance when an organization has exactly one active credit type
  • keep multi-type legacy spend unattributed rather than inventing a historical split
  • expose legacy_unattributed_amount on the organization balance API so multi-type clients can reconcile their per-type totals
  • add regression coverage for both single-type and multi-type legacy-spend scenarios

For a payment-only organization with 100 credited, 90 consumed before the credit-allocation rollout, and 1 consumed afterward, the balance now reports 91 consumed and 9 remaining instead of 1 consumed and 99 remaining.

For organizations with multiple active credit types, clients can reconcile the response as sum(credit type available) - legacy_unattributed_amount = aggregate remaining.

Validation

  • cargo fmt --all -- --check
  • cargo test -p database --test credit_allocation (18 passed)
  • cargo check -p api
  • cargo clippy -p api -p database -p services -- -D warnings

Follow-up to #1048 and supports the per-type balance display in nearai/nearai-cloud-ui#347.

Copilot AI lite review requested due to automatic review settings September 11, 2026 12:51
@think-in-universe
think-in-universe deployed to Cloud API test env September 11, 2026 12:51 — with GitHub Actions Active

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

🟢 Approval recommended

No unresolved blocking issues were identified.

Pull request overview

Updates single-type credit balance reporting to include legacy unattributed spend while preserving ambiguity for multi-type organizations.

Changes:

  • Fold legacy spend into the sole active credit type.
  • Add regression coverage for balance consistency.
File summaries
File Description
crates/database/tests/credit_allocation.rs Adds regression coverage for legacy spend.
crates/database/src/repositories/organization_limits_repository_impl.rs Adjusts single-type balance calculations.
Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 0
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@claude

claude Bot commented Sep 11, 2026

Copy link
Copy Markdown

Review — no blocking issues

I verified the correctness claim end to end rather than just the diff:

  • idx_org_limits_active_by_type (V0044) is a unique partial index on (organization_id, credit_type) WHERE effective_until IS NULL, and both writers (update_limits, update_staking_farm_limit) close the same-type row inside the transaction. So if let [status] = limits.as_mut_slice() is a sound test for "exactly one active credit type" — it cannot accidentally match a duplicate row for one type, nor miss a second type.
  • The folded math agrees with get_current_limits in every case, not just the happy path. Aggregate computes max(max(L-C,0) - U, 0); the breakdown now computes max(L-(C+U), 0). These are equal for all L, C, U >= 0 (both collapse to 0 when L < C), so the two endpoints can't drift apart for single-type orgs.
  • No double counting: legacy_unattributed_amount is a one-shot snapshot from V0078 and is never decremented into organization_credit_consumption. retry_preserves_unknown_attribution_for_legacy_usage already pins the retry path that could have violated that. settle_unfunded_usage only drains unresolved_unfunded_amount, which starts at 0 at migration time.
  • This is display-only — get_current_limit_breakdown reaches only GET /v1/organizations/{id}/balance via compute_organization_balance_response. Admission control still goes through get_current_limits, which is unchanged, so there is no enforcement or rolling-update risk.

Prior review on this PR (Copilot, lite) generated no comments; nothing to build on there.

Non-blocking notes

1. consumed + available == amount no longer holds for the folded type. Worth flagging to the consumer in nearai/nearai-cloud-ui#347, since a progress bar computing consumed / amount or amount - consumed will now go past 100% / negative.

Concrete case — payment-only org, ceiling reduced from 200 to 100 by a refund sync through PATCH /v1/admin/organizations/{org_id}/limits, with 150 of legacy spend:

```json
{ "type": "payment", "amount": 100, "consumed": 150, "available": 0 }
```

`available` is correctly floored and matches aggregate `remaining`, but `consumed` exceeds `amount`. Clamping `consumed` to `amount` in the repository would restore the identity at the cost of hiding real overage, so leaving it is defensible — just make sure the UI clamps.

2. Only the single-type branch is covered. single_credit_type_breakdown_includes_legacy_spend is a good regression test, but nothing pins the documented multi-type behavior (legacy stays unattributed, breakdown intentionally does not reconcile with aggregate). A sibling test setting two active types with non-zero legacy_unattributed_amount and asserting each type's consumed excludes it would keep a future refactor from silently spreading legacy spend across types.

3. Pre-existing, not introduced here: compute_organization_balance_response fires get_limit and get_credit_limits as two independent REPEATABLE READ snapshots under tokio::try_join!, so a concurrent charge can make aggregate and breakdown disagree by one request's cost in a single response. Same-snapshot consistency would need one repository call returning both.

@ironloopai

ironloopai Bot commented Sep 11, 2026

Copy link
Copy Markdown

Review · Status

🟩 Completed

IronLoop completed the review and posted it to GitHub.

Result

Open submitted review →

Run details
  • Run: d6d1e117-3dcf-4223-80e8-55c581ba4293
  • Base: main at 96ae228
  • Head: fix/legacy-credit-remaining at 97bbcae
  • Created: 2026-09-11 12:56 UTC
  • Updated: 2026-09-11 12:59 UTC

Automatic trigger · attempt 1 of 3 · completed in 3m 23s

@ironloopai ironloopai 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.

Review · Summary

🟢 No actionable findings

No actionable issues found in the reviewed change.

Validation
  • Single-type accounting inspection — Static tracing confirmed the breakdown applies legacy spend only for one active credit type and computes the same remaining capacity as the aggregate balance.
Review details
  • Run: d6d1e117-3dcf-4223-80e8-55c581ba4293
  • Attempts: 1

@github-actions github-actions 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.

🔍 OpenCodeReview found 2 issue(s) in this PR.

  • ✅ 2 posted as inline comment(s)
  • 📝 0 posted as summary

@think-in-universe
think-in-universe deployed to Cloud API test env September 11, 2026 13:25 — with GitHub Actions Active
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