feat(billing): plan taxonomy reconciliation + Stripe sync + internal-tester allowlist #164
Reference in New Issue
Block a user
Delete Branch "feat/billing-plan-taxonomy"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Three commits unblocking Phase O of the self-serve signup cutover. PR #162 left two real gaps: the marketing surface used a different plan taxonomy than the backend, and Phase O Task 46 needed a per-email allowlist that was never built. This PR closes both.
Commits
feat(billing): reconcile plan taxonomy and add Stripe sync script4ce3e594cb87: renameteam→enterpriseinplan_limits, addstarterrow (caps interpolated between free and pro), defensive update of any existing subscriptions on theteamslug.invite_code,billing,admin),Subscriptionmodel paid-plan checks, admin endpoints, and frontenduseSubscription. Resource visibility (Tree.visibility='team',StepLibrary.visibility='team') is a separate domain and intentionally untouched.backend/scripts/sync_stripe_plan_ids.py: idempotent upsert ofplan_billingrows from Stripe products by exact name match. Picks the active monthly recurring price for tiers that have one. Annual fields stay NULL by design — annual pricing is intentionally out of scope for now. Works against both test and live keys.chore(dev): pass STRIPE_* env to backend container; add repo-root .env.exampleSTRIPE_SECRET_KEY/STRIPE_PUBLISHABLE_KEY/STRIPE_WEBHOOK_SECRET/SELF_SERVE_ENABLED/INTERNAL_TESTER_EMAILSthroughdocker-compose.dev.yml..env.exampleat the repo root documenting the variables compose itself reads (separate frombackend/.env.example).feat(billing): add INTERNAL_TESTER_EMAILS allowlist for self-serve soft cutoverSettings.is_internal_testerandSettings.is_self_serve_active_forcentralize the allowlist + global-flag check.get_current_user_optionaldep — best-effort auth that returnsNoneinstead of 401 — used by/config/public./config/publicnow honors the allowlist for authenticated callers; anonymous calls always see the global flag./auth/registerswaps theSELF_SERVE_ENABLEDcheck foris_self_serve_active_for(user_data.email)so allowlisted emails register without an invite code.Stripe-side state (test mode, manual via MCP)
$500/motest price (Enterprise is sales-led, no catalog price by spec).sync_stripe_plan_ids.pyagainst test mode —plan_billingpopulated for all three tiers in dev DB.Test plan
pytest tests/test_billing_checkout.py tests/test_plans_public.py tests/test_admin_plan_limits.py tests/test_invite_plan.py tests/test_config_public.py tests/test_auth.py— 42/42 passingpython -m scripts.sync_stripe_plan_ids --dry-runthen live run against test mode —plan_billingcorrectly populatedPhase O remaining (manual ops)
sk_live_*,whsec_*,INTERNAL_TESTER_EMAILS, prod OAuth credentials,OAUTH_REDIRECT_BASE)plan_billinghassk_live_*price IDsSELF_SERVE_ENABLED=true(frontend redeploy required forVITE_SELF_SERVE_ENABLED)🤖 Generated with Claude Code
Two fixes that surfaced together: 1. LandingPage.tsx had `—` in a JSX attribute string — JSX attribute strings don't process JS escape sequences, so the literal six-character "—" was rendering in the browser tab title and OG description instead of the intended em dash. Replaced with the literal em dash character. Same pattern was previously valid because every other use of `\u...` in the codebase is inside a JS string (regular `'...'` string literal in TS, or `{...}` expression in JSX), where escapes resolve at compile time. Verified by grep — LandingPage was the only site with the bug. 2. PageMeta default fallback tagline was "Decision Tree Platform" — a stale tagline from before the FlowPilot pivot. Updated to the current "AI-Powered Troubleshooting for MSPs" (matches index.html and brand positioning). Default branch is rarely hit since every page passes a title, but cleaner. While building, hit TS errors that revealed the prior taxonomy commit (team -> enterprise + add starter) didn't propagate through the frontend. Cleared all of them: - types/account.ts, types/admin.ts: Subscription.plan, AdminAccountCreate.plan, InviteCodeCreateRequest.assigned_plan literals updated to the new tax. - types/billing.ts: dropped 'team' from CheckoutPlan (was hybrid old+new). - admin/AccountsPage.tsx, admin/InviteCodesPage.tsx: state-type literals, select onChange casts, and the visible <option> rows updated. PLAN_OPTIONS in InviteCodesPage now has all four tiers with correct labels. - AccountSettingsPage.tsx: `plan !== 'team'` -> `'enterprise'`, CheckoutButton prop value too. - subscription/CheckoutButton.tsx: prop type was 'pro' | 'team', updated to 'starter' | 'pro' | 'enterprise' with matching planLabels. Verified: tsc -b clean, lint clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>