Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions SYSTEM_DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -751,6 +751,40 @@ This contract invents no revocation SLA, grace period, expiry date, timing, or c

The module is imported by no runtime or Functions index and updates no officer procedure because it makes nothing officer-observable. It requires only `node:util`, reads no clock or environment, calls no Firebase/Stripe/provider service, stores nothing, logs nothing, and changes no current profile/role/claim. Source tests and a merge are not Firebase deployment or live behavior proof.

### 8.0h Member/account status projection and renewal affordance — SOURCE ONLY, UNUSED

MEMBERS-DUES-001F [#437](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/437) defines one unused pure contract for **item 6** of parent [#114](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/114) — "Minimum member/account status projection and renewal UX after the server contract is proven", whose acceptance line requires member pricing and protected content to consume "the server-derived active-membership projection rather than Auth provider or raw profile fields". §8.0a `deriveMembershipEntitlement` is the authorization gate: it returns one of three frozen results — `current_member` / `not_entitled` / `decision_pending` — **collapsing every reason** (a term an hour from expiry, an elapsed term, a refund-suspended term, an offboarded ended term, a not-yet-started future term, an unlinked record) into the same `not_entitled`, and it applies no renewal-window threshold and offers no renewal affordance. A member's own account page and the member-pricing surface need those reasons **disaggregated** plus a renewal affordance; that disaggregation is the ONLY thing this contract adds. §8.17 / §8.18 / §8.19 each forward-declare it unbuilt ("no member/account status projection or renewal UX (item 6) … remain with #114").

It is deliberately a projection OF the shipped authority §8.0a, which it **COMPOSES rather than re-implements** — the one source-only core that imports a sibling, a documented departure from the `node:util`-only norm the other cores follow. The safety property that matters here is **display never over-states authorization**: any status that reads as entitled (`active` / `expiring_soon`) must correspond to a `current_member` authorization, always. Composing the real authority makes that invariant TRUE BY CONSTRUCTION and robust to any future change in §8.0a's window logic, and it re-implements NONE of §8.0a's record validation (revision math, enum / time / opaque-id checks) — re-implementing that in a second module would be the salami failure mode this project forbids. The imported sibling is itself pure and `node:util`-only, so purity / determinism / no-I/O hold transitively; a source-boundary test locks this module's require set to exactly `{ node:util, ./membershipAuthority }` and locks `membershipAuthority`'s own require set to `{ node:util }`.

```mermaid
flowchart LR
V["§8.0a entitlement + record view + asOfMs + policy.renewalWindowMs"] --> G{"Well-formed input & policy?"}
G -- "No" --> X["denied: malformed_input | malformed_policy (input never echoed)"]
G -- "Yes" --> E{"§8.0a entitlement?"}
E -- "decision_pending" --> P["pending — no renewal"]
E -- "current_member" --> W{"endsAtMs - asOfMs <= renewalWindowMs?"}
W -- "No" --> A["active — no renewal"]
W -- "Yes" --> ES["expiring_soon — renewal offered"]
E -- "not_entitled" --> L{"linked & uid matches?"}
L -- "No" --> N["none — no renewal (a join, not a renewal)"]
L -- "Yes" --> T{"term state / window?"}
T -- "suspended" --> S["suspended — no renewal (clawback)"]
T -- "ended" --> EN["ended — no renewal (owner re-admits)"]
T -- "approved, asOfMs < startsAtMs" --> U["upcoming — no renewal (already renewed)"]
T -- "approved, asOfMs >= endsAtMs" --> EX["expired — renewal offered"]
```

Text alternative: the authoritative entitlement §8.0a derives is taken verbatim and disaggregated within its bucket. A `decision_pending` entitlement displays `pending`. A `current_member` entitlement — §8.0a's only entitled branch, a linked and matched association with an approved term whose window contains `asOfMs` — displays `expiring_soon` when the owner-configured `renewalWindowMs` covers the remaining time (`endsAtMs - asOfMs <= renewalWindowMs`, boundary inclusive) and `active` otherwise. A `not_entitled` entitlement is graduated in §8.0a's own order: an unlinked or uid-mismatched record is `none` (a non-member — a join, not a renewal); else a `suspended` term is `suspended` and an `ended` term is `ended`; else the term is approved but outside its window, so `asOfMs < startsAtMs` is `upcoming` (already renewed ahead) and `asOfMs >= endsAtMs` is `expired`. Renewal is offered for exactly `expiring_soon` and `expired`; the term-end instant is displayed for `active`/`expiring_soon`/`upcoming`/`expired` and is `null` otherwise. A malformed input or policy shape — proxy, revoked proxy, foreign prototype, array, symbol key, extra or missing field, accessor, bad version, or out-of-range time — denies with a fixed reason and never echoes the input; a malformed record makes the composed authority throw, which is caught and mapped to `malformed_input`.

The marquee property is that display never over-states authorization, proven across the seam: the projection's `entitlement` is `deriveMembershipEntitlement`'s output verbatim, so a status reads as entitled (`active`/`expiring_soon`) **if and only if** §8.0a returns `current_member`. The test drives the REAL authority over every fixture and a swept range of instants, asserting the projected entitlement equals the authority's and that no not-current record ever displays as entitled — including an approved, in-window term linked to a **different** uid (→ `none`, never `active`) and an in-window **suspended** term (→ `suspended`, never `active`). The second invariant is that renewal is offered only where a safe self-serve re-purchase is correct: `renewalOffered` is `true` for exactly `expiring_soon` and `expired`, and never for a `suspended` term (a refund/dispute clawback — re-purchase could re-grant disputed access or double-charge), an `ended` term (offboarding — the owner re-admits), a `pending` decision, an already-renewed `upcoming` term, a comfortably-`active` term, or a non-member.

The CommonJS module reads `input` `{ membershipAccountStatusSchemaVersion, record, uid, asOfMs }` and `policy` `{ membershipAccountStatusSchemaVersion, renewalWindowMs }` through the same descriptor-based closed-object read the membership-dues siblings use (getters never invoked; proxy / foreign prototype / array / symbol / extra / missing all deny), composes the shipped authority for the entitlement, and returns one frozen verdict; it never throws for any value in either argument position. The `record` is validated once by the composed authority and never re-validated here, so the two contracts share a seam but no duplicated validation. The verdict carries a display status, a boolean, the authoritative entitlement, and one term-end instant — no code, token, role, price, amount, account reference, PII, or command; a source-boundary test enforces the absence of that vocabulary. It is a read-model, so access itself stays gated by §8.0a at the access point.

This contract invents no policy and duplicates no sibling. It owns the *member-facing status disaggregation and renewal affordance* — which of eight display states this member is in and whether a self-serve renewal is correct — and it is deliberately not any neighbour. §8.0a `deriveMembershipEntitlement` is the authorization gate it composes; this adds no authority and only disaggregates the reason §8.0a collapses. §8.13 `projectMemberDiscounts` is a *consumer* of membership status at a content/pricing surface; this is the *producer* of the display projection such a surface reads, and grants no discount. §8.0g (MEMBERS-DUES-001E) is item 4's token-refresh/revocation disposition — a session-lifecycle decision, not a display read-model. §8.17 / §8.18 / §8.19 reconcile verified payment / reversal / checkout into term *commands*; this issues no command and reconciles no payment. §8.0c `membershipClaimReconciliation` derives the desired *claim value*; this writes and reads no claim. It performs no Auth-claim read or write, no token action, no renewal Checkout creation, no persistence, and no route or UI — the "renewal UX" wiring around this read-model, the actual member-pricing and protected-content enforcement that would consume it, and the owner-configured `renewalWindowMs` all remain with #114 and its dependencies.

The module is imported by no runtime or Functions index and makes nothing officer- or member-observable. It requires only `node:util` and `./membershipAuthority` (itself `node:util`-only), reads no clock or environment, calls no Firebase/Stripe/provider service, stores nothing, logs nothing, and changes no current profile/role/claim. Source tests and a merge are not Firebase deployment or live behavior proof.

### 8.1 Paid race registration

PAY-001B1 [#219](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/219) adds only the browser projection and first two server validation steps below. The website sends the active field set and omits volunteer tier. The callable preserves the opaque event ID, accepts an exact bounded envelope before Firestore, matches answers against the admitted selected server fields, and encodes callback values. It does not add the target request ID, snapshot, transaction, reservation, idempotent Session saga, safe confirmation capability, deployment, or live proof.
Expand Down
Loading