diff --git a/.ai/HANDOFF.md b/.ai/HANDOFF.md index 779000d7..f2c85df5 100644 --- a/.ai/HANDOFF.md +++ b/.ai/HANDOFF.md @@ -2,13 +2,18 @@ # HANDOFF.md -**Last updated:** 2026-05-06 (Phase 2 frontend-cutover code complete; cutover ops still pending) +**Last updated:** 2026-05-07 (Phase 2 code complete + four post-implementation fixes; cutover ops still pending) -**Active task:** None mid-flight. Branch `feat/self-serve-signup-phase-2` (HEAD `c75ce0c`) is ready to PR. +**Active task:** None mid-flight. Branch `feat/self-serve-signup-phase-2` (HEAD `502c0a4`) is ready to PR. ## Where this session ended -Tasks 27–44 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. +Tasks 27–44 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`. Cross-cutting review caught one redirect bug (fixed in-flight). After initial handoff, external code review surfaced four more real bugs — all fixed (commits `5e0c9d2`, `3630dd5`, `06200fa`, `502c0a4`): + +1. **OAuth refresh tokens never stored** (Phase 1 latent): `_store_refresh_token` extracted to module-public `store_refresh_token`; both Google + Microsoft callbacks now persist the JTI before returning. Test covers callback → `/auth/refresh` round-trip. +2. **OAuth callback never marked store authenticated** (Phase 2 introduced): `setTokens` now sets `isAuthenticated: true`. Verified safe for the refresh-interceptor caller. +3. **Stripe webhook idempotency could permanently drop failed events** (Phase 1 latent): `apply_subscription_event` now adds StripeEvent + runs handler + commits atomically; handler exception → rollback → Stripe retries. Inner `_handle_*` commits removed. +4. **Missing `/account/billing` and `/account/billing/select-plan` pages** (Phase 2 oversight): all the new billing CTAs (TrialPill, UpgradePrompt, NextStepCard, SetupChecklist) linked to non-existent routes. Built BillingPage (subscription summary + Manage-billing → portal session) and SelectPlanPage (plan cards + checkout flow). **Backend additions (Phase I, Tasks 27–31):** - `BillingService.open_customer_portal` + `GET /billing/portal-session` (allowlisted for canceled/unverified users). @@ -48,12 +53,13 @@ New env vars (Vite ARG/ENV in Dockerfile + `.env.example`): `VITE_SELF_SERVE_ENA - **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. +- **`SelectPlanPage` seat input doesn't validate against plan `max_seats`.** Stripe Checkout will reject if exceeded; nicer UX would cap client-side. +- **`BillingPage` Manage-billing button always enabled** — clicking with no `stripe_customer_id` falls back to a toast. By design; could disable + tooltip if billing-state payload were extended with that flag. ## Environment notes (carry-forward)