RevRec brainstorm · Domain & architecture · Domain model

14 — Domain model (normative)

Status: design lock candidate
Locks: ADR 0003, ADR 0005
Companion: 13-full-design.md · 15-asc606-engineering-spec.md · 03-event-catalog.md

This is the optimal model. Source systems and legacy views adapt to it.


1. Ubiquitous language

Term Meaning
Contract ASC 606 Step-1 arrangement with a customer (default: Chargebee subscription + term)
Performance obligation (PO) Distinct promise (or series) to transfer goods/services
Transaction price (TP) Consideration expected for the contract (excl. third-party tax collected)
Allocation TP (and VC updates) assigned to POs
Schedule line Amount eligible to recognize in a time bucket (day/period)
Recognize Satisfy PO → move amount from contract liability/asset mechanics to revenue
Amendment Approved change in scope/price → modification accounting
Billing document Invoice or credit note — billing, not promise inventing
Applied event Idempotent record that a source event was processed
Period Accounting period with explicit close state

Legacy “Sales Order” ≈ read projection of Contract (+ items as PO views), not the write model.


2. Aggregate map

Tenant
 └── Customer
      └── Contract ─────────────────────────────┐
           ├── PerformanceObligation            │
           │     ├── AllocationSlice            │
           │     └── RecognitionScheduleLine*   │
           ├── Modification*                    │
           └── (links) BillingLine.po_id?       │
                                                │
BillingDocument (Invoice|CN) ── BillingLine ────┘
CashApplication → Invoice
Period (tenant-scoped)
JournalBatch (period-scoped summary; AC may own richer JE)
AppliedEvent (tenant-scoped)

3. Entities & key fields

Illustrative; names can snake_case in SQL.

3.1 Customer

Field Notes
id, tenant_id Surrogate
source, source_ref e.g. chargebee customer id
currency Functional/transaction defaults
attributes jsonb extensibility

3.2 Contract

Field Notes
id, tenant_id, customer_id
source, source_ref subscription id
status draft|active|amended|satisfied|terminated
start_at, end_at Service/contract term
transaction_price Current TP (updated by mods/VC)
currency
collectibility_ok Step-1 gate

Invariant: Active contract has ≥1 PO. TP ≥ 0 (unless explicit negative adjustment model).

3.3 PerformanceObligation

Field Notes
id, contract_id
product_ref Catalog / item price id
description
method ratable|point_in_time|proportional|…
service_start, service_end Window for over-time
allocated_amount From allocation
satisfied_amount Recognized cumulative
status open|partial|satisfied|terminated
ssp Standalone selling price used (nullable until multi-PO)

Invariant: satisfied_amount ≤ allocated_amount except explicit reverse policies.

3.4 Allocation

Represent as slices per PO (table allocation_slices) with tp_version or as_of_event_id so history is auditable. MVP single-PO: one slice 100%. Multi-PO: relative SSP (ASC 606-10-32-31).

3.5 RecognitionScheduleLine

Field Notes
id, po_id
bucket_date Day grain (ADR 0005)
period Fiscal period id YYYYMM
amount
status open|recognized|voided
recognized_at, recognize_event_id

Invariant: Sum(open+recognized) for PO ≈ allocated − terminated adjustments (within rounding policy).

3.6 Modification

Field Notes
id, contract_id
effective_at
policy_id mod.prospective|mod.cumulative|mod.separate_contract
payload Before/after TP, items, windows
source_event_id

3.7 BillingDocument / BillingLine / CashApplication

Billing only. Lines may link po_id for allocation of cash/CN to obligations but must not create POs.

3.8 Period

Field Notes
tenant_id, period_id
state See state machine
recognized_at, posted_at, locked_at

3.9 AppliedEvent

Field Notes
tenant_id, source, source_event_id UNIQUE
type, contract_id?
applied_at, payload_hash
policy_ids[] What fired

4. State machines

4.1 Contract

draft → active → amended (loop) → satisfied
                 ↘ terminated

4.2 Period (ADR 0005)

OPEN → RECOGNIZING → RECOGNIZED → POSTING → POSTED → SOFT_CLOSED → HARD_LOCKED
         │                                              │
         └──────── reopen (controlled) ◄────────────────┘ (from SOFT only by default)

HARD_LOCKED mutations → PPA in next open period.

4.3 Schedule line

open → recognized
open → voided (mod/terminate/CN)
recognized → (PPA reverse entries create offsetting lines; don’t delete history)

