Captures the three post-Phase-1 deploy steps: run reconcile-helcim-payments.mjs against prod Mongo after the new code is serving, disable the default Helcim confirmation email for plans 50302 + 50303 (Branch B — we send our own via Resend), and run a real staging test charge to verify the Payment doc + single CRA-compliant confirmation email.
16 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.
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 (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. NoPaymentmodel exists today; needs new model + webhook wiring inserver/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 }toserver/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
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.