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.
17 KiB
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 intests/server/api/helcim-payment.test.js— unrelated to launch-blocking work, noted in the deploy checklist for visibility. mainis now caught up locally (2026-04-20):feature/helcim-plan-consolidation(40 commits) andfeature/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 id50303). 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(commitsbf5a333..91711aa, 2026-04-20). Unmerged. All four spec items shipped:Paymentmodel + idempotentupsertPaymentFromHelcimhelper, synchronous payment logging on both new paid subscriptions and free→paid upgrades, nightly reconciliation script,/joincharity note, andtaxReceiptPreferencesschema 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
/joinprogress overlay; now used by both/joinand/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;/joinstays 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=1renders "Welcome to Ghost Guild, {name}" instead of "Welcome back, {name}"./welcomeredirect now always carries the param;/accept-invitenavigates 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 nowcontributionAmount > 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--candleborder). Separate from the existing compact "Next payment" row in the Membership Card summary. - Fixed
subscription.get.jsHelcim field mapping. Helcim's GET/subscriptions/:idreturnsdataas a single object (not array) with the fielddateBilling(notnextBillingDate). 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 cachednextBillingDatewas missing. Note:subscription.post.jsandupdate-contribution.post.jsstill readsubscription.nextBillingDatefrom 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
Paymentmodel (server/models/payment.js) + idempotentupsertPaymentFromHelcimhelper keyed on uniquehelcimTransactionId(server/utils/payments.js). Synchronous write paths:- New paid subscription →
server/api/helcim/subscription.post.jsfetches the newest paid Helcim tx and upserts a Payment withpaymentTypefrom 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.
- New paid subscription →
- Confirmation email via Resend, not Helcim. Spec alternative (b) chosen.
server/emails/paymentConfirmation.jsis 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
/joinonly (app/pages/join.vue:83)./accept-inviteand/member/accountintentionally untouched per spec §3. - Member schema field.
taxReceiptPreferencesnested object added toserver/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.mjsiterates every Member withhelcimCustomerId, pulls recent Helcim transactions, and upserts via the same helper. Idempotent. Dry-run by default;--applyto 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-1intomain. - 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
maintoorigin/main. - Run
node scripts/migrate-contribution-amount.cjs --applyagainst prod Mongo BEFORE the new code serves traffic. Idempotent; dry-run on local counted 34 members. RequiresMONGODB_URIin env. The script writescontributionAmount(Number) derived from existingcontributionTier(String) on every Member doc; the old field is left intact for a window. - Set
NUXT_HELCIM_MONTHLY_PLAN_ID=50302in production env. - Set
NUXT_HELCIM_ANNUAL_PLAN_ID=50303in 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 --applyagainst prod Mongo AFTER the new code serves traffic to backfill Payment records for pre-existing members. Idempotent (uniquehelcimTransactionId); 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 fromupsertPaymentFromHelcim; 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_URIJWT_SECRET(orNUXT_JWT_SECRET— theNUXT_variant wins)RESEND_API_KEYHELCIM_API_TOKENNUXT_HELCIM_MONTHLY_PLAN_IDNUXT_HELCIM_ANNUAL_PLAN_IDSLACK_BOT_TOKENBASE_URLOIDC_COOKIE_SECRETNUXT_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 --httpsin one terminal,cloudflared tunnel --url https://localhost:3000(orngrok http https://localhost:3000) in another. Use the tunnel URL asBASE_URLin.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:
After applying, confirm in mongosh:node scripts/migrate-contribution-amount.cjs # dry-run node scripts/migrate-contribution-amount.cjs --apply # applydb.members.countDocuments({ contributionAmount: { $exists: true } })should equal total member count;db.members.countDocuments({ contributionAmount: { $type: 'string' } })must be0.
-
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:
contributionAmountstored as Number,billingCadencecorrect,helcimCustomerId+helcimSubscriptionIdpopulated,status: active, nocontributionTierfield, preReg transitioned toacceptedwithmemberIdset. -
Contribution-amount redesign end-to-end. Covers the full surface of the
contributionTier→contributionAmountrename.-
Signup flows —
/join: ✅ Passed 2026-04-20. All 5 variants ran functionally clean (welcome-heading regression was caught, fixed via?welcome=1propagation through/welcome, not retested — trusted):$0Monthly — Member created with no Helcim subscription.$5Monthly (preset) — HelcimrecurringAmount: 5.$17Monthly (non-preset) — HelcimrecurringAmount: 17,$15chip label viafindLast.$17Annual — HelcimrecurringAmount: 204,billingCadence: 'annual', Mongo stores monthly-equivalent17.$50Annual (top preset) — HelcimrecurringAmount: 600.
-
Edit flows —
/member/accountas an active paid member: ✅ Passed 2026-04-20 against Cleo's Annual subscription (Helcim sub 138682).- Raise $15 → $30 annual:
updateHelcimSubscriptionhit withrecurringAmount: 360, MongocontributionAmount: 30(Number). - Lower $30 → $5 annual:
recurringAmount: 60, MongocontributionAmount: 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).
- Raise $15 → $30 annual:
-
Admin flow —
/admin/members/[id]edit: ✅ Passed 2026-04-20.- Changed Cleo $5 → $15 via admin PUT. Mongo wrote
contributionAmount: 15(Number).contributionTierfield 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.
- Changed Cleo $5 → $15 via admin PUT. Mongo wrote
Assert across all flows:
- Mongo
contributionAmountis alwaysNumber, neverString. - No
contributionTiervalues 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 × 12exactly. - Guidance chip labels (
$0/$5/$15/$30/$50) are matched viafindLast, so $17 lands on the$15label, $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_paymentand contribution amount to0. Under the new model, cancelling a payment plan moves the member to the $0 contribution — status should stayactive. - Fix: change
status: 'pending_payment'→status: 'active'in both thefindByIdAndUpdatepayload (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.jsif it doesn't already exist.
B3. Vestigial pending_payment status
- Once payment is fully decoupled,
pending_paymentno longer gates anything and is functionally equivalent toactive. Consider removing it from the enum (server/models/member.js:38,server/utils/schemas.js:299) and treating new signups asactivefrom 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 existingpending_paymentrows toactive. - 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:69showpending_paymentas "Pending Payment". If B3 removes the status entirely, this disappears too. If we keeppending_paymentfor 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:115memberSavingsblock reports$0 savedfor 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
handleUpdateTierhandler inapp/pages/member/account.vue.