PRD: Entitlements - policy, target, grant & redemption
| Module | Sale (CORE-07) | PRD ID | PRD-ENT-001 |
| Status | Shipped | Owner | Sale squad |
| Date | 2026-06-15 | Version | v1.0 |
| Packages | @nx/sale · @nx/core | URD | ENT |
TL;DR
Lets a merchant sell something the customer uses over time - a coffee 10-pack, a 30-day gym pass, a class bundle, a metered minutes top-up - and then redeem it down to zero. The merchant defines a policy (what is sold, its quota and validity), scopes it with targets (which items it covers), and the moment the entitlement variant is sold the system mints a grant: a per-customer balance that freezes a snapshot of the policy so later catalogue edits never touch a sold pass. Each use writes a redemption against the grant - an append-only ledger that draws down the used counter, validates the redeemed item against the frozen scope, and supports reversals that hand quota back. The grant's lifecycle (pending → active → exhausted / expired / suspended / cancelled) tells everyone exactly what the customer still has.
1. Context & Problem
A POS sells items that are consumed instantly. But merchants increasingly sell claims on future value: a prepaid drinks bundle, a membership window, a punch card, a block of service minutes. None of these fit a one-shot order line - the customer pays once and then draws the value down across many later visits, each of which must check "is there balance left, is it still valid, does it cover this item?".
Without a first-class model for this, a merchant fakes it with manual notes or discount codes - no balance tracking, no expiry, no audit of who used what when, and no protection against a sold bundle silently changing when the catalogue is edited. The result is disputes, leakage, and no way to answer "how many sessions does this customer have left?".
This increment ships an entitlements system as a self-contained part of the Sale module. It separates the template a merchant configures (policy + targets) from the sold instance a customer holds (grant), and records every use as an immutable redemption so the balance is always auditable and the sold terms are always honoured exactly as sold.
2. Goals & Non-Goals
Goals
- A policy that turns a sellable variant into an entitlement with a quota axis, a validity axis, an activation mode, and declared limit dimensions.
- Targets that scope which items a policy's grant may be redeemed against - empty scope means open access.
- A grant minted when the entitlement variant is sold, carrying a unique code, a quota balance, a resolved validity window, and a frozen policy snapshot so catalogue edits never alter a sold grant.
- A redemption ledger that draws the grant's used counter down, validates the redeemed item against the frozen scope, and supports reversals that return quota.
- A grant lifecycle - pending, active, exhausted, expired, suspended, cancelled - that always states what the holder still has.
Non-Goals
- The pricing of the entitlement variant - the price lives in fares (Product); this PRD covers what is granted, not what it costs.
- Payment capture and order checkout - owned by Orders and Payment; a grant is minted off a completed sale, it does not run the sale.
- Loyalty point earning (PNT) - a separate reward mechanic; entitlements are sold balances, not earned points.
- A customer-facing wallet UI to browse remaining balances - the balance is recorded and queryable; a dedicated end-user screen is a later increment.
3. Success Metrics
| Metric | Target / signal |
|---|---|
| Balance accuracy | A grant's available balance equals quota total minus the sum of its redemptions at all times |
| Snapshot integrity | A policy edited after a sale never changes the terms of an already-sold grant |
| Scope safety | A redemption against an item outside the grant's frozen scope is refused (unless scope is open) |
| Audit completeness | Every quota change traces to an append-only redemption entry - no silent balance edits |
| Lifecycle honesty | A fully-used grant reads EXHAUSTED; one past its window reads EXPIRED |
4. Personas & Use Cases
| Persona | Goal in this feature |
|---|---|
| Owner / Manager | Configure what is sold as an entitlement - its quota, validity, scope - and read remaining balances |
| Cashier | Sell an entitlement and redeem a customer's balance at the point of sale in a tap |
| Customer | Buy a bundle / pass once and draw it down across many visits, confident the terms won't change |
Core scenario: an owner configures a "10 Coffees" policy on the matching variant - quota 10 cup, no expiry, activates on purchase, scoped to the coffee category via targets. A customer buys it; the system mints a grant with code ENT-…, balance 10, status ACTIVE, and freezes the policy + its targets into the grant. On each later visit the cashier redeems one cup: a redemption draws the used counter to 1, 2, … 10; at 10 the grant flips to EXHAUSTED. A mistaken redeem is corrected with a reversal that returns one cup and reopens the balance.
5. User Stories
- As an owner, I declare a sellable variant as an entitlement with a quota and an optional expiry, so I can sell bundles and passes.
- As an owner, I scope an entitlement to the items it covers, so a coffee pass can't be spent on food.
- As an owner, I trust that editing the catalogue later never changes a pass a customer already bought.
- As a cashier, I redeem one use of a customer's balance in a tap, and see what's left.
- As a cashier, I reverse a redemption I made by mistake, and the balance comes back.
- As a customer, I buy once and draw the value down over many visits, and the pass expires only when its window says so.
6. Functional Requirements
| # | Requirement | URD ref |
|---|---|---|
| FR-1 | A policy binds 1:1 to a sellable variant and declares its limit dimensions (count / time-window / metered) | URD-ENT-001..002 |
| FR-2 | A policy carries an optional quota (amount + unit) and an optional validity duration; either axis may be absent | URD-ENT-003 |
| FR-3 | A policy declares an activation mode - on purchase, on first use, or scheduled - governing when validity starts | URD-ENT-004 |
| FR-4 | A policy may require an attached customer, or allow a bearer entitlement | URD-ENT-005 |
| FR-5 | Targets scope a policy to specific items; an empty target set means open access | URD-ENT-006..007 |
| FR-6 | Selling the entitlement variant mints a grant with a unique code, quota balance, and resolved validity window | URD-ENT-008 |
| FR-7 | The grant freezes a snapshot of the policy and its targets at sale time, isolating it from later catalogue edits | URD-ENT-009 |
| FR-8 | A grant tracks quota total vs used (used counter is source of truth; available = total − used) | URD-ENT-010 |
| FR-9 | A grant moves through pending → active → exhausted / expired / suspended / cancelled | URD-ENT-011 |
| FR-10 | A redemption draws the used counter down for a quantity and is captured against its consuming order / item | URD-ENT-012 · URD-ENT-016 |
| FR-11 | A redemption quantity in a different unit from the grant's quota unit is converted via the unit ratio | URD-ENT-013 |
| FR-12 | A redemption validates the redeemed item against the grant's frozen target scope (open when no targets) | URD-ENT-014 |
| FR-13 | A reversal redemption returns quota to the grant; the ledger is append-only - corrections are new entries | URD-ENT-015 |
| FR-14 | Every policy, target, grant, and redemption is merchant-scoped and soft-deleted | URD-ENT-017 |
Full requirement text and acceptance criteria live in the Sale URD - ENT. This PRD references them rather than restating them.
7. Non-Functional Requirements
| Area | Requirement |
|---|---|
| Source of truth | The grant's used counter is authoritative for balance; redemptions are the reconciling audit ledger behind it |
| Immutability | The redemption ledger is append-only; a correction is a reversal entry, never an edit or delete of a prior redeem |
| Snapshot isolation | A grant's terms come from the policy snapshot frozen at sale - never re-read live - so catalogue edits cannot change a sold grant |
| Scope from snapshot | Redemption validates targets from the grant's denormalized snapshot, not a live target lookup |
| Tenancy & authz | All operations scoped per merchant (x-merchant-id) and gated by entitlement permissions |
| Precision | Quota and redemption quantities use decimal precision; cross-unit redemptions convert via the unit ratio |
| i18n | Policy name and description are bilingual ({ en, vi }) |
8. UX & Flows
The configuration surface lets an owner define a policy on the entitlement variant (quota, validity, activation, scope via targets). The point-of-sale surface sells the entitlement, looks up a customer's grant, shows the remaining balance, redeems a use, and reverses one when needed.
9. Data & Domain
| Entity | Role |
|---|---|
EntitlementPolicy | The template a merchant configures - bound 1:1 to a sellable variant; carries quota, validity, activation mode, required-customer flag, and limit dimensions |
EntitlementTarget | A scope binding of a policy to a specific item (variant by default), ordered; empty scope = open access |
EntitlementGrant | The sold instance held by a customer - unique code, quota total / used balance, validity window, lifecycle status, and a frozen policy + targets snapshot |
EntitlementRedemption | An append-only ledger entry against a grant - a redeem or a reversal, with quantity, unit, and the consuming order / item |
Conceptual only - full schema and invariants live in the sale domain model. Cross-schema references (variant, order, customer) are soft references; integrity is enforced in the service layer.
10. Dependencies & Assumptions
Depends on
- Sale orders (ORD) - a grant is minted off a completed sale of the entitlement variant; redemptions reference the consuming order / item.
- Product (Product) - a policy binds to a sellable variant, and targets scope to catalogue items.
- Units of measure (Inventory) - quota and redemption quantities carry a unit; cross-unit redemptions convert via the unit ratio.
@nx/core- the policy / target / grant / redemption models and merchant-scoped schemas.
Assumptions
- The merchant configures a policy before selling its variant; selling an unconfigured variant yields no grant.
- A customer is identified when a policy requires one (named entitlement); bearer entitlements may omit the customer.
- The quota counter on the grant and the redemption ledger are kept consistent by the service that applies each redemption.
11. Risks & Open Questions
| Risk / question | Mitigation / status |
|---|---|
| Catalogue edit silently changes a sold pass | Resolved - the grant freezes a policy + targets snapshot at sale; terms are read from the snapshot, never live |
| Balance and ledger could diverge | The used counter is the source of truth; redemptions reconcile it; reversals (not edits) keep the ledger append-only |
| Redeem against an out-of-scope item | Refused - redemption validates the item against the frozen target scope; open access only when the snapshot has no targets |
| Customer redeems in a different unit than the quota | Quantity is converted via the unit ratio before drawing down the counter |
| Per-day redemption caps | Out of scope this increment - a per-day cap field is carried on the policy for a future runtime check |
12. Release Plan & Launch Criteria
| Aspect | Plan |
|---|---|
| Phase | P2 - ENT in the URD feature catalog |
| Rollout | All merchants; no feature flag |
| Migration | New tables only (policy, target, grant, redemption); no change to existing order flows |
| Launch criteria | Selling the variant mints a grant with a frozen snapshot; redeem draws the balance down and exhausts at zero; reversal returns quota; out-of-scope redemptions are refused; everything is merchant-scoped |
| Monitoring | Grant balance vs ledger reconciliation, redemption refusal rate by reason (out of scope, exhausted, expired), grants by lifecycle status |
13. FAQ
What can I sell as an entitlement? Anything the customer draws down over time - a drinks bundle, a class pass, a membership window, a block of metered minutes. You bind a policy to the variant and set its quota and validity.
Does editing the policy later change passes already sold? No. Each grant freezes a snapshot of the policy and its scope at sale time and reads its terms from that snapshot, so a catalogue edit never alters a sold grant.
How is the remaining balance tracked? The grant carries a used counter (the source of truth); every use appends a redemption to an audit ledger that reconciles it. Available is the quota total minus what's used.
Can a customer spend a coffee pass on food? Only if the policy is scoped to allow it. A redemption checks the item against the grant's frozen target scope; an empty scope means open access, otherwise out-of-scope items are refused.
What if a cashier redeems by mistake? They record a reversal - an append-only correction that returns the quota. The original redeem is never edited or deleted.
When does a grant expire or exhaust? It reads EXHAUSTED once the used counter reaches the quota total, and EXPIRED once it passes its validity window - the window starting on purchase, first use, or a scheduled date per the policy's activation mode.
References
- URD: Sale - ENT
- Sibling PRDs: Sale order & cart · Loyalty points at order
- Module: Sale - overview + capabilities
- Developer: @nx/sale · @nx/core