Skip to main content

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, with cost_points against a specific point currency.
  • loyalty.reward_types: parametric and tenant-extensible, with is_system seeds.
  • 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_points is points only; mixed points + money is the redemption seam (DEC-H3), not a catalog concern.

Behaviour (normative)

  1. 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.
  2. cost_points is INTEGER, never a monetary amount, and never in a column shared with one (DEC-B5).
  3. A reward with stock decrements it on redemption, inside the redemption transaction. Stock is never advisory.
  4. A reward is never hard-deleted: it is deactivated. Historic redemptions must keep resolving to what the member actually received.
  5. Changing cost_points does not alter past redemptions — the redemption records the cost that applied at the time.
  6. Tenant-created reward_types rows have is_system = false and are scoped to that tenant. A tenant can never modify or deactivate an is_system row.
  7. 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)

  1. A reward priced in a status currency is rejected at creation with a typed error.
  2. A tenant-created reward type is visible only to that tenant — proven by a cross-tenant read.
  3. A tenant attempting to deactivate an is_system reward type is rejected.
  4. Deactivating a reward keeps every historic redemption resolvable to its name and cost.
  5. Changing cost_points from 2000 to 2500 leaves a redemption made at 2000 reporting 2000.
  6. members/me/rewards p95 <150 ms over a 500-reward catalog and excludes out-of-stock, out-of-window and limit-exhausted rewards.
  7. 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.

Changelog

Delivery record

Not implemented yet.