164 lines
14 KiB
Markdown
164 lines
14 KiB
Markdown
# Launch Readiness
|
|
|
|
**Status as of 2026-04-20.** Target launch: before 2026-05-01.
|
|
|
|
Single source of truth for work remaining before cutover. P0 blocks launch; P1 is strongly preferred but survivable. Completed items are 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 snapshot 2026-04-25 ~18:23 local: **703 passing / 8 failing / 2 skipped (713 total)**. The previously-flagged 6 helcim-payment failures are now green. The 8 current failures are in `tests/server/api/auth-verify.test.js` and `tests/server/api/cancel-subscription.smoke.test.js`, both belonging to in-flight Phase 5 fixes (#10 and #9) being landed by parallel impl subagents — they will resolve as those branches merge.
|
|
- All launch code is on local `main`: Helcim plan consolidation, contribution-amount redesign, cadence UX unification, and receipts Phase 1. 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.
|
|
- Receipts Phase 1 code is shipped; remaining work is deploy-time only (see Deploy checklist).
|
|
|
|
---
|
|
|
|
## P0 — Must fix before launch
|
|
|
|
None outstanding.
|
|
|
|
---
|
|
|
|
## P1 — Strongly preferred before launch
|
|
|
|
None outstanding.
|
|
|
|
---
|
|
|
|
## Deploy checklist
|
|
|
|
Applies when the app is deployed to **Dokploy on Hetzner**. Build is via the in-repo `Dockerfile` (`node:20-alpine`, runs `node .output/server/index.mjs` on port 3000); Dokploy autodetects it. Traefik (Dokploy's reverse proxy) handles SSL; `oidc-provider.ts:194` and the rate-limit middleware already trust `X-Forwarded-Proto` / `X-Forwarded-For`.
|
|
|
|
### One-time host setup
|
|
|
|
- [ ] **Provision the Dokploy app** pointing at this repo. Build context: repo root. Default Dockerfile. Container port: `3000`.
|
|
- [ ] **Set env vars in the Dokploy UI** (full list below). The `validate-env.js` Nitro plugin fails fast at boot if `MONGODB_URI` / `JWT_SECRET` / `RESEND_API_KEY` / `HELCIM_API_TOKEN` are missing — container refuses to start, so misconfig surfaces immediately in logs.
|
|
- [ ] **`BASE_URL` must exactly match the public origin** (e.g. `https://ghostguild.org`, no trailing slash). The `/api/helcim/customer` origin check at `server/api/helcim/customer.post.js:11-15` does exact-match comparison against the `Origin` header — if `BASE_URL` is wrong or unset, signup 403s.
|
|
- [ ] **`NODE_ENV=production`** must be set. Without it: `Secure` cookie flag, HSTS, and CSP all silently no-op.
|
|
- [ ] **Add a Dokploy Scheduled Task** for daily reconciliation. Command:
|
|
```
|
|
curl -fsS -X POST "$BASE_URL/api/internal/reconcile-payments" -H "X-Reconcile-Token: $NUXT_RECONCILE_TOKEN"
|
|
```
|
|
Schedule: `0 4 * * *` (or any time of day). The Nitro route does the heavy lifting (Mongo iteration, Helcim API, retries) — the scheduler just wakes it up.
|
|
|
|
### 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 Dokploy env.
|
|
- [ ] Set `NUXT_HELCIM_ANNUAL_PLAN_ID=50303` in Dokploy env.
|
|
- [ ] **Set `NUXT_RECONCILE_TOKEN`** to any 32+ char random string. Shared secret between the Dokploy scheduled task and `/api/internal/reconcile-payments`.
|
|
- [ ] Deploy.
|
|
- [ ] **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`); the daily Dokploy cron picks it up from there.
|
|
- [ ] **Prod audit for pre-fix series-pass bypass registrations.** Fixed in `f34b062` + `4e1888a` (2026-04-20). Before that, child events of pass-only series (`tickets.requiresSeriesTicket=true && tickets.allowIndividualEventTickets=false`) accepted drop-in registrations from non-pass-holders. For every such series, list its child-event `registrations` where the registrant is not in the parent series' pass-holder list, filter to `registeredAt < 2026-04-20`, and decide per-case: grandfather (keep + notify), refund + unregister, or silently unregister. Local Mongo was scrubbed of 2 such rows on 2026-04-20; prod was intentionally untouched.
|
|
- [ ] **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 against the deployed app** 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).
|
|
- [ ] **Rotate HELCIM_API_TOKEN** in the Helcim merchant portal and update the Dokploy env var. The token was previously exposed in `window.__NUXT__` payload until commit `208638e`.
|
|
- [ ] **Trigger the daily reconcile task once manually** in Dokploy to confirm scheduled task + token are wired correctly. Expect a `[reconcile] done {...}` log line.
|
|
|
|
**Env vars required in Dokploy (reference):**
|
|
- `NODE_ENV=production`
|
|
- `BASE_URL` (exact public origin, no trailing slash)
|
|
- `MONGODB_URI`
|
|
- `JWT_SECRET` (or `NUXT_JWT_SECRET` — the `NUXT_` variant wins)
|
|
- `RESEND_API_KEY`
|
|
- `HELCIM_API_TOKEN`
|
|
- `NUXT_HELCIM_MONTHLY_PLAN_ID=50302`
|
|
- `NUXT_HELCIM_ANNUAL_PLAN_ID=50303`
|
|
- `NUXT_PUBLIC_HELCIM_PORTAL_URL`
|
|
- `NUXT_RECONCILE_TOKEN` (32+ char random string)
|
|
- `SLACK_BOT_TOKEN`
|
|
- `OIDC_COOKIE_SECRET`
|
|
|
|
---
|
|
|
|
## Fixed 2026-04-25
|
|
|
|
Day-of-launch security and correctness audit. All commit shas TBD until Phase 5.
|
|
|
|
### CRITICAL (security)
|
|
- **Fix #1** — `HELCIM_API_TOKEN` removed from public runtime config + dead `useHelcim.js` deleted. **Token must be rotated post-deploy** (was previously exposed via `window.__NUXT__`).
|
|
- **Fix #2** — `/api/helcim/customer` gated with origin check + per-IP/email rate limit + magic-link email verification (replaces unauthenticated `setAuthCookie`).
|
|
- **Fix #3** — `/api/events/[id]/payment` deleted (dead code with auth bypass). `processHelcimPayment` stub + `eventPaymentSchema` removed.
|
|
- **Fix #4** — `/api/helcim/initialize-payment` re-derives ticket amount server-side via `calculateTicketPrice`; new `series_ticket` metadata type.
|
|
- **Fix #5** — `/api/helcim/customer` upgrades existing `status:guest` members in place rather than rejecting with 409.
|
|
|
|
### HIGH (correctness)
|
|
- **Fix #6** — Recurring reconciliation: Netlify scheduled function calls `/api/internal/reconcile-payments` daily. Requires `NUXT_RECONCILE_TOKEN` env var.
|
|
- **Fix #7** — `validateBeforeSave: false` added to event subdoc saves (waitlist endpoints) to dodge legacy location validators.
|
|
- **Fix #8** — Series-pass purchase always creates a guest Member when caller is unauthenticated, mirroring event-ticket flow.
|
|
- **Fix #9** — `cancel-subscription` leaves status `active` (per ratified bylaws); adds `lastCancelledAt` audit field.
|
|
- **Fix #10** — `/api/auth/verify` uses `validateBody` with `.strict()` Zod schema.
|
|
- **Fix #11** — Added 8 vitest cases for `cancel-subscription.post.js` (was uncovered).
|
|
|
|
### Side-quests
|
|
- Visual audit Phase 4 changes (events/series surface)
|
|
- Per-fix branch verification: see `docs/superpowers/specs/2026-04-25-fix-*.md`
|
|
|
|
---
|
|
|
|
## Manual browser tests still needed
|
|
|
|
None outstanding. All launch-blocking flows verified via local dev or cloudflared tunnel with real Helcim test card + real email (see archive for the full log). The one remaining browser verification is the staging test charge bundled into the Deploy checklist above.
|
|
|
|
---
|
|
|
|
## 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~~ — fixed 2026-04-29 (commit `23154ff`); root cause was `oidc-provider`'s `devInteractions` overriding our custom `interactions.url`.
|
|
- ~~Admin layout migration from `guild-*` tokens to zine spec~~ — verified clean 2026-04-29; grep for `guild-[0-9]|candlelight-[0-9]|ember-[0-9]` across `app/layouts/`, `app/pages/admin/`, `app/components/admin/` returns zero matches. All tokens already converted.
|
|
- ~~Admin dashboard quick-action button contrast~~ — verified stale 2026-04-29.
|
|
- ~~Members table NAME column clipping~~ — verified stale 2026-04-29.
|
|
- 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~~ — fixed 2026-04-29 (commit `f66455e`); `memberSavings` now gated on `hasMemberAccess(member)`.
|
|
- Simplify-pass follow-ups (2026-04-25): SHIPPED 2026-04-27 on branch `chore/simplify-pass-follow-ups` (pending merge). See `~/.claude/projects/-Users-jennie-Sites-ghostguild-org/memory/project_simplify_pass_2026_04_25.md`.
|
|
- ~~Reconcile `customerCode` bug~~ — fixed on `main` in commit `3c38333` ("pass customerCode (not helcimCustomerId) to Helcim transactions API"). Verified in `server/api/internal/reconcile-payments.post.js:97`.
|
|
- ~~Drive-by from 2026-04-29 phantom-Tailwind sweep: `app/components/EventSeriesBadge.vue` has zero usages~~ — deleted 2026-04-29 (commit `f85f284`); 81 lines removed.
|
|
|
|
### Known gotchas worth addressing post-launch
|
|
|
|
- **Admin edit does not sync Helcim `recurringAmount`.** `/admin/members/[id]` PUT writes `contributionAmount` direct to Mongo by design. Admins must PATCH Helcim manually. Worth surfacing in admin UI or docs.
|
|
- **Cadence switch rejected on active subscriptions.** `update-contribution.post.js:184-189` refuses cadence changes mid-subscription; no UI toggle exists on `/member/account`. Adding cadence switch would require a Helcim subscription replacement flow, not a plain update.
|
|
- **S2 test fixture `id`/`slug` inconsistency.** (Local dev only.) Seeded S2 series has `id: 'test-s2-drop-in-allowed'` but `slug: 'test-s2-drop-in-allowed-series'`. Doesn't affect prod — fix the seed script if anyone re-runs fixtures and is confused why `id`-based Mongo queries return empty.
|
|
|
|
### Events-surface visual audit — deferred items (2026-04-21)
|
|
|
|
Context: Phase 4 audit against `docs/specs/events-visual-audit-findings.md` fixed all critical phantom-palette, rounded-corner, CTA-mismatch, and input-styling issues across `EventTicketCard`, `EventTicketPurchase`, `EventSeriesTicketCard`, `SeriesPassPurchase`. Items below were explicitly deferred or out of reach.
|
|
|
|
- ~~**Success-state color convention (4 instances).**~~ Resolved 2026-04-29: gold (`--candle`) chosen as zine-consistent. Phantom-Tailwind cleanup shipped in `dc2becf` (`EventSeriesTicketCard.vue` + `SeriesPassPurchase.vue` member-benefit notice).
|
|
- ~~**Sidebar breakpoint unverified.**~~ Verified clean 2026-04-29 — `.events-mini` hides at ≤1024px cleanly across 1023/1024/1025/1100. Actual rule lives in `EventsMiniSidebar.vue:129` + `ColumnsLayout.vue:83` (audit doc cited the wrong line).
|
|
- ~~**`EventTicketPurchase.vue:469` magic padding.**~~ Fixed 2026-04-29 (commit `7e44809`); consent block now uses a grid approach.
|
|
- ~~**`.section-label` extraction candidate.**~~ Verified 2026-04-29 — utility already exists at `main.css:128` and is used in 30+ places. Two scoped overrides intentionally diverge.
|
|
- ~~**Past-events toggle component.**~~ Audited 2026-04-29 — consistent with the design system (dashed-border button, gold active state, valid `aria-pressed` toggle). Added missing `:focus-visible` outline in commit `dadec1a`; no other changes warranted.
|
|
|
|
### Contribution-amount redesign — cosmetic cleanup (naming only, not behavior)
|
|
|
|
SHIPPED 2026-04-29 in commit `955217a` (admin column header, dropdown labels, handler rename, log message).
|
|
|