> ## 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.

# Tiers and Qualification Windows

> After this ships, a member can be Gold, know exactly what keeps them Gold, and get better earning rates for being Gold.

## Context

Tiers are the retention mechanism of a loyalty program: they give a member something to lose. That
also makes them the most emotionally charged feature in the module — a downgrade a member did not
see coming is a support ticket and often a churned customer.

Two decisions carry most of the weight. **The qualification window** — rolling twelve months versus
calendar year — changes the entire feel of the program: rolling is continuous and fair, calendar
creates a year-end push and a January cliff. And **the downgrade policy**, which needs a grace
period not for correctness but because dropping someone the instant they fall below a threshold is
a product failure even when it is arithmetically right.

This is what status points exist for (ADR-011). Qualifying on redeemable points would mean a member
loses status by spending, which punishes exactly the behaviour the program wants.

## Scope *(normative)*

* `loyalty.tier_definitions`: ordered levels with thresholds against a qualification metric.
* `loyalty.tier_qualification_metrics`: parametric, `is_system` seeds `points_earned`, `spend`,
  `event_count`.
* Window modes `rolling_days` and `calendar_year`.
* `loyalty.tier_memberships` with full history: entered, exited, reason.
* Explicit downgrade policy with a configurable grace period.
* `loyalty.tier_overrides`: earning multipliers, reward pricing, expiration extension per tier.
* Recomputation driven by ledger events, with a scheduled safety net.

## Non-scope *(normative)*

* Tier-based notifications. This feature emits the events; the campaigns are `messaging`.
* Manual tier assignment by staff — a later console feature with its own audit semantics.
* Tier-gated reward visibility beyond the pricing override, which is FS-LOY-0005's concern.

## Behaviour *(normative)*

1. Tiers are ordered by `level`, unique per program, with strictly increasing thresholds. Two tiers
   at the same level are FORBIDDEN.
2. Qualification uses the configured metric over the configured window. Redeemable points **never**
   qualify: spending must not cost status.
3. Upgrades are immediate on crossing a threshold. A member should never wait to receive good news.
4. Downgrades are **never immediate**. The member enters a grace period; if they requalify before
   it ends, no downgrade is recorded at all. Only on expiry does the downgrade apply and emit.
5. Membership history is append-only. A member's tier at any past date must be reconstructible —
   this is what answers "why was I charged that price".
6. `tier_overrides` apply at the moment of the transaction, not retroactively. Reaching Gold does
   not re-price yesterday's redemption.
7. Recomputation is event-driven on ledger writes and re-verified by a scheduled job at the
   tenant's freshness tier. Divergence is alerted, never silently corrected.
   7b. **When the status currency is configured to expire** (OQ-LOY-02, FS-LOY-0002), the
   qualification metric falls on its own and a member can drop a tier without having stopped
   buying. In that configuration the grace period and the `tier.grace_started` event are
   **mandatory**, and `members/me/tier` MUST show the projected drop and its date before it
   happens. Flexibility that surprises a member is a support ticket, not a feature.
8. `tier_qualification_metrics` is **not** tenant-extensible: the metric governs system logic.

## Data *(normative)*

| Table                                | Key invariants                                                                                                                              |
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `loyalty.tier_definitions`           | `program_id`; `level` unique per program; `threshold`; `metric_code` FK; `window_mode` and `window_days`; `grace_period_days`; `is_active`  |
| `loyalty.tier_memberships`           | append-only; `contact_id`, `tier_id`, `entered_at`, `exited_at` nullable, `reason_code`; at most one open membership per (program, contact) |
| `loyalty.tier_overrides`             | FK tier; `earning_multiplier`, `reward_cost_multiplier`, `expiration_extension_days`, all nullable = no override                            |
| `loyalty.tier_qualification_metrics` | parametric; `is_system` seeds as scoped; not tenant-extensible                                                                              |

## API *(normative)*

| Endpoint                           | Class      | Permission                             | Budget       |
| ---------------------------------- | ---------- | -------------------------------------- | ------------ |
| `GET/POST/PATCH /v1/loyalty/tiers` | Management | `loyalty.tiers.{read\|create\|update}` | p95 \<1 s    |
| `GET /v1/loyalty/members/me/tier`  | Runtime    | member token                           | p95 \<150 ms |

`members/me/tier` returns the current tier, progress toward the next, and — when in grace — the
date the downgrade would apply. Hiding that date would be the single most user-hostile choice
available here.

## Events *(normative)*

`loyalty.tier.upgraded` and `loyalty.tier.downgraded`, both available as outgoing webhooks. Entering
grace emits no event in F1; whether it should is an open question below.

## Acceptance criteria *(normative)*

1. Crossing a threshold upgrades within 5 s p95 of the qualifying ledger write and emits once.
2. Falling below a threshold starts grace and does **not** downgrade. Requalifying inside grace
   records no downgrade at all.
3. Grace expiry downgrades and emits exactly once.
4. Rolling-window correctness: a member whose qualifying activity is 366 days old no longer counts
   it, verified against a seeded timeline.
5. Membership history reconstructs the tier held on any given past date.
6. A tier override multiplier applies to earnings after the upgrade and not to earlier ones.
7. **Negative:** redeeming points never lowers a member's qualification metric.

## Execution

Asynchronous pipeline, driven by ledger events; the safety-net recomputation is a scheduled
per-tenant fan-out job.

## Open questions

None outstanding. Every question this spec carried was answered in the consolidated register
([`../../../design/open-questions-v1.md`](/design/open-questions-v1), v1.1) and folded
into the normative sections above.

## Changelog

| Version | Date       | Change                                                                     | Why                                      | Author                 |
| ------- | ---------- | -------------------------------------------------------------------------- | ---------------------------------------- | ---------------------- |
| 0.2.0   | 2026-08-17 | Open questions resolved (OQ-LOY-\*) and folded into the normative sections | Owner answered the consolidated register | daniel + claude-opus-5 |
| 0.1.0   | 2026-08-17 | Initial draft                                                              | —                                        | daniel + claude-opus-5 |

## Delivery record

*Not implemented yet.*
