Context
Points with nothing to spend them on are a liability with no marketing value. The catalog is what turns the ledger into a program. The design decision that matters is DEC-B4:reward_types is tenant-extensible, and it is one
of only two catalogs in the whole platform that are. The reason is the rule stated in the data
standard — a catalog is tenant-extensible when its values are the tenant’s own business taxonomy,
and never when they govern a state machine. “Free shipping”, “a free coffee”, “10% off the next
order” are a gym’s or a bakery’s vocabulary, not ours. What each type means operationally is
still the tenant’s business, which is why fulfilment is deliberately out of scope.
Scope (normative)
loyalty.rewards: catalog per program, withcost_pointsagainst a specific point currency.loyalty.reward_types: parametric and tenant-extensible, withis_systemseeds.- Availability windows, stock limits and per-contact limits.
- Tier-based cost overrides, reserved but unused until FS-LOY-0010.
- Management CRUD and a member-facing read of the available catalog.
Non-scope (normative)
- Redeeming a reward — FS-LOY-0006. This feature defines what exists; spending is a separate act with entirely different failure modes.
- Fulfilment. We record that a reward was redeemed; shipping the coffee is the tenant’s business process. Softcrum is not a fulfilment system and this is not a phase-one gap.
- Money-priced rewards.
cost_pointsis points only; mixed points + money is the redemption seam (DEC-H3), not a catalog concern.
Behaviour (normative)
- A reward belongs to exactly one program and is priced in exactly one point currency. Pricing a reward in status points is FORBIDDEN — status points qualify, they do not buy.
cost_pointsis INTEGER, never a monetary amount, and never in a column shared with one (DEC-B5).- A reward with stock decrements it on redemption, inside the redemption transaction. Stock is never advisory.
- A reward is never hard-deleted: it is deactivated. Historic redemptions must keep resolving to what the member actually received.
- Changing
cost_pointsdoes not alter past redemptions — the redemption records the cost that applied at the time. - Tenant-created
reward_typesrows haveis_system = falseand are scoped to that tenant. A tenant can never modify or deactivate anis_systemrow. - The member-facing read returns only rewards that are active, in window, in stock, and within the member’s per-contact limit.
Data (normative)
API (normative)
Events (normative)
None. A catalog change is an administrative act covered by the audit log; no consumer reacts to it.Acceptance criteria (normative)
- A reward priced in a
statuscurrency is rejected at creation with a typed error. - A tenant-created reward type is visible only to that tenant — proven by a cross-tenant read.
- A tenant attempting to deactivate an
is_systemreward type is rejected. - Deactivating a reward keeps every historic redemption resolvable to its name and cost.
- Changing
cost_pointsfrom 2000 to 2500 leaves a redemption made at 2000 reporting 2000. members/me/rewardsp95 <150 ms over a 500-reward catalog and excludes out-of-stock, out-of-window and limit-exhausted rewards.- Negative: no endpoint allows a hard delete of a reward.
Execution
Single slice, synchronous command. Schema in TS-001, endpoints in the module’s first API slice.Open questions
None outstanding. Every question this spec carried was answered in the consolidated register (../../../design/open-questions-v1.md, v1.1) and folded
into the normative sections above.