# Launch Readiness **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`. --- ## Current state - 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.** - Cadence/contribution UX unified across signup + edit surfaces 2026-04-20. Uncommitted in working tree — see "Cadence UX refinements" below. - **Charitable receipts Phase 1 built on `feature/receipts-phase-1` (commits `bf5a333..91711aa`, 2026-04-20). Unmerged.** All four spec items shipped: `Payment` model + idempotent `upsertPaymentFromHelcim` helper, synchronous payment logging on both new paid subscriptions and free→paid upgrades, nightly reconciliation script, `/join` charity note, and `taxReceiptPreferences` schema field (no UI — Phase 2). Resend-owned confirmation email (`server/emails/paymentConfirmation.js`) is CRA-safe. Remaining work is deploy-time only (merge branch, disable Helcim default email on plans 50302 + 50303, backfill, real staging charge) — tracked in Deploy checklist. ### Cadence UX refinements (2026-04-20, uncommitted) Shipped across `accept-invite.vue`, `join.vue`, `member/account.vue`, `welcome.vue`, `member/dashboard.vue`, and a new shared `SignupFlowOverlay.vue`: - **Shared SignupFlowOverlay component.** Extracted from `/join` progress overlay; now used by both `/join` and `/accept-invite`. - **Static "Monthly Contribution" label** on all three contribution inputs (previously dynamic — flipped to "Annual Contribution" when annual cadence was selected, which was misleading because the stored value is always the monthly base). - **"Per Year" / "Per Month"** toggle copy (was "Annual" / "Monthly"). On `/accept-invite`, Per Year is now the default; `/join` stays on Per Month by default. - **Live billing-summary card** below the contribution input on both signup flows — reads e.g. "You'll be charged $180 today ($15/month × 12). Then $180 every year, until you cancel." - **Welcome heading on dashboard** for new signups: `/member/dashboard?welcome=1` renders "Welcome to Ghost Guild, {name}" instead of "Welcome back, {name}". `/welcome` redirect now always carries the param; `/accept-invite` navigates to the dashboard with the param directly. - **$0 member polish on `/member/account`**: Payment History section hidden for $0 members with no prior charges (condition now `contributionAmount > 0 || paymentHistory.length > 0` — fixes a regression where paid-then-$0 members lost visibility of their past payments). Solidarity-Fund sentence in the Danger Zone also hidden at $0. - **Next charge row above payment history** on `/member/account`: When a member has an upcoming charge, a "Next charge: $X on DATE" row renders above the transaction list (dashed `--candle` border). Separate from the existing compact "Next payment" row in the Membership Card summary. - **Fixed `subscription.get.js` Helcim field mapping.** Helcim's GET `/subscriptions/:id` returns `data` as a single object (not array) with the field `dateBilling` (not `nextBillingDate`). The lazy refresh endpoint now handles both shapes — previously it returned empty strings, so neither the Membership-card "Next payment" nor the new "Next charge" row rendered for any member whose cached `nextBillingDate` was missing. Note: `subscription.post.js` and `update-contribution.post.js` still read `subscription.nextBillingDate` from Helcim's CREATE response (same wrong field), which is why the cache was empty to begin with. Left unfixed in this pass — the lazy GET refresh now masks it. Worth cleaning up post-launch. - **State-aware contribution-change hint** on `/member/account`: "You'll be charged $X today to start your subscription." ($0 → paid) / "Your paid subscription will be cancelled." (paid → $0) / "Changes apply on your next billing cycle." (paid → paid, different amount). - **Server-side invite accept** now creates the Helcim customer and sets the auth cookie before returning, for both free and paid branches. --- ## P0 — Must fix before launch None outstanding. --- ## P1 — Strongly preferred before launch ### Charitable receipts — Phase 1 ✅ COMPLETE (`docs/specs/receipts-launch-spec.md`) Built on `feature/receipts-phase-1`, commits `bf5a333..91711aa` (2026-04-20). **Unmerged.** All four spec items shipped; remaining work is deploy-time only (tracked in Deploy checklist). Shipped: - **Payment logging.** New `Payment` model (`server/models/payment.js`) + idempotent `upsertPaymentFromHelcim` helper keyed on unique `helcimTransactionId` (`server/utils/payments.js`). Synchronous write paths: - New paid subscription → `server/api/helcim/subscription.post.js` fetches the newest paid Helcim tx and upserts a Payment with `paymentType` from cadence + `sendConfirmation: true`. Wrapped in try/catch so a logging failure cannot break subscription creation. - Free → paid upgrade → `server/api/members/update-contribution.post.js` (Case 1 branch) does the same. - Paid → paid amount change (Case 3) is intentionally **not** wired synchronously — no new tx at the moment of change; the next recurring charge is captured by the reconciliation script. - **Confirmation email via Resend, not Helcim.** Spec alternative (b) chosen. `server/emails/paymentConfirmation.js` is CRA-safe: charity name "Baby Ghosts Studio Development Fund" + "not an official donation receipt / tax receipts available later in 2026" disclaimer. Triggered only on new Payment inserts; send failures are swallowed. Helcim's default confirmation must be disabled on plans 50302 + 50303 at cutover (Deploy checklist). - **Join page copy.** Factual charity note below contribution tiers on `/join` only (`app/pages/join.vue:83`). `/accept-invite` and `/member/account` intentionally untouched per spec §3. - **Member schema field.** `taxReceiptPreferences` nested object added to `server/models/member.js` (filesCanadianTaxes, middleInitial, confirmedAddress sub-object, setupCompletedAt). Defaults null/false — existing members read as "not set up." Schema-only; no Zod, no route, no UI. Phase 2 binds to it without migration. - **Reconciliation script.** `scripts/reconcile-helcim-payments.mjs` iterates every Member with `helcimCustomerId`, pulls recent Helcim transactions, and upserts via the same helper. Idempotent. Dry-run by default; `--apply` to write. No confirmation emails sent during reconcile. Dual purpose: launch-day backfill for the ~34 pre-existing members, and nightly cron post-launch to catch recurring charges that bypass the synchronous write paths. Remaining (deploy-time, not code): - [ ] Merge `feature/receipts-phase-1` into `main`. - Manual Helcim-dashboard step + prod reconcile + staging test charge — see Deploy checklist. --- ## Deploy checklist 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. - [ ] 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. - [ ] **Run `node scripts/reconcile-helcim-payments.mjs --apply` against prod Mongo AFTER the new code serves traffic** to backfill Payment records for pre-existing members. Idempotent (unique `helcimTransactionId`); safe to re-run as a nightly reconciliation job post-launch. - [ ] **Helcim dashboard: disable the default payment-confirmation email for plans 50302 + 50303.** We send our own CRA-safe confirmation via Resend (`server/emails/paymentConfirmation.js`) triggered from `upsertPaymentFromHelcim`; leaving Helcim's default on = duplicate emails. - [ ] **Run one real test charge on staging** via the cloudflared tunnel and verify (a) a Payment doc in Mongo with `amount`, `paymentType`, `status: 'success'`, and (b) exactly one CRA-compliant confirmation email (charity name + "not an official donation receipt" disclaimer; no banned assertive phrasing). **Env vars required in production (reference):** - `MONGODB_URI` - `JWT_SECRET` (or `NUXT_JWT_SECRET` — the `NUXT_` variant wins) - `RESEND_API_KEY` - `HELCIM_API_TOKEN` - `NUXT_HELCIM_MONTHLY_PLAN_ID` - `NUXT_HELCIM_ANNUAL_PLAN_ID` - `SLACK_BOT_TOKEN` - `BASE_URL` - `OIDC_COOKIE_SECRET` - `NUXT_PUBLIC_HELCIM_PORTAL_URL` --- ## Manual browser tests still needed 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). **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`. --- - [x] **Pre-registrant invite → accept flow with a paid contribution amount.** ✅ Passed 2026-04-20 — both Monthly $7 and Annual $15 variants completed end-to-end. DB verified programmatically: `contributionAmount` stored as Number, `billingCadence` correct, `helcimCustomerId` + `helcimSubscriptionId` populated, `status: active`, no `contributionTier` field, preReg transitioned to `accepted` with `memberId` set. - **Contribution-amount redesign end-to-end.** Covers the full surface of the `contributionTier` → `contributionAmount` rename. - [x] **Signup flows — `/join`:** ✅ Passed 2026-04-20. All 5 variants ran functionally clean (welcome-heading regression was caught, fixed via `?welcome=1` propagation through `/welcome`, not retested — trusted): 1. `$0` Monthly — Member created with no Helcim subscription. 2. `$5` Monthly (preset) — Helcim `recurringAmount: 5`. 3. `$17` Monthly (non-preset) — Helcim `recurringAmount: 17`, `$15` chip label via `findLast`. 4. `$17` Annual — Helcim `recurringAmount: 204`, `billingCadence: 'annual'`, Mongo stores monthly-equivalent `17`. 5. `$50` Annual (top preset) — Helcim `recurringAmount: 600`. - [x] **Edit flows — `/member/account` as an active paid member:** ✅ Passed 2026-04-20 against Cleo's Annual subscription (Helcim sub 138682). - Raise $15 → $30 annual: `updateHelcimSubscription` hit with `recurringAmount: 360`, Mongo `contributionAmount: 30` (Number). - Lower $30 → $5 annual: `recurringAmount: 60`, Mongo `contributionAmount: 5` (Number). - ~~Switch cadence (Monthly $17 ↔ Annual $17).~~ **Deferred from launch.** Server (`update-contribution.post.js:184-189`) explicitly rejects cadence changes on existing subscriptions; no UI toggle exists on `/member/account`. Re-scope post-launch if/when we want to support cadence switch (would need Helcim subscription replacement flow, not a plain update). - [x] **Admin flow — `/admin/members/[id]` edit:** ✅ Passed 2026-04-20. - Changed Cleo $5 → $15 via admin PUT. Mongo wrote `contributionAmount: 15` (Number). `contributionTier` field absent across all 34 members (`countDocuments({ contributionTier: { $exists: true } }) === 0`). - Known non-blocker: admin edit does not sync the change to Helcim's `recurringAmount`. Admin override is direct Mongo-only by design; had to PATCH Helcim manually to re-sync Cleo post-test. Worth noting in docs or surfacing in admin UI post-launch. **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). --- ## Bylaws decoupling — follow-ups (added 2026-04-18) Context: bylaws are being amended to remove automatic termination for nonpayment. Membership status will be fully decoupled from payment status; failed payments trigger committee outreach, not status change. Copy + UI access gates already aligned in `useMemberStatus.js` and `account.vue` (2026-04-18). Server-side status gating shipped as B2 (see archive). The behavioral changes below remain. Not blocking launch — the amendment hasn't passed yet, and the user-visible copy/UI is already consistent. Pick up once the amendment is ratified. ### 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 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. ### B3. Vestigial `pending_payment` status - Once payment is fully decoupled, `pending_payment` no longer gates anything and is functionally equivalent to `active`. Consider removing it from the enum (`server/models/member.js:38`, `server/utils/schemas.js:299`) and treating new signups as `active` from the moment of account creation. - Touches: signup flow (`helcim/customer.post.js:34`, `invite/accept.post.js:48`), admin filter UI (`app/pages/admin/members/index.vue:45,382,499,1145`, `[id].vue:69,286`), admin alerts (`server/utils/adminAlerts.js:22,100-116`, `server/models/adminAlertDismissal.js:6`), and a data migration to flip existing `pending_payment` rows to `active`. - Larger refactor — break out into its own ticket once B1 lands. ### B4. Admin "Pending Payment" filter label (cosmetic) - `app/pages/admin/members/index.vue:45,499`, `[id].vue:69` show `pending_payment` as "Pending Payment". If B3 removes the status entirely, this disappears too. If we keep `pending_payment` for now, rename in admin UI to "Payment setup incomplete" so admins also stop conflating it with membership state. --- ## Post-launch backlog See `docs/TODO.md` for: - Button minimum target size (WCAG AAA 2.5.5). - `/oidc/interaction/[uid]` routing quirk. - Admin layout migration from `guild-*` tokens to zine spec. - Admin dashboard quick-action button contrast. - Members table NAME column clipping. - 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 (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`. - Update error log message referencing "tier" in `server/api/members/update-contribution.post.js:221`. - Rename `handleUpdateTier` handler in `app/pages/member/account.vue`.