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_code → cn.* 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)
- Allocation: ∑ PO.allocated = Contract.TP (± rounding).
- Schedule: ∑ schedule amounts for PO = allocated (± term/CN adjustments).
- Recognition: PO.satisfied = ∑ recognized lines.
- Balance sheet sketch:
- Bill before recognize → contract liability (deferred).
- Recognize before bill → contract asset (unbilled). - Period: after
recognize(P), noopenlines 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
- Hotglue OrderDetails manufacturing
- Camunda job graphs
- Spreadsheet merge_asof PO invention
- AC COA / tax maps (AC context)
- ERP connector protocols (Catalyst world)