---
id: STATE-MBR-STR-005
title: Holding standing on a date
object: DOM-MBR-STR-005
product_area: membership
module: structure
revision: 1
status: draft
design_intent: proposed
implementation_evidence: observed
source_sha:
  services-membership: 5994c270e8a37a30f848e99ba5b5ee6b87c08893
  react-console: baf6dae382d6f41dce976c7f848667ccc37e076f
states:
  - name: none
    meaning: No effective or pending term gives the holding a standing.
  - name: pending
    meaning: 'No effective term exists, but a pending term has been prepared.'
  - name: future
    meaning: An effective term starts later and no effective term covers or precedes the requested date.
  - name: active
    meaning: A covering effective term's classification confers benefits.
  - name: active_without_benefits
    meaning: An effective term covers the date without conferring classification benefits.
  - name: grace
    meaning: >-
      The latest effective term has ended, no other term covers the date and the inclusive grace allowance has
      not expired.
  - name: lapsed
    meaning: No term covers the date and the grace following the latest ended effective term has finished.
  - name: suspended
    meaning: The suspension flag overrides every term-based standing.
transitions:
  - from: future
    to: active
    trigger: The requested day reaches a future term's start.
    guard: >-
      A benefit-conferring effective term covers that day and the holding is not suspended; the sweep selects
      start-date boundaries.
    side_effects:
      - Memberships can update the cached standing after recomputation.
    who: Membership sweep using the engine
    provenance: observed
    source: 'services-membership:modules/terms/services/sweep.ts:sweepBoundariesOf'
  - from: pending
    to: active
    trigger: A prepared term is fulfilled while its dates cover today.
    guard: Memberships completes its guarded term commit under the holding lock.
    side_effects:
      - The owning transaction recomputes the cache.
    who: Memberships
    provenance: observed
    source: 'services-membership:modules/memberships/services/membership-caches.ts'
  - from: active
    to: grace
    trigger: The requested day passes the covering term's last day.
    guard: 'No effective term covers the date, suspension is absent and positive grace remains.'
    side_effects:
      - 'Sweep detection records expiry once for the term, dated the day after its end.'
    who: Membership sweep using the engine
    provenance: test
    source: >-
      services-membership:modules/terms/tests/unit/status.spec.ts#holding status: the rows of §2 › is in grace
      on the last day of grace (D = E + grace_days)
  - from: active
    to: lapsed
    trigger: The requested day passes an effective term's end with zero grace.
    guard: There is no covering term or suspension.
    side_effects:
      - The sweep can record expiry and lapse once each.
    who: Membership sweep using the engine
    provenance: observed
    source: 'services-membership:modules/terms/services/sweep.ts:detectionsOf'
  - from: grace
    to: lapsed
    trigger: The requested date passes the inclusive grace boundary.
    guard: No covering renewal or suspension changes the result.
    side_effects:
      - The sweep records the first lapse.
      - >-
        That first detection publishes a termination event with the term's last day without changing the
        stored term state.
    who: Membership sweep using the engine
    provenance: test
    source: >-
      services-membership:modules/terms/tests/unit/sweep.spec.ts#the sweep’s detections › detects the lapse
      the day after grace ends, effective that day, with the expiry
  - from: grace
    to: active
    trigger: A renewal supplies effective coverage for the requested date.
    guard: The owning renewal flow succeeds; date continuation depends on the package rule.
    side_effects:
      - Memberships recomputes caches and resolves the expiring-membership attention item on commitment.
    who: Memberships
    provenance: observed
    source: 'services-membership:modules/terms/services/term-rules.ts:startingPoint'
  - from: lapsed
    to: active
    trigger: A reinstatement or rejoin supplies effective coverage.
    guard: >-
      The owning purchase or staff flow succeeds; terminated-term continuation depends on its reinstatement
      allowance and other terms follow their continuation rule.
    side_effects:
      - Continuity is recalculated and splits when the configured gap is exceeded.
    who: Memberships
    provenance: observed
    source: 'services-membership:modules/terms/services/term-rules.ts:startingPoint'
  - from: active
    to: suspended
    trigger: Staff suspend an active membership.
    guard: >-
      The principal membership version matches, it is not already suspended and the supplied suspension reason
      is active and matches.
    side_effects:
      - The engine suppresses expiry and lapse detections while the suspension flag remains set.
    who: Membership staff operations
    provenance: test
    source: >-
      services-membership:modules/terms/tests/unit/status.spec.ts#holding status: the precedence of §2 › puts
      suspended over grace
  - from: suspended
    to: active
    trigger: Staff restore a suspended membership while it is covered.
    guard: >-
      The principal membership version matches and it is currently suspended; resulting active status also
      requires a benefit-conferring covering term.
    side_effects:
      - The cache is recomputed from current term facts after clearing suspension.
    who: Membership staff operations
    provenance: observed
    source: 'services-membership:modules/terms/services/status.ts:statusOf'
  - from: none
    to: future
    trigger: A first committed term begins after the requested date.
    guard: The owning term write succeeds and no other effective term determines status.
    side_effects:
      - An absent original join date takes the earliest effective start.
    who: Memberships
    provenance: observed
    source: 'services-membership:modules/terms/services/holding-cache.ts:firstJoinDateOf'
  - from: none
    to: pending
    trigger: A pending term is prepared for a holding with no effective term.
    guard: The purchase claim succeeds.
    side_effects:
      - The engine returns pending without persisting anything itself.
    who: Membership purchase participant
    provenance: observed
    source: 'services-membership:modules/terms/services/status.ts:statusOf'
