ghostguild-org/docs/LAUNCH_READINESS.md
Jennie Robinson Faber 493be2f3bc docs(launch): tidy post-merge state, expand remaining manual tests
Branch merges and 7/9 manual tests are done — moved to archive. Live
doc now only carries open work: charitable receipts Phase 1, prod
contribution-amount migration + Helcim plan env vars, and two manual
tests (pre-registrant invite, contribution-amount end-to-end). Both
remaining tests now include setup, test steps, assertions, and the
file references needed to complete them without additional context.
2026-04-20 09:02:40 +01:00

12 KiB
Raw Blame History

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.

P0 — Must fix before launch

None outstanding.


P1 — Strongly preferred before launch

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 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.


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.

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.

  • 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 contributionTiercontributionAmount 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).


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.