Features / supported
Usage ingestion
Purpose
Idempotent ingestion of usage measurements, immutable void/replacement corrections, quarantine instead of silent loss, and cutoff-reproducible aggregation feeding metered billing.
User outcomes
- An integration submits events (single or batch) exactly-once by external
event ID: identical replays are
duplicate, changed payloadsconflict. - Problem events (unknown subscription, too old, too far in the future, oversized properties) are quarantined with the full payload — never silently discarded; a later accepted re-ingest resolves the entry.
- Finance can reproduce any billing quantity: aggregation at a frozen cutoff always returns the same evidence set.
Actors and permissions
Ingestion and corrections: integration_client, billing_admin, or
team_admin. Reads/aggregation additionally finance_operator and
auditor. All operations are team-scoped.
Domain terminology
- Event key — unpartitioned
(team, external_event_id)reservation with payload hash; the idempotency authority. - Measurement / void / replacement —
usage_eventskinds. A void references the original; a replacement is an ordinary measurement linked viareplacement_for_event_id. - Cutoff evidence set — measurements received on or before the cutoff with no void received on or before that same cutoff.
Workflows
ingest_event— normalizes (occurred_atUTC, decimalvalue, canonicalpropertieswithout floats), resolves the subscription by internal or external ID, applies policy limits, reserves the event key (ON CONFLICT DO NOTHING), inserts the measurement.received_atis assigned by PostgreSQLclock_timestamp()and never accepted from callers.ingest_batch(BC-US-051) — each event in its own transaction; one failing event never rolls back siblings; batches over the cap fail up front (:batch_too_large). When ≥1 event is accepted, exactly ONEusage_batch.accepted.v1outbox event carries the batch counts.void_event(BC-US-052) — the void has its OWN external event ID (the correction’s idempotency key); optional replacement in the same locked transaction. Both copy the original’soccurred_at(partition locality) and record their own serverreceived_at(cutoff semantics).aggregate(BC-US-053/054/055) — pure read over a half-open billing period at a frozen cutoff;sum(default),count,max,unique_count(distinct values of a properties key). Late-received in-period measurements are reported asexcluded_late.ensure_partitions/PartitionWorker— monthly partitions kept ≥ 2 months ahead (daily cron 03:00); there is no DEFAULT partition, so anoccurred_atoutside created partitions raises.
State transitions
Event status: effective → voided (lifecycle projection only — all
payload columns stay frozen; a database trigger rejects any other change).
Quarantine entries: open → resolved_at set on accepted re-ingest.
Business rules / invariants
- At most one effective void per measurement (partial unique index):
replaying the same void ID is idempotent (
{:ok, :already_voided}), a second different void is{:error, :already_voided}. - A void received after a cutoff does NOT remove the measurement from that cutoff’s evidence — reproducibility over recency.
- Period assignment: the DATE of
occurred_atin the team’s billing time zone (UTC-date fallback when the zone is unknown to the tz database). - Policy limits (module config, defaults):
max_age_days90,max_future_hours24,max_batch_size1000,max_properties_bytes65,536.
GraphQL contract
ingestUsageEvent (BC-US-050) and ingestUsageBatch (BC-US-051) with
typed per-event outcomes (accepted/duplicate/quarantined),
voidUsageEvent (BC-US-052, optional replacement), and the usagePreview
query (BC-US-053: deterministic aggregation at a fixed cutoff). HTTP
coverage in test/graphql/usage_test.exs.
CLI surface
Not yet implemented (BC-US-157 planned).
MCP surface
Not yet implemented (BC-US-158 planned).
UI behavior
LiveView surfaces under construction; domain commands available via GraphQL.
Accounting / ERP effects
None directly. Metered invoice lines embed the aggregation evidence (metric, aggregation, event count, excluded_late, cutoff) in their calculation trace (see invoice-preview-and-freeze).
Async / failure / recovery behavior
Batch items survive storage-level raises (e.g. missing partition) as
per-item storage_error rejections without losing committed siblings.
Recalculation of already-billed periods is driven downstream by the
usage_event.voided.v1 event (credit/rebill — not yet automated).
Observability
Audit: usage.event.quarantined, usage.event.voided. Outbox:
usage_batch.accepted.v1, usage_event.voided.v1.
Tests
test/workflows/usage_ingestion_test.exs — idempotency triad, server
received_at, quarantine + resolution, batch independence and the single
batch event, void/replacement cutoff semantics, all four aggregations,
isolation/authorization. test/workflows/metered_billing_test.exs —
usage → graduated-tier invoice lines with traces.
Security / privacy
Properties are canonicalized JSON without floats; oversized payloads are quarantined, not stored unbounded. Cross-team ingest/void/aggregate is uniformly denied.
Limitations
- No GraphQL/HTTP ingestion endpoint yet — context API only.
- No automated credit/rebill pipeline reacting to voids of already-billed
periods;
usage_event.voided.v1is emitted for a future consumer. - Quarantine review/replay tooling is limited to re-ingesting the corrected event.