invalid:
  - from: lapsed
    to: grace
    why: >-
      With unchanged inputs, advancing the date cannot return lapse to grace; changing grace allowances or
      terms can change the answer.
  - from: suspended
    to: lapsed
    why: >-
      Suspension continues to override lapse until the flag is cleared; restoration can then produce lapse
      directly.
concurrency: >-
  Status is a calculation over the requested date and current inputs, not a persisted transition machine.
  Memberships serializes cache writes on the holding lock and uniquely records detections. Repeated sweeps
  avoid duplicate detections; catch-up after missing nights refreshes selected records in separate
  transactions.
invariants:
  - INV-DOM-MBR-STR-005-04
  - INV-DOM-MBR-STR-005-06
  - INV-DOM-MBR-STR-005-07
evidence:
  - >-
    services-membership at 5994c270: modules/terms/services/status.ts, sweep.ts, holding-cache.ts,
    term-rules.ts; modules/memberships/services/sweep/status-sweep.ts and membership-caches.ts (the callers).
---

# Holding standing on a date

## The Lifecycle

```mermaid
stateDiagram-v2
  [*] --> none
  none --> pending: purchase prepared
  none --> future: future coverage added
  future --> active: covering date reached
  pending --> active: covering term fulfilled
  active --> grace: end passed with grace
  active --> lapsed: end passed without grace
  grace --> lapsed: grace exhausted
  grace --> active: effective renewal
  lapsed --> active: effective new term
  active --> suspended: suspension flag set
  suspended --> active: restored while covered
```

## Transitions Explained

Standing is recalculated from effective terms, suspension, grace and benefits. The diagram illustrates common changes for benefit-conferring coverage; coverage without benefits follows equivalent date boundaries. Configuration changes can also change a result, and restoration need not return to active standing.

## What Cannot Happen

For unchanged inputs, time cannot move lapse back to grace. Pending and cancelled terms cannot supply coverage. Suspension and effective coverage prevent expiry and lapse detections.

## Concurrency and Timing

The resolver uses current terms and settings. Caches can lag for as long as sweeps fail, while the next successful catch-up recomputes every holding. Membership writes and sweep recomputation use the same holding lock.
