docs(handoff): capture Phase 2 (frontend cutover) code completion

Tasks 27–44 implemented across 18 commits on this branch. Phase O (Stripe
live setup, internal validation, flag flip) is the manual operational
follow-up and is the resume point.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-06 23:46:15 -04:00
parent c75ce0c9a3
commit fee4cb5b74
3 changed files with 62 additions and 19 deletions

View File

@@ -2,35 +2,65 @@
# HANDOFF.md
**Last updated:** 2026-05-06 (Phase 1 backend complete on `feat/self-serve-signup-spec`)
**Last updated:** 2026-05-06 (Phase 2 frontend-cutover code complete; cutover ops still pending)
**Active task:** Phase 1 self-serve signup backend foundation — DONE on branch. PR not yet opened.
**Active task:** None mid-flight. Branch `feat/self-serve-signup-phase-2` (HEAD `c75ce0c`) is ready to PR.
## Where this session ended
24 commits on top of `main` (`31ca3fb`). All 26 tasks from `docs/superpowers/plans/2026-05-06-self-serve-signup-phase-1-backend.md` complete. Full pytest run is green (1167 passed, 35 deselected). Single alembic head: `c6cbfc534fad`.
Tasks 2744 of Phase 2 implemented across 18 commits on `feat/self-serve-signup-phase-2`, branched off `main` (`f918b76`). Each task went through implementer + spec review + code-quality review per `superpowers:subagent-driven-development`; review findings either fixed in-flight (commit-amends, no merge churn) or noted as deliberate scope deferrals. Final cross-cutting review found one real bug (relative `/beta-signup` redirect target) — fixed in the last amend.
Phase 1 covered: schema additions (oauth_identities, plan_billing, sales_leads, stripe_events, plus 5 new columns across users/accounts/account_invites), Subscription complimentary status + has_pro_entitlement, the two new guards (`require_active_subscription`, `require_verified_email_after_grace`), full BillingService (start_trial / create_checkout_session / apply_subscription_event / get_billing_state), Stripe webhook handler, Google + Microsoft OAuth callbacks with oauth_identities linking, OAuth-only password guard, register-time verification email + invite email-match, bulk + soft-revoke invite routes, GET /billing/state, and the pilot complimentary backfill migration.
**Backend additions (Phase I, Tasks 2731):**
- `BillingService.open_customer_portal` + `GET /billing/portal-session` (allowlisted for canceled/unverified users).
- `PATCH /users/me/onboarding-step` + `POST /users/me/onboarding-dismiss-rest`.
- `POST /sales-leads` (public, 5/hr/IP rate limit, fire-and-forget notification email, server-side PostHog event stub).
- `/admin/plan-limits` GET/PUT round-trips `plan_billing` (Stripe IDs, prices, public/archived flags) in one transaction; `BillingService.invalidate_billing_cache` no-op stub for future cache.
- `GET /config/public` (`{self_serve_enabled, oauth_providers}`); register-endpoint gate now `REQUIRE_INVITE_CODE and not SELF_SERVE_ENABLED and not invite_code`.
- `GET /accounts/invites/{code}/lookup` (Phase 36).
- OAuth callback extended to honor `account_invite_code` + `invited_email` for invitee-via-OAuth path; rejects existing-email user with `email_already_registered_use_login`.
- `GET /plans/public` (Phase 42).
- `POST /beta-signup` returns 307 to `${FRONTEND_URL}/register?from=beta`.
- `OnboardingStatus` extended with `email_verified` + `shop_setup_done`; `UserResponse` exposes `onboarding_step_completed` + `onboarding_dismissed`.
The conftest's `test_user` fixture was modified to seed a Pro/active Subscription post-register (delete-then-insert) so the new subscription guard doesn't 402 every existing test. Two existing tests adapted because they explicitly assumed the old free-plan default: `test_subscription_limits.py` (the two free-plan tests now downgrade inline) and `test_kb_accelerator.py::TestQuota::test_get_quota` (the `kb_setup` fixture downgrades to free).
**Frontend additions:**
- `useBillingStore` (Zustand, polled 60s via `useBillingPoll` mounted in `AppLayout`), `useFeature` / `useFeatureLimit` / `useTrialBanner` hooks, `FeatureGate` / `UpgradePrompt` / `EmailVerificationGate` components.
- `RegisterPage` redesign: OAuth (Google/Microsoft) buttons + invite-code-conditional. `OAuthCallbackPage` at `/auth/{google,microsoft}/callback` with CSRF state validation. `useAppConfig` hook (consumes `/config/public`, falls back to `VITE_SELF_SERVE_ENABLED`).
- `AcceptInvitePage` at `/accept-invite?code=...` (locked email, 3 sign-in options, `/?welcome=teammate` on success).
- `EmailVerificationBanner` refactored to design-system tokens + grace-period hide; `EmailVerificationWall` polished; `VerifyEmailPage` at `/verify-email?token=...` (single-fire guard).
- `WelcomeRouter` + `WelcomeStep1/2/3` at `/welcome*`. PSA tile UI + bulk invite emails per row.
- `TrialPill` in topbar (pristine/warning/urgent/expired/paid/complimentary/past_due/canceled).
- `NextStepCard` + `SetupChecklist` (replaces orphaned `OnboardingChecklist`); unified list, no SOLO/TEAM split, no Script Builder item.
- `PricingPage` at `/pricing` (B-style, 3 plan cards, hardcoded comparison v1).
- `ContactSalesPage` at `/contact-sales`. `LandingPage` got "See pricing" CTA + replaced beta-signup form with `<Link to="/register?from=beta">`.
New env vars (Vite ARG/ENV in Dockerfile + `.env.example`): `VITE_SELF_SERVE_ENABLED`, `VITE_GOOGLE_CLIENT_ID`, `VITE_MS_CLIENT_ID`, `VITE_OAUTH_REDIRECT_BASE`, `VITE_CALENDLY_URL`. Backend env: `SALES_LEAD_RECIPIENT_EMAIL` (default `sales@resolutionflow.com`).
## Resume point — DO THIS NEXT
1. Open the PR for branch `feat/self-serve-signup-spec`. Use `gh pr create` against `main`. Suggested title: `feat: self-serve signup backend (Phase 1)`. Body should mention dark-launch posture (every new endpoint is gated by env config, not a feature flag — see Task 26 §3 in the plan).
2. Phase 2 (frontend + cutover) lives in a sibling plan: `docs/superpowers/plans/2026-05-06-self-serve-signup-phase-2-frontend.md` (assumed; verify path). It's the next logical task once Phase 1 ships.
1. **Review the branch and open a PR.** 18 commits, all squashed-or-not at user discretion. Branch `feat/self-serve-signup-phase-2` from `main`.
2. **Phase O — manual operational tasks** (Tasks 4547 from the plan):
- **Task 45:** Stripe live-mode setup (Products, Prices, Customer Portal, webhook endpoint + signing secret) and Railway prod env vars (`STRIPE_*`, `OAUTH_REDIRECT_BASE`, `GOOGLE_CLIENT_*`, `MS_CLIENT_*`). Run `python -m scripts.sync_stripe_plan_ids` to populate `plan_billing` rows with live Stripe IDs.
- **Task 46:** Internal validation pass via `INTERNAL_TESTER_EMAILS` allowlist (per-email bypass of `SELF_SERVE_ENABLED=false`). Backend support for the allowlist itself is NOT implemented — needs a small addition to `auth.py`/`config.py` if the validation pass requires it. Otherwise validate in test mode with the flag flipped temporarily.
- **Task 47:** Flip `SELF_SERVE_ENABLED=true` (backend) + `VITE_SELF_SERVE_ENABLED=true` (frontend rebuild). Watch PostHog funnel + Stripe webhook error rate.
3. **Followups deferred during Phase 2** (logged below).
## Followups deferred from this session
## Followups deferred from Phase 2
- **OAuth callbacks don't call `_store_refresh_token`.** The Google/Microsoft callbacks issue a refresh JWT but never persist its hash to `refresh_tokens` (the password-login flow does via `auth.py:_store_refresh_token`). Result: refresh-token revocation/rotation lookups won't find OAuth-issued tokens. Decide before Phase 2 dark-launch whether to backfill — likely yes, by extracting `_store_refresh_token` to a shared module and calling it from `_sign_in_or_register`.
- **`stripe_enabled` was relaxed** in Task 14 from `bool(STRIPE_SECRET_KEY) and bool(STRIPE_WEBHOOK_SECRET)` to just the secret key. The webhook handler in Task 16 independently checks `STRIPE_WEBHOOK_SECRET` before calling `construct_event`, so signature verification is still safe — but if any other code reads `stripe_enabled` and assumes the webhook secret is set, that's a latent bug. Audit before Phase 2 cutover.
- **`backend/app/core/stripe_handlers.py`** is a stub module that's no longer referenced after Task 16. Safe to delete in a follow-up; left in place to keep Phase 1 diff focused.
- **Pilot backfill migration `c6cbfc534fad` has not been applied to prod yet.** It runs once at deploy time and is forward-only.
- **PostHog server-side capture infrastructure missing.** `/sales-leads` calls `_capture_posthog_event` which lazy-imports `app.core.analytics.posthog` (no-op if module absent). Wire up the real PostHog server SDK before cutover — needs `app/core/analytics.py` with a configured client, plus `python-posthog` dep.
- **Frontend `/usage/{field}` endpoint missing.** `useFeatureLimit` lazy-fetches `apiClient.get('/usage/' + field)` — endpoint doesn't exist; hook silently falls back to `used=0`. Build the backend endpoint before any UI surface relies on real usage counts.
- **OAuth state CSRF — semi-validated.** RegisterPage path validates state round-trip on callback. AcceptInvitePage path uses a base64url-JSON envelope with embedded csrf — also validated. UTF-8-safe via `unescape(encodeURIComponent(...))`.
- **Pre-existing dual subscription stores** (`authStore.subscription` legacy + `billingStore.subscription` new). `Subscription.status` union in `frontend/src/types/account.ts` is missing `'complimentary'`. Phase 2 didn't introduce — but worth deprecating the old store before any UI reads `complimentary` from the wrong source.
- **`INTERNAL_TESTER_EMAILS` per-email allowlist** (Task 46): backend support not implemented. Add when Phase O Task 46 needs it.
- **`accept-invite` not gated by `self_serve_enabled`** — by design (invites work in any mode). Confirm if pilot wants stricter gating.
- **`UserResponse.onboarding_step_completed` validator** suggested by reviewer (reject `complete` without data) — skipped, spec types `data?` as optional.
- **Welcome wizard behind `EmailVerificationGate`:** Day 7+ unverified users hit the wall on `/welcome/*` and have no path back into the wizard until they verify. Per spec — wall says "Resend verification" → user verifies → wall closes → wizard resumes. If product wants a different UX, allowlist `/welcome/*` in the gate.
## Environment notes (carry-forward)
- Code-server LXC has bun + docker but no native python/node/npm. Use `docker exec resolutionflow_{backend,frontend} ...` for build/test commands.
- Pytest WORKDIR is `/app` — test paths in pytest commands are `tests/<file>`, NOT `backend/tests/<file>`.
- Backend pytest cmd: `docker exec resolutionflow_backend pytest tests/<path> -v --override-ini="addopts="`. The full run takes ~25 min.
- Alembic via `docker exec -w /app resolutionflow_backend alembic ...`. Never pass `--rev-id`.
- No `gh` CLI on this LXC — use the Gitea API (`$GITEA_TOKEN` in `.claude/settings.local.json`) for PR/issue work, or run `gh` from a host that has it.
- Headless Chromium (`/qa`, `/browse`) needs `CONTAINER=1` in the env launching the browse server (LXC namespace constraint).
- Code-server LXC: docker-only, no native python/node/npm. Use `docker exec resolutionflow_{backend,frontend} ...`.
- Pytest: `docker exec resolutionflow_backend pytest tests/<file> -v --override-ini="addopts="`.
- Vitest: `docker exec -w /app resolutionflow_frontend npm test -- <path> --run`.
- TS build: `docker exec -w /app resolutionflow_frontend npx tsc -b`.
- Alembic: `docker exec -w /app resolutionflow_backend alembic ...`. Never `--rev-id`.
- No `gh` CLI — Gitea API via `$GITEA_TOKEN` for PR/issue work.
- Single alembic head: `c6cbfc534fad` (from Phase 1). Phase 2 added no migrations.