feat(billing): plan taxonomy reconciliation + Stripe sync + internal-tester allowlist #164

Merged
chihlasm merged 6 commits from feat/billing-plan-taxonomy into main 2026-05-11 05:07:08 +00:00
9 changed files with 24 additions and 20 deletions
Showing only changes of commit 2c9f5e95ff - Show all commits

View File

@@ -8,6 +8,7 @@ interface PageMetaProps {
} }
const SITE_NAME = 'ResolutionFlow' const SITE_NAME = 'ResolutionFlow'
const DEFAULT_TAGLINE = 'AI-Powered Troubleshooting for MSPs'
const DEFAULT_DESCRIPTION = 'Transform troubleshooting into guided workflows with automatic documentation' const DEFAULT_DESCRIPTION = 'Transform troubleshooting into guided workflows with automatic documentation'
/** /**
@@ -20,7 +21,7 @@ export function PageMeta({
ogImage, ogImage,
ogType = 'website', ogType = 'website',
}: PageMetaProps) { }: PageMetaProps) {
const fullTitle = title ? `${title} | ${SITE_NAME}` : `${SITE_NAME} - Decision Tree Platform` const fullTitle = title ? `${title} | ${SITE_NAME}` : `${SITE_NAME} ${DEFAULT_TAGLINE}`
return ( return (
<Helmet> <Helmet>

View File

@@ -1,12 +1,12 @@
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
interface CheckoutButtonProps { interface CheckoutButtonProps {
plan: 'pro' | 'team' plan: 'starter' | 'pro' | 'enterprise'
className?: string className?: string
} }
export function CheckoutButton({ plan, className }: CheckoutButtonProps) { export function CheckoutButton({ plan, className }: CheckoutButtonProps) {
const planLabels = { pro: 'Pro', team: 'Team' } const planLabels = { starter: 'Starter', pro: 'Pro', enterprise: 'Enterprise' }
return ( return (
<button <button

View File

@@ -418,10 +418,10 @@ export function AccountSettingsPage() {
<p className="text-sm text-muted-foreground">Plan limits unavailable.</p> <p className="text-sm text-muted-foreground">Plan limits unavailable.</p>
)} )}
{plan !== 'team' && ( {plan !== 'enterprise' && (
<div className="flex flex-wrap justify-end gap-2 pt-2"> <div className="flex flex-wrap justify-end gap-2 pt-2">
{plan === 'free' && <CheckoutButton plan="pro" />} {plan === 'free' && <CheckoutButton plan="pro" />}
<CheckoutButton plan="team" /> <CheckoutButton plan="enterprise" />
</div> </div>
)} )}
</section> </section>

View File

@@ -15,7 +15,7 @@ const FAQ_ITEMS = [
}, },
{ {
q: 'What PSA tools do you integrate with?', q: 'What PSA tools do you integrate with?',
a: 'Launching with ConnectWise PSA \u2014 session documentation exports directly as internal ticket notes. Atera and Syncro integrations are next. During beta, you can copy formatted notes into any PSA.', a: 'Launching with ConnectWise PSA session documentation exports directly as internal ticket notes. Atera and Syncro integrations are next. During beta, you can copy formatted notes into any PSA.',
}, },
{ {
q: 'What counts as a \u201csession\u201d?', q: 'What counts as a \u201csession\u201d?',
@@ -23,7 +23,7 @@ const FAQ_ITEMS = [
}, },
{ {
q: 'What if FlowPilot gets it wrong?', q: 'What if FlowPilot gets it wrong?',
a: 'FlowPilot is a copilot, not autopilot. Every suggestion is a recommendation \u2014 you decide what to act on. And because every step is documented, you always have a full audit trail of what was tried and why.', a: 'FlowPilot is a copilot, not autopilot. Every suggestion is a recommendation you decide what to act on. And because every step is documented, you always have a full audit trail of what was tried and why.',
}, },
] ]
@@ -75,8 +75,8 @@ export default function LandingPage() {
return ( return (
<> <>
<PageMeta <PageMeta
title="ResolutionFlow \u2014 From Issue to Resolution, Documented" title="ResolutionFlow From Issue to Resolution, Documented"
description="Your AI troubleshooting copilot. Describe the issue, get help fixing it, and get clean ticket notes \u2014 automatically." description="Your AI troubleshooting copilot. Describe the issue, get help fixing it, and get clean ticket notes automatically."
/> />
<div className="landing-page"> <div className="landing-page">

View File

@@ -88,7 +88,7 @@ export function UsersPage() {
}) })
const [inviteLoading, setInviteLoading] = useState(false) const [inviteLoading, setInviteLoading] = useState(false)
const [showCreateAccountModal, setShowCreateAccountModal] = useState(false) const [showCreateAccountModal, setShowCreateAccountModal] = useState(false)
const [createAccountForm, setCreateAccountForm] = useState({ name: '', plan: 'free' as 'free' | 'pro' | 'team', owner_email: '' }) const [createAccountForm, setCreateAccountForm] = useState({ name: '', plan: 'free' as 'free' | 'pro' | 'starter' | 'enterprise', owner_email: '' })
const [createAccountLoading, setCreateAccountLoading] = useState(false) const [createAccountLoading, setCreateAccountLoading] = useState(false)
const fetchAccounts = useCallback(async () => { const fetchAccounts = useCallback(async () => {
@@ -469,7 +469,8 @@ export function UsersPage() {
<option value="all">All plans</option> <option value="all">All plans</option>
<option value="free">Free</option> <option value="free">Free</option>
<option value="pro">Pro</option> <option value="pro">Pro</option>
<option value="team">Team</option> <option value="enterprise">Enterprise</option>
<option value="starter">Starter</option>
</select> </select>
<select <select
value={statusFilter} value={statusFilter}
@@ -629,7 +630,7 @@ export function UsersPage() {
<label className="mb-1 block text-sm font-medium text-foreground">Initial Plan</label> <label className="mb-1 block text-sm font-medium text-foreground">Initial Plan</label>
<select <select
value={createAccountForm.plan} value={createAccountForm.plan}
onChange={(e) => setCreateAccountForm((form) => ({ ...form, plan: e.target.value as 'free' | 'pro' | 'team' }))} onChange={(e) => setCreateAccountForm((form) => ({ ...form, plan: e.target.value as 'free' | 'pro' | 'starter' | 'enterprise' }))}
className={cn( className={cn(
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground', 'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
'focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20' 'focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20'
@@ -637,7 +638,8 @@ export function UsersPage() {
> >
<option value="free">Free</option> <option value="free">Free</option>
<option value="pro">Pro</option> <option value="pro">Pro</option>
<option value="team">Team</option> <option value="enterprise">Enterprise</option>
<option value="starter">Starter</option>
</select> </select>
</div> </div>
<div> <div>

View File

@@ -12,8 +12,9 @@ import type { InviteCodeResponse, InviteCodeCreateRequest } from '@/types/admin'
const PLAN_OPTIONS = [ const PLAN_OPTIONS = [
{ value: 'free', label: 'Free' }, { value: 'free', label: 'Free' },
{ value: 'starter', label: 'Starter' },
{ value: 'pro', label: 'Pro' }, { value: 'pro', label: 'Pro' },
{ value: 'team', label: 'Team' }, { value: 'enterprise', label: 'Enterprise' },
] as const ] as const
const planBadgeVariant = (plan: string): 'success' | 'destructive' | 'warning' | 'default' => { const planBadgeVariant = (plan: string): 'success' | 'destructive' | 'warning' | 'default' => {
@@ -33,7 +34,7 @@ export function InviteCodesPage() {
// Form state // Form state
const [email, setEmail] = useState('') const [email, setEmail] = useState('')
const [expiresInDays, setExpiresInDays] = useState('') const [expiresInDays, setExpiresInDays] = useState('')
const [assignedPlan, setAssignedPlan] = useState<'free' | 'pro' | 'team'>('free') const [assignedPlan, setAssignedPlan] = useState<'free' | 'pro' | 'starter' | 'enterprise'>('free')
const [trialDays, setTrialDays] = useState('') const [trialDays, setTrialDays] = useState('')
const [note, setNote] = useState('') const [note, setNote] = useState('')
@@ -269,7 +270,7 @@ export function InviteCodesPage() {
aria-label="Plan" aria-label="Plan"
value={assignedPlan} value={assignedPlan}
onChange={(e) => { onChange={(e) => {
const plan = e.target.value as 'free' | 'pro' | 'team' const plan = e.target.value as 'free' | 'pro' | 'starter' | 'enterprise'
setAssignedPlan(plan) setAssignedPlan(plan)
if (plan === 'free') setTrialDays('') if (plan === 'free') setTrialDays('')
}} }}

View File

@@ -10,7 +10,7 @@ export interface Account {
export interface Subscription { export interface Subscription {
id: string id: string
account_id: string account_id: string
plan: 'free' | 'pro' | 'team' plan: 'free' | 'pro' | 'starter' | 'enterprise'
status: 'active' | 'past_due' | 'canceled' | 'trialing' | 'orphaned' status: 'active' | 'past_due' | 'canceled' | 'trialing' | 'orphaned'
current_period_start: string | null current_period_start: string | null
current_period_end: string | null current_period_end: string | null

View File

@@ -113,7 +113,7 @@ export interface AdminAccountDetailResponse extends AdminAccountListItem {
export interface AdminAccountCreate { export interface AdminAccountCreate {
name: string name: string
plan: 'free' | 'pro' | 'team' plan: 'free' | 'pro' | 'starter' | 'enterprise'
owner_email?: string owner_email?: string
} }
@@ -257,7 +257,7 @@ export interface InviteCodeCreateRequest {
expires_at?: string | null expires_at?: string | null
note?: string | null note?: string | null
email?: string | null email?: string | null
assigned_plan?: 'free' | 'pro' | 'team' assigned_plan?: 'free' | 'pro' | 'starter' | 'enterprise'
trial_duration_days?: number | null trial_duration_days?: number | null
} }

View File

@@ -54,7 +54,7 @@ export interface BillingStateApiResponse {
* Checkout / Customer-Portal session types * Checkout / Customer-Portal session types
* ------------------------------------------------------------------------- */ * ------------------------------------------------------------------------- */
export type CheckoutPlan = 'starter' | 'pro' | 'team' | 'enterprise' export type CheckoutPlan = 'starter' | 'pro' | 'enterprise'
export type BillingInterval = 'monthly' | 'annual' export type BillingInterval = 'monthly' | 'annual'
export interface CheckoutSessionRequest { export interface CheckoutSessionRequest {