One immutable row per (region, account type, version). Money is integer micros in the card's currency.
A price change appends version 2; every historical receipt keeps its frozen rate_card_version
forever — UPDATE and DELETE are blocked by trigger.
| India Individual | India Firm | US Individual | US Firm | |
|---|---|---|---|---|
| Subscription | None | ₹25,000/seat/mo | None | $250/seat/mo |
| Agreement | None | 3 seats · 12 months | None | 3 seats · 12 months |
| Included pages/month | None | 2,500/seat, pooled | None | 1,000/seat, pooled |
| INTAKE / CREATE page | ₹15 | ₹10 overage | $0.40 | $0.25 overage |
| Onboarding pages | None | 10,000 once, firm-wide | None | 4,000 once, firm-wide |
The Story-2485 content-free ledger is evolved into the customer money book. Every row freezes:
billing_account_id (= firm), event_kind
(usage | credit | allowance_grant | reversal), verb (intake | create,
null for grants), allowance_bucket, included_pages_applied / overage_pages,
unit_price_micros, currency, amount_micros, rate_card_version,
the full-package source_completion_event_id, and initiated_by_user_id.
Idempotency keys, firm RLS, the append-only trigger, and the correction-guard trigger all remain.
full INTAKE/CREATE package succeeds (pages = N)
→ take per-firm advisory transaction lock (two concurrent completions serialize)
→ already billed under this idempotency root? → return the existing events, write nothing
→ consume pools in order, purely from events:
onboarding pool (firm-wide, once)
monthly pool (per billing period)
offline purchases (cheque-paid pages, validity-checked)
→ whatever remains is overage, priced at the frozen per-page rate
→ one single-bucket usage segment per pool touched + one overage segment,
all sharing the completion receipt
Because the lock serializes pool math per firm, two jobs crossing the boundary together can never spend the same included page twice: the suite proves two concurrent 80-page charges against a 100-page pool always land as exactly 100 included + 60 overage. Individual accounts skip pools entirely — every page at the flat rate.
staff selects firm
→ POST /admin/firms/{firm_id}/page-allowance-grants
pages + amount + payment method + cheque/receipt reference
+ received date + validity + reason + acting staff identity
→ immutable allowance_grant receipt appended (audit event emitted)
→ projection rebuilt in the same transaction
→ firm's available pool increases
Nothing ever runs UPDATE remaining_pages. A mistaken grant is corrected by
POST …/{event_id}/reversal, which appends a linked mirror event
(reverses_event_id) — and refuses if the granted pages were already consumed.
A zero-value goodwill grant requires a reason. Writes are platform-admin only; both actions
emit typed audit events carrying the staff actor.
The monthly row (pages used by verb, included applied, overage, overage amount, and per-pool pages remaining) is a pure recomputation over ledger events. Drop the row, rebuild, and the totals match exactly — proved by the CL-13 test. The product reads the projection for speed; the ledger stays the only truth.