ghostguild-org/docs/LAUNCH_READINESS.md
Jennie Robinson Faber 1fbe9c3227
Some checks failed
Test / playwright (push) Blocked by required conditions
Test / Notify on failure (push) Blocked by required conditions
Test / visual (push) Blocked by required conditions
Test / vitest (push) Has been cancelled
docs(launch): mark receipts Phase 1 complete, add branch-merge checkbox
Flips the P1 section from 'none of it is implemented' to a shipped/remaining
breakdown citing commits bf5a333..91711aa, and adds a one-line current-state
bullet pointing at the unmerged feature branch.
2026-04-20 13:51:20 +01:00

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

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

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