Repository navigation
feat(billing): reconcile legacy consumption by credit type - #1236
Conversation
Review: legacy credit reconciliationI found no critical issues. The writes run in a single transaction and take the existing org accounting lock, so concurrent writers are serialized. The idempotency replay is checked before any mutable state, dry runs roll back, and the post-write Minor, non-blocking:
✅ |
There was a problem hiding this comment.
🟡 Changes recommended
The migration introduces unclassified plaintext and JSON columns that break database-encryption verification.
2 open findings
What changed in this PR
Adds an audited admin workflow for assigning reviewed legacy consumption to specific credit types while preserving aggregate capacity.
Changes:
- Adds transactional reconciliation with previews and idempotency.
- Coordinates reconciliation with historical discounts.
- Adds API routing, OpenAPI documentation, tests, and operational guidance.
| File | Description |
|---|---|
docs/credit-allocation.md |
Documents reconciliation semantics and usage. |
crates/database/tests/credit_allocation.rs |
Tests balances, retries, concurrency, and discounts. |
crates/database/src/repositories/organization_usage_discount.rs |
Blocks incompatible historical discounts. |
crates/database/src/repositories/mod.rs |
Exports the repository module. |
crates/database/src/repositories/credit_reconciliation.rs |
Implements transactional reconciliation. |
crates/database/src/migrations/sql/V0093__legacy_credit_reconciliations.sql |
Adds the audit ledger. |
crates/api/tests/e2e_all/main.rs |
Registers the E2E module. |
crates/api/tests/e2e_all/admin_credit_reconciliation.rs |
Tests authentication and API behavior. |
crates/api/src/routes/mod.rs |
Exports the route module. |
crates/api/src/routes/admin_credit_reconciliation.rs |
Defines request, response, and handler logic. |
crates/api/src/openapi.rs |
Registers endpoint schemas. |
crates/api/src/lib.rs |
Mounts the admin endpoint. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Review · Status🟩 CompletedIronLoop completed the review and posted it to GitHub. ResultRun detailsAutomatic trigger · attempt 1 of 3 · completed in 13m |
There was a problem hiding this comment.
Review · Summary
🟢 No actionable findings
No new actionable findings identified in the reviewed change.
Validation
- ✅ Static review — Reviewed the route, authorization, accounting transaction and locking, allocation integration, discount coordination, migration, tests, and documentation.
Review details
- Run:
c5fd97de-2fa2-4be8-862c-daa75f52c624 - Attempts: 1
hanakannzashi
left a comment
There was a problem hiding this comment.
Approved after current-head review. Verified transactional reconciliation, idempotency, accounting locking, discount coordination, and successful CI.
|
The logic looks right to me: one transaction under the organization accounting lock, replay checked before any mutable state, and aggregate capacity is the same before and after. Two things before merge, and one question. 1. Drop the compatibility code for the earlier snapshot shape.
2. Add the step order for moving an existing organization to postpay to the doc. The section is titled "before enabling postpay", but every selected type needs an active limit with enough remaining capacity. So:
A short numbered sequence (dry run, commit, then the limit changes) would avoid a 409, or an order that leaves the organization without capacity. Question. Usage posted since the allocation ledger went live is already allocated per request, to Note: #1237 also adds a |
|
Addressed in d2356f6:
Yes, already-attributed usage intentionally keeps its recorded type. Grant allocations written since the ledger went live remain grant allocations after a postpay switch. This endpoint only assigns #1237 is still open; the V0094 migration must be renumbered in whichever PR merges second. Validation: formatting, all-target/all-feature API compilation, and the remaining reconciliation unit test pass. The earlier balance-field change also passed the PostgreSQL reconciliation endpoint test. Latest CI is running. |


Addresses the balance and future credit-allocation issue in #1235: existing organizations can have usage recorded before credit-type attribution was introduced. When postpay is later added, that historical spend remains unattributed and does not reduce the correct per-type balance. Future usage can therefore consume prepaid credits that were already spent and receive an incorrect credit-type allocation.
Adds
POST /v1/admin/organizations/{org_id}/credit/reconciliationsto move reviewed legacy consumption into per-type counters. For example, a $100 payment limit, $200 postpay limit and $80 unknown spend can be reconciled to payment remaining $20 and postpay remaining $200, preserving aggregate capacity at $220.legacy_unattributed_amountin balance responses, including organizations without active credit types.The migration only creates the audit table. No organization is reconciled automatically. Original usage logs and saved allocations remain unchanged; this addresses the balance/future-allocation portion of #1235, not historical invoice attribution or allocation-policy changes. Partial reconciliation leaves remaining history unresolved.
Validation:
cargo fmt --all -- --checkcargo check -p api --all-targets --offlinecargo clippy -p api -p database --all-targets --all-features --offline -- -D warningsPostgreSQL 16:
cargo test -p database --test credit_allocation --offline -- --test-threads=4— 26 passed, including lock-timeout conflicts, retired-credit snapshots, normalized retries, mixed/partial preview, concurrent writes and discount regression cases.PostgreSQL 16:
cargo test -p api --test e2e_all admin_credit_reconciliation --offline -- --test-threads=1— 1 passed, covering authentication, token permissions, validation, preview, commit and audit identity.cargo test -p database --lib credit_reconciliation --offline— 1 passed, covering count validation. Unreleased snapshot compatibility code has been removed.PostgreSQL 16:
cargo test -p api --test e2e_all database_encryption:: --offline -- --test-threads=1— 10 passed. The audit table’s textual/JSON fields have explicit restricted billing-data classification and coverage.cargo check -p api --all-targets --all-features --locked— passed after updating transitivequinn-protofrom 0.11.15 to patched 0.11.18, fixing the five advisories reported by CI.Refs #1235, #704.