Skip to content

Phase D operability: explain + verify endpoints - #14

Merged
pbudzik merged 1 commit into
mainfrom
feat/explain-verify-period
May 16, 2026
Merged

Phase D operability: explain + verify endpoints#14
pbudzik merged 1 commit into
mainfrom
feat/explain-verify-period

Conversation

@pbudzik

@pbudzik pbudzik commented May 16, 2026

Copy link
Copy Markdown
Owner

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&to

Returns 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", ... }
  ]
}

lines is the rollup-with-raw-tail path grouped by every billing dimension. rollup_segments / raw_segments list IDs overlapping the range for this account's bucket — so operators can drill into them later. corrections lists Correction/Retraction events raw, not netted.

GET /v1/accounts/{account_id}/verify?from&to

Computes 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_rollups is the operator response.

Refactor

start_server extracted build_router(state) -> Router so the HTTP layer can be tested via tower::oneshot without 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 rows
  • explain_surfaces_corrections_separately — Correction event in corrections[] + net SUM = 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-dep for axum response body parsing.

Test plan

  • cargo build --all-targets clean with -D warnings
  • cargo test --all-targets — 88 tests pass (was 82; +6)
  • CI green

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 explain endpoint reports the overlapping segments at query time, but RollupSegmentMeta doesn'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

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>
@pbudzik
pbudzik merged commit 215cdf3 into main May 16, 2026
1 check passed
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