Repository: granit-fx/granit-dotnet
Author: jfmeyers
## Objective
Align the Granit framework on the conceptual model of ORB, a usage-based billing platform, where it makes sense without copying ORB's full implementation. Granit targets B2B SaaS apps (10K-10M events/month per tenant), not big-data billing platforms — so we adopt ORB's domain concepts (Item, recompute on-demand, lifecycle, idempotency standards) while keeping our PostgreSQL-friendly architecture.
The gap analysis identified the following misalignments:
- No shared catalog of items between Metering and Subscriptions (each module lives in a silo)
- Non-standard idempotency on Metering ingestion (payload field + DB unique constraint), while Granit.Http.Idempotency (RFC 8700, header-based) already exists
- Metrics frozen at creation (enum AggregationType), no native retroactive recompute, no COUNT(DISTINCT)
- No Draft/Published/Archived lifecycle on MeterDefinition (just an Activated bool)
- Granit.Subscriptions lacks per-customer overrides, phases, discounts; PricingModel.Tiered is an enum without backing entities
- Granit.CustomerBalance covers ~85% of ORB Credits — small gaps to close
- Invoicing.InvoiceLineItem.SourceType exists but no strong convention enforces traceability source_id == meter_definition_id for audit
## Strategy
Hybrid approach — keep what works, add what's missing:
- Keep UsageAggregate pre-computed (hot path: quotas, dashboards, auto-billing)
- Add cold-path on-demand recompute for retroactive corrections
- Introduce Granit.Catalog as a new module with CatalogItem as the shared aggregate root (not a narrow BillableItem — vocation to grow toward e-commerce)
- Reuse WorkflowLifecycleStatus everywhere lifecycle is needed
- Extend Granit.CustomerBalance rather than create a parallel Granit.Credits module
## Features
- #1156 - Phase 0 — HTTP idempotency alignment on Metering events (3-5 days)
- #1157 - Phase 1 — Granit.Catalog module / CatalogItem shared aggregate (1-2 weeks)
- #1158 - Phase 2 — Metering hybrid enhancements: recompute, COUNT(DISTINCT), lifecycle (2-3 weeks)
- #1159 - Phase 3 — Subscriptions overrides, phases, tiered pricing (3-4 weeks)
- #1160 - Phase 4 — Granit.CustomerBalance ORB Credits alignment (1-2 weeks)
- #1161 - Phase 5 — Invoicing line items source convention (3-5 days)
## Constraints
- GDPR: No PII in events metadata, retention policies on raw events
- ISO 27001: Audit trail on all admin operations (debit, recompute, deprecate)
- Backward compatibility: All ItemId references nullable at first; lifecycle migration via CASE WHEN Activated; no breaking changes without one deprecation release
- No new infrastructure: PostgreSQL stays sufficient — no ClickHouse / TimescaleDB bascule
- No new NuGet dependencies: reuse existing (Workflow, QueryEngine, DataExchange, FluentValidation, EF Core 10, OpenTelemetry)
## Success criteria
- A SaaS using Granit can model the full chain: CatalogItem -> MeterDefinition -> MeterEvent -> UsageAggregate -> InvoiceLineItem -> CustomerBalance drawdown
- An admin can issue an invoice, retroactively correct usage events, and trigger a recompute that produces a corrected invoice
- HTTP Idempotency-Key header replays return Idempotent-Replayed: true on duplicate sends; payload-level dedup remains the last line of defense
- Tiered pricing (Volume + Graduated) computes correctly across tier boundaries
- All architecture tests pass; framework boundary rules cover the new Catalog module
- Documentation site has a dedicated module page for Catalog and an ADR for each structural decision (5 new ADRs total)
## Cross-references
This EPIC spans multiple existing module EPICs:
- #833 Granit.Metering
- #830 Granit.Subscriptions
- #831 Granit.Invoicing
- #874 Granit.CustomerBalance
- (new module) Granit.Catalog
Part of the broader programme #828 SaaS & Commerce Ecosystem.