diff --git a/docs/LAUNCH_READINESS.md b/docs/LAUNCH_READINESS.md index 4a7854d..a787c19 100644 --- a/docs/LAUNCH_READINESS.md +++ b/docs/LAUNCH_READINESS.md @@ -1,6 +1,6 @@ # Launch Readiness -**Status as of 2026-04-18.** Target launch: before 2026-05-01. +**Status as of 2026-04-20.** Target launch: before 2026-05-01. Single source of truth for work that must happen before cutover. P0 blocks launch. P1 is strongly preferred but survivable. Completed items have been archived — see `~/.claude/projects/-Users-jennie-Sites-ghostguild-org/memory/project_launch_readiness_archive.md`. Post-launch backlog lives in `docs/TODO.md`. @@ -8,36 +8,27 @@ Single source of truth for work that must happen before cutover. P0 blocks launc ## Current state -- Vitest: 577/577 passing. -- Helcim plan consolidation migration ran against prod 2026-04-18 (Monthly plan id `50302`, Annual plan id `50303`). All six paid-flow manual tests pass via tunnel. -- Remaining launch blockers: see lists below. +- Vitest on `main`: **652/658 passing**. 6 pre-existing failures in `tests/server/api/helcim-payment.test.js` — unrelated to launch-blocking work, noted in the deploy checklist for visibility. +- `main` is now caught up locally (2026-04-20): `feature/helcim-plan-consolidation` (40 commits) and `feature/contribution-amount-redesign` (17 commits) fast-forwarded in. Not pushed — site is not on Netlify yet. +- Helcim plan consolidation migration ran against prod 2026-04-18 (Monthly plan id `50302`, Annual plan id `50303`). **Contribution-amount migration has NOT yet been run against prod.** --- ## P0 — Must fix before launch -None outstanding. Privacy/Terms pages shipped, duplicate-customer bug fixed, pre-deploy migrations run. +None outstanding. --- ## P1 — Strongly preferred before launch -### In-app billing management; demote Helcim portal to escape hatch -- Helcim's hosted portal requires a separate password the member never set during `/join`. First-touch flow is "click link → see Helcim login → click Forgot password → wait for email → set password → sign in." Reads as broken. -- **Ship in-app equivalents for the 80% case:** - - Past invoices / receipts list on `/member/account` — new server route pulls from Helcim invoice API by `helcimCustomerId`; render a simple list with date, amount, download/view link. - - Change card — reuse `useHelcimPay` composable to get a new `cardToken`, then new server route updates the customer's default payment method and the active subscription's payment method. -- **Keep** the existing "Manage billing in Helcim →" link but relabel to something like "Advanced billing in Helcim →" so it reads as an escape hatch for disputes/edge cases, not the primary surface. -- Rough scope: 1–2 days. Two new `server/api/helcim/*` routes + two new sections on `/member/account`. -- **Status (2026-04-19):** Implemented on `feature/helcim-plan-consolidation` — `server/api/helcim/payment-history.get.js`, `server/api/helcim/update-card.post.js`, plus the two new sections on `app/pages/member/account.vue`. Manual browser tests for both flows are in the "Manual browser tests still needed" section below. - ### Charitable receipts — Phase 1 (`docs/specs/receipts-launch-spec.md`) Spec exists but **none of it is implemented**. Phase 2 (the actual receipt generation) is post-launch (live Jan 2027), but Phase 1 must land at launch so Phase 2 has the data + compliance posture it needs. Skipping Phase 1 means: payments made between launch and Phase 2 may be missing fields needed to receipt them, and the Helcim default confirmation email may make CRA-noncompliant claims. What needs to ship: - **Payment logging.** Every successful Helcim payment writes a record (member id, amount CAD, payment date, Helcim transaction id, payment type monthly/annual, `receiptIssued: false`, `receiptId: null`). Failed payments logged separately. No `Payment` model exists today; needs new model + webhook wiring in `server/api/helcim/`. - **Helcim confirmation email review.** Customize so it does NOT use "tax receipt," "donation receipt," or "for tax purposes," and includes the registered charity name "Baby Ghosts Studio Development Fund" plus the disclaimer wording from the spec (section 2). If Helcim's template editor can't accomplish this, suppress the Helcim confirmation and send our own via Resend. -- **Join page copy.** Add the factual charity / tax-deductible note near the contribution tiers per spec section 3 — `app/pages/join.vue`. No form fields, just the line. +- **Join page copy.** Add the factual charity / tax-deductible note near the contribution amount input per spec section 3 — `app/pages/join.vue`. No form fields, just the line. - **Member schema field.** Add `taxReceiptPreferences: { filesCanadianTaxes: Boolean, middleInitial: String|null, confirmedAddress: {...}|null, setupCompletedAt: Date|null }` to `server/models/member.js`. Default null/false. Not exposed in UI yet. Rough scope: 1 day if Helcim's email template is editable; +0.5 day if we have to route confirmations through Resend instead. Decide which path during implementation. @@ -46,13 +37,13 @@ Rough scope: 1 day if Helcim's email template is editable; +0.5 day if we have t ## Deploy checklist -Pre-deploy migrations have all been run. What's left: +Applies when the site is connected to Netlify / production hosting. Nothing here is actionable until that connection exists; kept here so nothing gets forgotten at cutover. -- [ ] Merge `feature/helcim-plan-consolidation` into `main`. -- [ ] Merge `feature/contribution-amount-redesign` into `main` (forked from `feature/helcim-plan-consolidation`; renames `contributionTier` → `contributionAmount` with arbitrary whole-dollar amounts, annual = amount × 12, no discount framing). -- [ ] Run `node scripts/migrate-contribution-amount.cjs --apply` against prod Mongo AFTER contribution-amount merge and BEFORE Netlify deploy of that change. Idempotent; dry-run against local counted 34 members. -- [ ] Set `NUXT_HELCIM_MONTHLY_PLAN_ID=50302` in Netlify production env. -- [ ] Set `NUXT_HELCIM_ANNUAL_PLAN_ID=50303` in Netlify production env. +- [ ] Push local `main` to `origin/main`. +- [ ] **Run `node scripts/migrate-contribution-amount.cjs --apply` against prod Mongo BEFORE the new code serves traffic.** Idempotent; dry-run on local counted 34 members. Requires `MONGODB_URI` in env. The script writes `contributionAmount` (Number) derived from existing `contributionTier` (String) on every Member doc; the old field is left intact for a window. +- [ ] Set `NUXT_HELCIM_MONTHLY_PLAN_ID=50302` in production env. +- [ ] Set `NUXT_HELCIM_ANNUAL_PLAN_ID=50303` in production env. +- [ ] Decide on the 6 failing tests in `tests/server/api/helcim-payment.test.js` — either fix or consciously accept. Not launch-blocking, but pre-existing red tests tend to mask new regressions. **Env vars required in production (reference):** - `MONGODB_URI` @@ -70,17 +61,64 @@ Pre-deploy migrations have all been run. What's left: ## Manual browser tests still needed -Cannot be verified by Vitest. All require a real browser + real Helcim test card + real email. +Cannot be verified by Vitest. Both require a real browser + real Helcim test card + real email, via cloudflared tunnel or ngrok HTTPS (Helcim requires HTTPS for the pay.js iframe). -- [x] **Event ticket purchase with payment** (HelcimPay.js iframe; use cloudflared tunnel or ngrok HTTPS). *(Verified 2026-04-19 via tunnel: guest purchase on "Cooperative Game Dev Masterclass" succeeded — registration recorded with `paymentStatus: completed`, `paymentId: 47230660`, `tickets.public.sold` incremented, guest Member created. Member-ticket path not exercised because this event's member price is $0; no paid-member-ticket event currently seeded.)* -- [ ] **Pre-registrant invite → accept flow** with a paid contribution amount (exercises Helcim customer creation during acceptance). *(Refactor to arbitrary contribution amounts landed on `feature/contribution-amount-redesign` 2026-04-19; retest once that branch merges.)* -- [ ] **Contribution-amount redesign end-to-end** on `feature/contribution-amount-redesign` with Helcim sandbox: `/join` with arbitrary amount (incl. $0 and a non-preset like $17) for both Monthly and Annual cadence; `/member/account` edit contribution amount up and down; admin edit of a member's `contributionAmount` via `/admin/members/[id]`. Verify ×12 annual math on UI and on Helcim `subscription.recurringAmount`, guidance chips match via `findLast`, no "save $X" / "2 months free" copy anywhere. Run `node scripts/migrate-contribution-amount.cjs --apply` against local Mongo first so the test members have the new field. -- [x] **Magic-link login** including 15-min expiry and jti burn on reuse. *(Verified 2026-04-19 against local dev + local Mongo. Target: `alex.rivera@pixelcollective.coop` (active, member role). Happy path: `POST /api/auth/login` → 200, `magicLinkJti` set, `magicLinkJtiUsed:false`; reconstructed token from stored jti + `NUXT_JWT_SECRET` and `POST /api/auth/verify` → 200 with `redirectUrl:/member/dashboard`, `auth-token` cookie set (httpOnly, Max-Age=604800, SameSite=Lax), `magicLinkJtiUsed:true`, `lastLogin` updated. Replay: same token re-POSTed → 401 at `verify.post.js:53` (jti-burn branch), Mongo state unchanged. Expiry: `jwt.sign({...},{expiresIn:'-1s'})` with fresh unburned jti on the member → 401 at `verify.post.js:22` (jwt.verify catch, before jti check), no mutation.)* -- [x] **Guest event signup** — four branches: new email + consent, new email without consent, existing guest, existing active member. Confirms cookie only sets for new/guest, and confirmation email appends `/login` link for real members. *(Verified 2026-04-19 via tunnel with throwaway event + timestamped test emails; cleanup done.)* -- [x] **Mobile responsive layout** — main chrome sidebar hides ≤768px (not ≤1024px as previously noted); in-page two-column layouts collapse at ≤1024px. Mobile header/drawer works on phone widths. *(Verified 2026-04-19.)* -- [x] **`--text-dim` / `--text-faint` WCAG AA contrast check.** *(Verified 2026-04-19; only live failure was `.circle-desc` on selected/hover tiles — fixed in `e7ad076` by promoting from `--text-faint` to `--text-dim`.)* -- [x] **In-app payment history** (`/member/account` → Past payments section). Verified: active monthly subscriber sees past charges with dates/amounts; annual subscriber sees their single upfront charge; member with no payments shows empty state; cancelled member still sees historical charges. **Per-row download/view link NOT implemented** — Helcim's `/card-transactions/` API doesn't expose per-transaction receipt URLs. Accepted as satisfied by the existing "Advanced billing in Helcim →" escape hatch, which lands members in the Helcim portal where receipts are downloadable. *(Verified 2026-04-19.)* -- [x] **In-app change card** (`/member/account` → Change card). Verify: HelcimPay.js modal opens, new card tokenizes, customer's default payment method updates in Helcim dashboard, active subscription's payment method updates, and `billing_card_updated` activity log entry is written. Force a failure path (e.g. invalid card or Helcim 4xx after default updates) to confirm the rollback in `server/api/helcim/update-card.post.js` actually restores the prior default. *(Verified 2026-04-19.)* +**Shared setup (do once):** +- `npx nuxi dev --https` in one terminal, `cloudflared tunnel --url https://localhost:3000` (or `ngrok http https://localhost:3000`) in another. Use the tunnel URL as `BASE_URL` in `.env`. +- Helcim sandbox test card: see `~/.claude/projects/-Users-jennie-Sites-ghostguild-org/memory/reference_helcim_sandbox.md`. +- Apply the contribution-amount migration against local Mongo first so seeded members match the new schema: + ``` + node scripts/migrate-contribution-amount.cjs # dry-run + node scripts/migrate-contribution-amount.cjs --apply # apply + ``` + After applying, confirm in mongosh: `db.members.countDocuments({ contributionAmount: { $exists: true } })` should equal total member count; `db.members.countDocuments({ contributionAmount: { $type: 'string' } })` must be `0`. + +--- + +- [ ] **Pre-registrant invite → accept flow with a paid contribution amount.** Exercises Helcim customer creation during acceptance. Un-deferred 2026-04-20 — the contribution-amount refactor that was expected to replace this flow has landed on `main`, so the flow is in its final shape. + + **Setup:** In admin UI or mongosh, pick a `PreRegistration` entry (or insert one with a throwaway email). From `/admin/pre-registrants`, send an invite. In a second browser/incognito, open the invite email and click through to `/accept-invite?token=...`. + + **Test — run twice:** + 1. Monthly cadence, non-preset amount (e.g. `$7`). + 2. Annual cadence, a preset amount (e.g. `$15`, expected Helcim `recurringAmount: 180`). + + **Expect:** + - `Member` doc created with `contributionAmount: 7` (Number, not String), correct `billingCadence`, `helcimCustomerId` populated, `status: 'active'` (or `pending_payment` if B1 hasn't been implemented yet — either is acceptable here, the point is a clean create). + - Helcim customer exists and has a subscription with `recurringAmount` = amount (Monthly) or amount × 12 (Annual). + - No `contributionTier` String field on the new Member doc. + - Welcome email delivered via Resend. + - Auto-login succeeds and lands on `/member/dashboard`. + + **Key files if debugging:** `server/api/invite/accept.post.js`, `app/pages/accept-invite.vue`, `server/api/helcim/customer.post.js`. + +- [ ] **Contribution-amount redesign end-to-end.** Covers the full surface of the `contributionTier` → `contributionAmount` rename. + + **Signup flows — `/join`:** + 1. `$0` Monthly — should create Member with no Helcim subscription, `contributionAmount: 0`. + 2. `$5` Monthly (preset) — Helcim subscription `recurringAmount: 5`. + 3. `$17` Monthly (non-preset, between $15 and $30 chips) — Helcim subscription `recurringAmount: 17`, UI shows the `$15` chip's label via `findLast`. + 4. `$17` Annual — Helcim subscription `recurringAmount: 204`, `billingCadence: 'annual'`, Mongo `contributionAmount: 17` (stores monthly-equivalent). + 5. `$50` Annual (top preset) — Helcim subscription `recurringAmount: 600`. + + **Edit flows — `/member/account` as an active paid member:** + - Raise amount ($17 → $30). Confirm `updateHelcimSubscription` called with `recurringAmount: 30` (Monthly) or `360` (Annual). + - Lower amount ($30 → $5). Same assertion at the new values. + - Switch cadence (Monthly $17 ↔ Annual $17). Confirm `billingCadence` updated and `recurringAmount` re-derived. + + **Admin flow — `/admin/members/[id]` edit:** + - `contributionAmount` input accepts any non-negative whole dollar. Save writes Number to Mongo. + - No chip UI here (admin is plain number input by design). + + **Assert across all flows:** + - Mongo `contributionAmount` is always `Number`, never `String`. + - No `contributionTier` values written anywhere (greppable: `db.members.findOne({}, { contributionTier: 1 })` should return whatever the migration left; no *new* writes to that field). + - No "save $X", "2 months free", or discount copy appears in any UI surface. Annual is just `amount × 12` exactly. + - Guidance chip labels (`$0`/`$5`/`$15`/`$30`/`$50`) are matched via `findLast`, so $17 lands on the `$15` label, $49 lands on `$30`, $51 lands on `$50`. + + **Key files if debugging:** `app/pages/join.vue`, `app/pages/member/account.vue`, `app/pages/admin/members/[id].vue`, `server/api/helcim/subscription.post.js`, `server/api/members/update-contribution.post.js`, `server/api/admin/members/[id].put.js`, `app/config/contributions.js` + `server/config/contributions.js`. + + **Cosmetic follow-ups noted in Post-launch backlog below** — won't block this test (they're naming, not behavior). --- @@ -92,7 +130,7 @@ Not blocking launch — the amendment hasn't passed yet, and the user-visible co ### B1. `cancel-subscription` flips status to `pending_payment` - `server/api/members/cancel-subscription.post.js:31,48` -- When a member cancels their paid subscription, status is set to `pending_payment` and tier to `'0'`. Under the new model, cancelling a payment plan moves the member to the $0 tier — status should stay `active`. +- When a member cancels their paid subscription, status is set to `pending_payment` and contribution amount to `0`. Under the new model, cancelling a payment plan moves the member to the $0 contribution — status should stay `active`. - **Fix:** change `status: 'pending_payment'` → `status: 'active'` in both the `findByIdAndUpdate` payload (line 31) and the response (line 48). Comment at line 26 also needs updating ("(not cancelled) so member can re-subscribe" → reflect new framing). - Add coverage in `tests/server/api/cancel-subscription.test.js` if it doesn't already exist. @@ -117,7 +155,7 @@ See `docs/TODO.md` for: - OWASP ASVS L1 Phase 4 (file-upload validation pipeline, granular RBAC, credential encryption). - `tickets/available.get.js:115` `memberSavings` block reports `$0 saved` for inactive members — cosmetic; suppress comparison block when `!hasMemberAccess(member)` if it ever surfaces in UI. -### Contribution-amount redesign — cosmetic cleanup (cosmetic, ship post-launch if not caught in the PR) +### Contribution-amount redesign — cosmetic cleanup (naming only, not behavior) - Rename admin members column header "Tier" → "Contribution" (`app/pages/admin/members/index.vue:265`). - Delete dead `app/components/TierPicker.vue`. - Update stale tier comment in `app/composables/useMemberPayment.js:59`.