Finance Billing Events Pipeline
Updated: June 2026 — Two-layer pipeline with processed usage facts, replay, and reconciliation
Overview
The finance billing pipeline processes raw usage events through a multi-stage metering, entitlement, and rating chain that produces durable processed usage facts. This two-layer design separates:
- Raw usage events — immutable ingress records stored in
finance.usage_events - Processed usage facts — derived billing records stored in
finance.processed_usage_facts
Raw events are the immutable truth. Processed facts are the authoritative explanation of what was billed, why, and how. Invoices, wallet transactions, and reconciliation all trace back to processed facts.
Architecture
Processing Pipeline (UsageProcessingService)
Every usage event — whether from real-time ingest or replay — flows through the same 8-step pipeline:
Meter Execution Strategies
Each meter has an AggregationType that determines how raw events contribute to aggregated usage. Strategies are resolved via MeterExecutionStrategyResolver:
| Type | Derived Quantity | Grain Behavior | Required Properties |
|---|---|---|---|
| Sum | event.Quantity | current + qty | — |
| Count | 1 | current + 1 | — |
| CountUnique | 1 | Tracks unique values in HashSet; aggregate = set size | UniqueProperty on meter |
| Avg | event.Quantity | Tracks sum/count; aggregate = running average | — |
| Max | event.Quantity | Math.Max(current, qty) | — |
| Latest | event.Quantity | Replaces with latest value | — |
| SumWithMultiplier | qty × meter.Multiplier | current + derived | Multiplier on meter |
| WeightedSum | qty × weightProperty | current + derived | WeightProperty on meter |
Subscription Entitlement Integration
When a usage event carries a SubscriptionId, the pipeline evaluates quota:
- Calls
AppSubscriptionGrain.ConsumeUsageAsync(eventCode, derivedQuantity) - Maps the result to a
QuotaOutcome:- FullyCovered — entire quantity consumed from quota
- PartialOverflow — some quota, rest is billable
- SoftOverage — quota exhausted, usage still allowed
- None — no subscription context
- Only the billable overflow is sent to the pricing engine
Pricing (RatingInput → RatingResult)
The PricingEngineGrain now accepts a RatingInput carrying:
BillableQuantity(after quota deduction)CumulativeQuantity(for tiered/package calculations)QuotaOutcome(for traceability)
Returns a RatingResult with TotalCost, UnitCost, TransformedQuantity, and MatchedChargeId.
Processed Usage Facts
Each processed fact captures the full audit trail of a billing event:
- Identity: FactId, RawEventId, IdempotencyKey
- Scope: TenantId, InstanceId, EnvironmentId, EnvironmentKey
- Customer: ExternalCustomerId, CustomerId, SubscriptionId
- Meter: MeterId, EventCode, Source
- Quantity chain: RawQuantity → DerivedQuantity → TransformedQuantity → PricingQuantity
- Entitlement: QuotaOutcome, QuotaConsumed, BillableOverflow
- Pricing: UnitCost, TotalCost, ChargeId, Currency
- Billing: BillingMode, WalletDebitAmount, AccruedAmount
- Correction: Revision, Sign (for replay compensation), ProcessingVersion, Replayed
Stored in finance.processed_usage_facts (TimescaleDB hypertable, migration 024).
Wallet Traceability
Wallet transactions now carry ProcessedFactId, linking every balance change back to the processed fact that caused it. This enables:
- Auditing why a balance changed
- Reconciling wallet totals against processed fact totals
- Tracing from invoice → fact → wallet transaction
Replay
The ReplayCoordinatorService re-processes raw events through the same pipeline:
- Determines next revision via
GetMaxRevisionAsync - Scans raw events using keyset pagination on
(event_time, created_at) - Feeds each event through
UsageProcessingService.ProcessEventAsyncwithisReplay=true - Emits new processed facts with incremented revision
- Supports cancellation and resumability via cursor persistence
Replay scope: environment, optional event code and customer filters, time window (max 90 days).
API endpoints: POST /replay/, GET /replay/{jobId}, GET /reconciliation/, GET /reconciliation/processed-facts
Reconciliation
GetReconciliationSummaryAsync compares raw events versus processed facts:
- Raw event count and total quantity
- Processed fact count, derived/billable totals, cost totals
- Wallet debit and accrued charge totals
- Max revision and replayed fact count
- Event-fact delta (raw count − processed count)
Invoice Generation from Processed Facts
UsageInvoiceBuilder generates invoice line items directly from processed facts:
- Queries
GetBillableChargeSummariesAsyncfor the billing window - Groups by event code and charge
- Produces
InvoiceLineItemrecords with quantity, unit cost, and descriptive labels InvoiceProcessingPipelineServiceenriches draft invoices with these line items before finalization
This replaces the legacy approach of building invoices from live aggregate counters.
Key Code Locations
Pipeline
- UsageProcessingService.cs — central processing pipeline
- MeterExecutionStrategies.cs — per-aggregation-type strategies
Grains
- MeterAggregationGrain.cs — strategy-driven hot-path counters
- PricingEngineGrain.cs — pricing with RatingInput/RatingResult
- AppSubscriptionGrain.cs — quota evaluation
Storage
- DapperUsageEventStore.cs — raw event persistence + replay scans
- DapperProcessedUsageFactStore.cs — processed fact persistence + reconciliation
Replay & Reconciliation
- ReplayCoordinatorService.cs — replay engine
- ReplayEndpoints.cs — replay/reconciliation API
Invoice
- UsageInvoiceBuilder.cs — builds line items from processed facts
- InvoiceProcessingPipelineService.cs — invoice finalization pipeline
Domain Models
- ProcessedUsageFact.cs — processed fact record
- RatingInput.cs — pricing engine input
- ReplayJob.cs — replay job model
- ReconciliationSummary.cs — reconciliation output
Summary
The billing pipeline now implements a two-layer architecture: immutable raw events for truth, durable processed facts for billing explanation. Every stage — metering, entitlement, pricing, wallet charging — is traced through processed facts. Replay reprocesses raw events through the same pipeline. Invoices are built from processed facts, not live counters. Reconciliation compares raw and processed layers to detect and resolve discrepancies.