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
2 changes: 2 additions & 0 deletions GITHUB_ISSUES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# GitHub Issue Backlog: Secure Commerce Program

**MEMBERS-CANCEL-001A / #705 (2026-10-11):** owner-approved cancellation retains membership access through the already-paid annual term and does not issue an automatic refund. Scope: versioned backend source policy, existing authority expiry/ownership/suspension regressions, and officer handoff. No cancellation endpoint, persistence, claim, Rule, IAM, website, provider or deployment change. #114 and hosted membership/event signup/cancellation remain open. Officer impact: the rule is recorded; no new member control is available. Officer documentation: entrypoint and Events/Shop/Members guide. Deployment evidence: source/local checks only until native issue records otherwise.

**CI-001C4 / #701 (2026-10-11):** exactly `createMemberOnSignUp` and `ensureMemberProfile` were deployed to isolated engineering staging by protected run 38176811598 from `2843388`, with six exact-main checks and normal delegated daliu approval. Both native source archives match the reviewed bundle; Node 22/min0/max2/256 MB/30s and explicit runtime/build identities are verified. Genuine browser App Check/Auth, seven callable denials, signup, recovery, two concurrency cases, profile/claim preservation and background replay passed. Two owned test accounts/profiles were removed; Auth count0/empty collection metadata restored. No schema migration, real data, email, payment, website publication or production deployment. Officer impact: verified staging profiles and no administrator bypass. Officer documentation: low-cost procedure, maps and entrypoints updated. Deployment evidence: native issue and dated guide. #113/#133/#136 and broader core operations stay open.

