Billing Service — Domain Model
Status: populated
Owner: Platform Engineering + Finance
Last updated: 2026-04-18
1. Aggregates
BillingEvent (root)
One chargeable message event. Immutable after insert.
| Field | Type | Notes |
|---|
billingEventId | UUIDv4 | Identity |
messageId | UUIDv4 | Dedup key; FK to orch.sms_messages (logical) |
tenantId | UUIDv4 | Isolation boundary |
accountId | UUIDv4 | Billing subject |
operatorId | string | Used to resolve pricing + cost |
direction | BillingDirection | MT (mobile-terminated outbound) |
segmentCount | number | Determines charge when per-segment pricing |
pricingTableId | UUIDv4 | FK to resolved pricing_tables row |
customerPrice | Money | Revenue recognized |
operatorCost | Money | Cost booked |
margin | Money | Computed: customerPrice - operatorCost |
currency | ISO 4217 | e.g. USD |
chargedAt | Instant | From DLR event |
createdAt | Instant | |
PricingTable (root)
Versioned pricing configuration per billing segment.
| Field | Type | Notes |
|---|
pricingTableId | UUIDv4 | Identity |
accountTier | AccountTier | STARTER, GROWTH, ENTERPRISE, CUSTOM |
operatorId | string | Carrier/operator identifier |
direction | BillingDirection | MT |
pricingModel | PricingModel | PER_SEGMENT or FLAT_PER_MESSAGE |
unitPrice | Money | Price per segment or per message |
currency | ISO 4217 | |
effectiveFrom | date | Inclusive |
effectiveTo | date | null | null = currently active |
createdBy | string | Admin userId |
OperatorCost (root)
Platform's cost for a given operator/direction, used for margin modelling.
| Field | Type | Notes |
|---|
operatorCostId | UUIDv4 | Identity |
operatorId | string | |
direction | BillingDirection | |
costPerSegment | Money | |
currency | ISO 4217 | |
effectiveFrom | date | |
effectiveTo | date | null | |
Invoice (root)
Monthly aggregated invoice for one account.
| Field | Type | Notes |
|---|
invoiceId | UUIDv4 | Identity |
tenantId | UUIDv4 | |
accountId | UUIDv4 | |
periodStart | date | First day of month |
periodEnd | date | Last day of month |
totalMessages | number | |
totalSegments | number | |
subtotalAmount | Money | |
currency | ISO 4217 | |
status | InvoiceStatus | DRAFT → FINALIZED → VOID |
s3Key | string | null | Object store key for PDF |
generatedAt | Instant | null | |
UsageSummary
Pre-aggregated usage bucket (hourly) for query efficiency.
| Field | Type | Notes |
|---|
summaryId | UUIDv4 | |
tenantId / accountId / operatorId | identifiers | Grouping dimensions |
bucketHour | TIMESTAMPTZ | Truncated to hour |
messageCount / segmentCount | numbers | Counters |
totalCustomerPrice / totalOperatorCost | Money | |
2. Value Objects
| VO | Invariant |
|---|
Money | amount (Decimal, ≥ 0) + currency (ISO 4217); arithmetic only within same currency |
AccountTier | Enum: STARTER, GROWTH, ENTERPRISE, CUSTOM |
BillingDirection | Enum: MT |
PricingModel | Enum: PER_SEGMENT, FLAT_PER_MESSAGE |
InvoiceStatus | Enum: DRAFT, FINALIZED, VOID |
3. Domain Services
| Service | Purpose |
|---|
PricingResolver | Resolves effective PricingTable by (accountTier, operatorId, direction, currency, chargedAt) with Redis cache |
CostResolver | Resolves effective OperatorCost by (operatorId, direction, chargedAt) |
MarginCalculator | customerPrice.subtract(operatorCost) with currency assertion |
InvoiceGenerator | Aggregates summaries, renders PDF template, handles S3 upload |
4. Domain Events
| Event | Trigger |
|---|
billing.invoice.generated.v1 | Invoice persisted as FINALIZED and PDF stored |
5. Invariants
messageId is unique in billing_events — no double billing.
- Exactly one
PricingTable row is active per (accountTier, operatorId, direction, currency) at any point in time (enforced by partial unique index).
- Invoice transitions:
DRAFT → FINALIZED (cron only), FINALIZED → VOID (platform.finance only).
margin = customerPrice - operatorCost; negative margin is allowed (flagged by alert).
- Currency of
customerPrice must match PricingTable.currency.