> ## Documentation Index
> Fetch the complete documentation index at: https://internal.softcrum.com/llms.txt
> Use this file to discover all available pages before exploring further.

# ADR-011 — Points Ledger: Immutable, Dual Currency, FIFO Lots

> loyalty.ledger_transactions is append-only; types (parametric): earn|redeem|expire|revoke|adjust; states pending→available (earn confirms after the tenant-configured return window). A mutable balance column is FORBIDDEN.

Status: Proposed · Refs: v0.2 §2.2, benchmark (Open Loyalty locked points, Voucherify pending periods)

## Decision

* `loyalty.ledger_transactions` is append-only; types (parametric): earn|redeem|expire|revoke|adjust; states pending→available (earn confirms after the tenant-configured return window). A mutable balance column is FORBIDDEN.
* Two currencies per program minimum: `redeemable` and `status` (XP). Never mixed (airline model).
* Earns create `loyalty.point_lots` with `expires_at`; redemptions consume lots FIFO. Expiration modes per program (tier-overridable): rolling N days | end-of-month | end-of-year. `points.expiring_soon` scan feeds retention campaigns.
* `loyalty.contact_balances` is a projection updated in the SAME transaction as the ledger write (near-real-time, non-negotiable); nightly reconciliation detects drift (data-freshness add-on raises frequency of the reconciliation, never of the balance itself).
* Redemptions are parent/child (multi-redeemable requests); rollback at parent level with revert|keep modes; `Idempotency-Key` mandatory. `money_component` (amount+currency, nullable) reserves the mixed points+money seam (DEC-H3) — unused in F1.

## Consequences

* Auditable liability, replayable balances, standard industry semantics. − More rows than a naive balance; mitigated by partitioning evaluation and projections.