5. Apply API (normative behavior)

All commands: tenant-scoped, idempotent on source event id, emit audit.

5.1 open_contract

Pre: collectibility OK; distinct POs identified.
Steps 1–4: create Contract, POs, allocation, build schedule for method.
Post: contract active.

5.2 amend_contract

Input: effective date, new items/prices/windows, policy_id (or rules engine picks).
Effects by policy: - mod.prospective: freeze past recognized; rebuild remaining open lines from effective date. - mod.cumulative: catch-up adjustment in current period for cumulative obligation change. - mod.separate_contract: spawn additional contract for distinct added scope at SSP.

5.3 record_billing

Create BillingDocument; AR ↑; if cash not yet due performance, increase contract liability (deferred) per accounting identity — no new POs.

5.4 record_payment

CashApplication; AR ↓; cash/clearing per AC mapping when AC enabled.

5.5 record_credit

Resolve reason_codecn.* policy; adjust deferred/schedule/recognized per ADR 0005.

5.6 record_delivery

For proportional / evidence-based PIT: attach delivery evidence; open schedule or immediate recognize per method.

5.7 recognize(period)

For all open lines with period = P on non-locked period: mark recognized; update PO satisfied_amount; decrease deferred / decrease contract asset as appropriate; write recognition facts for AC.

5.8 post_journals(period)

Build summarized JournalBatch from period movements (or delegate detail journaling to AC using published subledger facts). RevRec guarantees amounts; AC guarantees Dr=Cr and COA.

5.9 terminate

Apply term.cancel policy; void remaining open lines; set PO/contract terminated; optional penalty billing event.


6. Accounting identities (engine checks)

  1. Allocation: ∑ PO.allocated = Contract.TP (± rounding).
  2. Schedule: ∑ schedule amounts for PO = allocated (± term/CN adjustments).
  3. Recognition: PO.satisfied = ∑ recognized lines.
  4. Balance sheet sketch:
    - Bill before recognize → contract liability (deferred).
    - Recognize before bill → contract asset (unbilled).
  5. Period: after recognize(P), no open lines remain for period P (unless held).

Failures quarantine the contract and block close until waived/fixed.


7. Policy registry (data, not code forks)

Family Examples
recog.method.* ratable daily, PIT on start, proportional on delivery
mod.* prospective, cumulative, separate_contract
cn.* refund_unrecog, reverse_recog, ar_only, overflow.*
term.cancel.* void_remaining, recognize_remaining
ppa.* next_period_catchup
ssp.* relative, residual (enterprise-deep)
vc.* expected_value, most_likely, constraint

Config tables versioned; AppliedEvent stores which version fired.


8. Source mapping (Chargebee default)

CB concept Domain
Customer Customer
Subscription (+ contract_term) Contract
Subscription item / plan charge PO candidate
Invoice / CN BillingDocument
Transaction (payment) CashApplication
Subscription changed amend_contract
Subscription cancelled terminate

Detailed event catalog: 03-event-catalog.md.


9. Projections (compat)

Projection Consumer
SO / SOI shaped DTO Legacy UI / migration
Unearned / waterfall rows Reports / smoke equality
RPO time bands Disclosures
ARR/MRR SaaS metrics

Projections never write back into Contract/PO without going through apply.


10. Golden scenarios → model

Scenario Exercises
S01 ratable open + recognize + deferred
S02 bill/pay record_billing/payment
S03 upgrade amend prospective
S04 CN record_credit
S05 cancel terminate
Smoke simple-mod / prospective-mod multi-wave amend + reopen/PPA
Smoke plan-types proration policies
Smoke deferred-revenue-adj unbilled/deferred adjustments
Smoke contract-term-mod mod treatment classification

11. Schema sketch (Postgres)

customers (tenant_id, id, source, source_ref, ...)
contracts (...)
performance_obligations (...)
allocation_slices (po_id, tp_version, amount, ssp, ...)
recognition_schedule_lines (po_id, bucket_date, period, amount, status, ...)
modifications (...)
billing_documents (...); billing_lines (...); cash_applications (...)
periods (tenant_id, period_id, state, ...)
journal_batches (...); journal_lines (...)  -- or owned by AC schema
applied_events (tenant_id, source, source_event_id UNIQUE, ...)
outbox (...); policy_versions (...)

Indexes: (tenant_id, source_ref), (tenant_id, contract_id), (tenant_id, period, status) on schedule lines.


12. What we deliberately do not model in core