Phase D operability: explain + verify endpoints - #14
Merged
Conversation
Two new operator-facing endpoints answer the questions billing
operators actually need:
GET /v1/accounts/{account_id}/explain?from&to
Returns the breakdown that contributed to an account's total over a
period, plus segment provenance and the corrections that affected
the total. Specifically:
- `lines` — breakdown by (product, meter, model, source, unit)
via the rollup-with-raw-tail path; same SUM/COUNT the regular
/usage endpoint returns but grouped by every billing dimension
- `rollup_segments` / `raw_segments` — segment IDs that overlap
the range for this account's bucket (so an operator can drill
into them later via inspect-segment)
- `corrections` — raw Correction/Retraction events in the range,
returned individually rather than netted
- `watermark_ms` — current rollup watermark so the caller knows
which part of the range is sealed
GET /v1/accounts/{account_id}/verify?from&to
Computes SUM(quantity) two ways (raw scan and rollup) and reports
both totals plus drift = raw - rollup. Drift of zero on a sealed
period (`to <= watermark_ms`, indicated by `period_sealed: true` in
the response) is the invariant. Non-zero drift indicates a rollup
bug, a late event that landed below the watermark, or a missing
rollup segment that operator-driven rebuild_rollups should fix.
These are the building blocks for the spec §10/§19 "explainable
totals" + "reconcilable raw vs rollup" guarantees the external
review called out as essential for a billing-grade store.
Refactored start_server to expose `build_router(state) -> Router` so
the HTTP layer can be tested via `tower::oneshot` (used by the new
tests) without binding a port.
Tests (tests/explain_verify.rs, 6 tests):
- explain_breaks_down_by_billing_dimensions
- explain_surfaces_corrections_separately (Correction event listed
individually + net total = 100 - 40 = 60)
- explain_rejects_invalid_dates
- verify_reports_zero_drift_when_rollup_matches_raw
- verify_detects_drift_when_rollup_misses_late_event (late event
after watermark advance ⇒ drift = 50)
- verify_reports_period_sealed_status
Adds http-body-util as a dev-dependency for axum response body
parsing in tests.
Total tests: 88 (was 82; +6). Clean under RUSTFLAGS=-D warnings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two operator-facing endpoints that answer the questions billing operators actually need — "where did this total come from?" and "do my rollups match the raw events?". Foundations for the spec §19.8 ("rollup totals reconcile with raw audit") and §10/§19 ("explainable totals") guarantees the external review called out as essential.
New endpoints
GET /v1/accounts/{account_id}/explain?from&toReturns the breakdown that contributed to an account's total over a period, plus segment provenance and the corrections that affected the total.
{ "account_id": "acc_x", "from_ms": 1700000000000, "to_ms": 1702000000000, "watermark_ms": 1701900000000, "lines": [ { "product_id": "...", "meter_id": "...", "model_id": "...", "source": "...", "unit": "...", "quantity": "10000", "count": 100 } ], "rollup_segments": ["rollup_abc..."], "raw_segments": ["raw_def..."], "corrections": [ { "event_id": "...", "quantity": -40, "kind": "Correction", ... } ] }linesis the rollup-with-raw-tail path grouped by every billing dimension.rollup_segments/raw_segmentslist IDs overlapping the range for this account's bucket — so operators can drill into them later.correctionslists Correction/Retraction events raw, not netted.GET /v1/accounts/{account_id}/verify?from&toComputes
SUM(quantity)two ways and reports drift:{ "account_id": "acc_x", "from_ms": ..., "to_ms": ..., "watermark_ms": ..., "period_sealed": true, "raw_total": "12345", "rollup_total": "12345", "drift": "0", "matches": true }drift = raw - rollup. Zero on a sealed period (to <= watermark_ms) is the invariant. Non-zero indicates a rollup bug, a late event below the watermark, or a missing rollup segment —rebuild_rollupsis the operator response.Refactor
start_serverextractedbuild_router(state) -> Routerso the HTTP layer can be tested viatower::oneshotwithout binding a port. Used by the new tests.Tests
6 new tests in
tests/explain_verify.rs, all driving the actual axum Router:explain_breaks_down_by_billing_dimensions— two meters → two rowsexplain_surfaces_corrections_separately— Correction event incorrections[]+ net SUM = 100 - 40 = 60explain_rejects_invalid_datesverify_reports_zero_drift_when_rollup_matches_rawverify_detects_drift_when_rollup_misses_late_event— late event after watermark advance ⇒ drift = 50verify_reports_period_sealed_statusAdds
http-body-utilas a dev-dep for axum response body parsing.Test plan
cargo build --all-targetsclean with-D warningscargo test --all-targets— 88 tests pass (was 82; +6)What's missing for full spec §19.10
Spec §19.10 wants invoice snapshots to reference both a watermark and a specific source segment set. The current
explainendpoint reports the overlapping segments at query time, butRollupSegmentMetadoesn't yet record which raw segment IDs contributed to each rollup segment. That's the next step for full forensic provenance, deferred to a follow-up.🤖 Generated with Claude Code