**Prepared:** 2026-07-12
Expand Down
2 changes: 2 additions & 0 deletions IMPLEMENTATION_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ RUNTIME-001 [#691](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/691) pr

## 1. Program outcome

**Membership cancellation policy, 2026-10-11 — #705:** Dave approved retaining access until the already-paid annual term ends, without an automatic refund. Versioned backend source configuration and boundary regressions are a small dependency-clearing slice only. A member-owned durable cancellation request, retries/audit, UI and disposable hosted staging proof remain separate implementation work; do not route this request through legacy admin race cancellation or mark the membership ended early.

MPRC can open a race or merchandise item for sale only when the platform can:

- Prove one client request creates at most one active Checkout Session and business record.
Expand Down
2 changes: 2 additions & 0 deletions OFFICER_START_HERE.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ Attach the exact wording, public link, or approved photo when you have it. The A

## Choose the closest task

**Membership cancellation decision, 2026-10-11:** Dave approved keeping access through the already-paid annual term, without an automatic refund. [#705](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/705) records the rule and checks the existing term-end logic only. A member cancellation control is **NOT AVAILABLE YET**; do not edit a member record or use race cancellation as a workaround. See [the membership policy handoff](./docs/officers/EVENTS_SHOP_MEMBERS.md#membership-cancellation--policy-approved-not-available-yet).

**Staging profiles verified, 2026-10-11:** [#701](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/701) deployed and tested exactly two profile Functions, using scoped identities, genuine browser App Check and made-up accounts. Signup, recovery, denied requests, retries and concurrency passed; both test accounts/profiles were removed. With Dave's delegated decision, administrator bypass is disabled for production/backend-staging; normal self-approval remains. Original read-only staging is unchanged. Follow [the dated staging proof](./docs/officers/LOW_COST_COMMUNITY.md#engineering-staging-profiles--701). Production, membership, registration and payments are not launched; no officer handles a key or member record.

**Backend ownership, 2026-10-11:** Dave approved continuing without a backup owner under [#697](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/697). Dave remains the sole backend release/recovery approver; backup coverage is absent, not complete. This removes only the backup-appointment gate. Protected deployment, staging proof, rollback and all data/payment approvals remain required. See [the sole-owner procedure](./docs/officers/ACCESS_CONTINUITY.md#sole-owner-backend-exception--approved-october-11-2026); no officer handles keys or runs commands.
Expand Down
2 changes: 2 additions & 0 deletions OPERATIONS_RUNBOOK.md
Original file line number Diff line number Diff line change
Expand Up @@ -828,6 +828,8 @@ The current protected release plan names only the profile-recovery Functions. It

## 9. Staging dress rehearsal

For future membership cancellation acceptance, use a disposable account and a synthetic approved term. Prove access remains just before the original term end and is denied at/after it; the term and payment evidence must not change. Also prove ownership denials, repeated/lost-response recovery, and that suspension is not undone. #705 currently supplies only the approved policy and existing-authority unit regressions, not the cancellation request or hosted proof. Do not test with production members or real payments.

Before every major payment release:

1. Create a capped test race and a product with one unit in a staging Firebase project.
Expand Down
2 changes: 2 additions & 0 deletions STRIPE_COMMERCE_DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -1219,6 +1219,8 @@ The #161 classifier uses only synthetic fixtures. It can split known legacy mean

### Cancellation

Membership cancellation is separate from race/Checkout cancellation. Under owner-approved #705, voluntary membership cancellation retains access through the already-paid annual term and issues no automatic refund. The versioned source policy is `functions/membershipCancellationPolicy.json`; it does not run a refund, expire a Session, disable an existing refund handler, or establish a deployed cancellation workflow. The first annual dues design remains a one-time payment, not a subscription. Race cancellation rules below are unchanged.

- Cancelling an unpaid record expires the active Stripe Session before or as part of the saga, then releases the hold.
- If Session expiry fails transiently, keep a cancellation-pending state and retry; do not merely mark local cancelled while payment remains possible.
- Cancelling a paid record requires an explicit refund/no-refund policy and permission. It cannot be the same operation as unpaid cancellation.
Expand Down
2 changes: 2 additions & 0 deletions SYSTEM_DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -782,6 +782,8 @@ WEB-002C [#623](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/623) publi

### 8.0a Provider-neutral membership authority and entitlement — SOURCE ONLY, UNUSED

**Approved cancellation rule, 2026-10-11 — #705:** `functions/membershipCancellationPolicy.json` records schema 1 / policy `mprc-membership-cancellation-2026-10-11-v1`. Dave selected access through the already-paid annual term, with no automatic refund. A future voluntary cancellation command must record its request separately, not shorten/extend the approved term, set it `ended` early, change payment evidence, or grant claims. Access still ends at the existing exclusive `endsAtMs`; unlinked, wrong-account, suspended and ended records remain denied. This does not override security suspension, authorize refunds, prevent a later separately approved renewal, or create an automatically recurring subscription. The file is not runtime-imported; regression tests pin the existing authority boundary, not a cancellation command. Persistence, UI and hosted staging acceptance remain **NOT AVAILABLE YET**.

MEMBERS-IDENTITY-001A [#208](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/208), with its command-order correction in MEMBERS-IDENTITY-001H [#451](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/451), defines one unused pure contract that keeps a stable MPRC membership separate from the Firebase account used to sign in. A membership record can exist and receive monotonic term decisions without a UID. Such a record grants no website entitlement. Google, WhatsApp, Strava, email equality, a profile role, and any browser field remain projections or inputs to future reviewed workflows; none is membership authority.

```mermaid
Expand Down
6 changes: 6 additions & 0 deletions docs/officers/EVENTS_SHOP_MEMBERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ The repository contains screens for these jobs. Their presence does not prove th

There is currently no proven no-code switch that safely stops all new Stripe payments. Hiding a button, closing an event, or marking a product sold out may leave an already-created Stripe Checkout page payable.

## Membership cancellation — POLICY APPROVED, NOT AVAILABLE YET

Dave approved this rule on October 11, 2026: cancelling membership keeps access through the already-paid annual term. Cancellation does not issue a refund. It does not extend the term or restore suspended access. Refunds and race cancellation remain separate.

There is no new member cancellation button or officer procedure from [#705](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/705). Its versioned rule and local tests do not prove a hosted workflow. Do not edit membership dates, mark a term ended, issue a refund, or use an admin race action as a workaround. Ask Dave, the sole release owner, for the separately reviewed request workflow and disposable staging proof before using it. A specialist is still needed to build and release that workflow; an officer needs no terminal, source edit, secret or member record to review this decision.

## Approval table

| Change | Required owners |
Expand Down
6 changes: 6 additions & 0 deletions functions/membershipCancellationPolicy.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"schemaVersion": 1,
"policyVersion": "mprc-membership-cancellation-2026-10-11-v1",
"accessEndsAt": "paid_term_end",
"automaticRefund": false
}
111 changes: 111 additions & 0 deletions functions/membershipCancellationPolicy.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
'use strict';

const policy = require('./membershipCancellationPolicy.json');
const {
createMembershipAuthority,
applyMembershipAuthorityCommand,
deriveMembershipEntitlement,
} = require('./membershipAuthority');

// These regressions pin the approved policy and the existing authority seam.
// They do NOT execute a cancellation command or prove persistence or deployment.
const START = 1_000_000;
const END = 2_000_000;
const UID = 'uid_cancellation_fixture';

function membership(termState = 'approved', linked = true) {
let record = createMembershipAuthority({
membershipAuthoritySchemaVersion: 1,
membershipId: 'mbr_cancellation_fixture',
commandId: 'cmd_create_fixture',
});
record = applyMembershipAuthorityCommand(record, {
membershipAuthoritySchemaVersion: 1,
commandType: 'record_term_decision',
commandId: 'cmd_term_fixture',
expectedRevision: 1,
termRevision: 1,
termState,
termId: 'term_cancellation_fixture',
startsAtMs: START,
endsAtMs: END,
planRef: 'plan_fixture',
evidenceRef: 'evidence_fixture',
policyVersion: policy.policyVersion,
});
if (linked) {
record = applyMembershipAuthorityCommand(record, {
membershipAuthoritySchemaVersion: 1,
commandType: 'associate_account',
commandId: 'cmd_link_fixture',
expectedRevision: 2,
uid: UID,
});
}
return record;
}

function entitlement(record, asOfMs, uid = UID) {
return deriveMembershipEntitlement({
membershipAuthoritySchemaVersion: 1, record, uid, asOfMs,
}).entitlement;
}

describe('owner-approved membership cancellation policy (source only)', () => {
test('pins a versioned paid-term-end rule with no automatic refund', () => {
expect(policy).toEqual({
schemaVersion: 1,
policyVersion: 'mprc-membership-cancellation-2026-10-11-v1',
accessEndsAt: 'paid_term_end',
automaticRefund: false,
});
});

test.each([
[START - 1, 'not_entitled'],
[START, 'current_member'],
[END - 1, 'current_member'],
[END, 'not_entitled'],
[END + 1, 'not_entitled'],
])('existing paid term at %i returns %s without rewriting it', (instant, expected) => {
const record = membership();
expect(entitlement(record, instant)).toBe(expected);
expect(record.term.state).toBe('approved');
expect(record.term.endsAtMs).toBe(END);
expect(record.revision).toBe(3);
});

test('repeated evaluation never extends the paid term or changes evidence', () => {
const record = membership();
const before = JSON.stringify(record);
for (let attempt = 0; attempt < 3; attempt += 1) {
expect(entitlement(record, END - 1)).toBe('current_member');
expect(entitlement(record, END)).toBe('not_entitled');
}
expect(JSON.stringify(record)).toBe(before);
});

test.each(['suspended', 'ended'])('does not revive a %s term', (state) => {
expect(entitlement(membership(state), END - 1)).toBe('not_entitled');
});

test('does not confer access to a different account', () => {
expect(entitlement(membership(), END - 1, 'uid_other_fixture')).toBe('not_entitled');
});

test('does not confer access to an unlinked membership', () => {
expect(entitlement(membership('approved', false), END - 1)).toBe('not_entitled');
});

test('does not convert a pending decision into a paid membership', () => {
expect(entitlement(membership('decision_pending'), END - 1)).toBe('decision_pending');
});

test.each([START, Number.NaN, Number.MAX_SAFE_INTEGER])(
'rejects a malformed term end instead of granting access (%s)', (endsAtMs) => {
const record = membership();
const malformed = { ...record, term: { ...record.term, endsAtMs } };
expect(() => entitlement(malformed, END - 1)).toThrow();
},
);
});
Loading