Files
simstudioai--sim/apps/sim/lib/billing/plan-helpers.ts
T
wehub-resource-sync d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

156 lines
5.1 KiB
TypeScript

/**
* Plan type helpers for the credit-tier billing system.
*
* Plan names follow the convention `{type}_{credits}`:
* - `pro_6000` (Pro), `pro_25000` (Max)
* - `team_6000` (Team Pro), `team_25000` (Team Max)
* - `free`, `enterprise` (unchanged)
*
* Legacy plan names (`pro`, `team`) are also recognized for backward compat
* and map to their original dollar amounts ($20 / $40).
*/
import type { AnyColumn } from 'drizzle-orm'
import { eq, like, or, type SQL } from 'drizzle-orm'
import {
CREDIT_TIERS,
DEFAULT_PRO_TIER_COST_LIMIT,
DEFAULT_TEAM_TIER_COST_LIMIT,
} from '@/lib/billing/constants'
export type PlanCategory = 'free' | 'pro' | 'team' | 'enterprise'
export function isPro(plan: string | null | undefined): boolean {
if (!plan) return false
return plan === 'pro' || plan.startsWith('pro_')
}
export function isMax(plan: string | null | undefined): boolean {
return isPro(plan) && getPlanTierCredits(plan) >= 25000
}
export function isTeam(plan: string | null | undefined): boolean {
if (!plan) return false
return plan === 'team' || plan.startsWith('team_')
}
export function isFree(plan: string | null | undefined): boolean {
return !plan || plan === 'free'
}
export function isEnterprise(plan: string | null | undefined): boolean {
return plan === 'enterprise'
}
export function isPaid(plan: string | null | undefined): boolean {
return isPro(plan) || isTeam(plan) || isEnterprise(plan)
}
/**
* True when the plan **name** is a team/enterprise plan. This is a
* plan-name check, NOT a scope check — a `pro_*` plan attached to an
* organization is org-scoped at the billing level even though this
* returns `false` for it. For scope decisions use
* `isOrgScopedSubscription` (sync) or `isSubscriptionOrgScoped` (async).
*/
export function isOrgPlan(plan: string | null | undefined): boolean {
return isTeam(plan) || isEnterprise(plan)
}
/**
* Extract the credit count from a plan name (e.g. `'pro_6000'` => `6000`).
* Legacy names map to their original dollar values:
* `'pro'` => 4000 credits ($20 at 1:200), `'team'` => 8000 credits ($40 at 1:200).
*/
export function getPlanTierCredits(plan: string | null | undefined): number {
if (!plan) return 0
const match = plan.match(/_(\d+)$/)
if (match) return Number.parseInt(match[1], 10)
if (plan === 'pro') return 4000
if (plan === 'team') return 8000
return 0
}
/**
* Get the dollar value of a plan's credit tier.
* Looks up from CREDIT_TIERS for exact mapping, with legacy plan fallbacks.
*/
export function getPlanTierDollars(plan: string | null | undefined): number {
if (!plan) return 0
const credits = getPlanTierCredits(plan)
const tier = CREDIT_TIERS.find((t) => t.credits === credits)
if (tier) return tier.dollars
if (plan === 'pro') return DEFAULT_PRO_TIER_COST_LIMIT
if (plan === 'team') return DEFAULT_TEAM_TIER_COST_LIMIT
return 0
}
/**
* Return the broad plan category regardless of tier suffix.
*/
export function getPlanType(plan: string | null | undefined): PlanCategory {
if (isPro(plan)) return 'pro'
if (isTeam(plan)) return 'team'
if (isEnterprise(plan)) return 'enterprise'
return 'free'
}
/**
* Return the plan category used for rate limits, storage, and execution timeouts.
* Max plans (>= 25K credits) are promoted to team-level limits.
*/
export function getPlanTypeForLimits(plan: string | null | undefined): PlanCategory {
const credits = getPlanTierCredits(plan)
if (credits >= 25000 && isPro(plan)) return 'team'
return getPlanType(plan)
}
/**
* Build the canonical plan name for a given type and credit tier.
* @example buildPlanName('pro', 6000) => 'pro_6000'
*/
export function buildPlanName(type: 'pro' | 'team', credits: number): string {
return `${type}_${credits}`
}
/**
* Get the list of valid plan names for a given category.
*/
export function getValidPlanNames(type: 'pro' | 'team'): string[] {
return CREDIT_TIERS.map((t) => buildPlanName(type, t.credits))
}
/**
* Get the user-facing display name for a plan.
* @example getDisplayPlanName('pro_25000') => 'Max'
* @example getDisplayPlanName('team_6000') => 'Pro for Teams'
* @example getDisplayPlanName('pro') => 'Legacy Pro'
*/
/**
* SQL-level plan filters for Drizzle queries.
* These are the SQL equivalents of the JS helpers above.
*/
export function sqlIsPro(column: AnyColumn): SQL | undefined {
return or(eq(column, 'pro'), like(column, 'pro_%'))
}
export function sqlIsTeam(column: AnyColumn): SQL | undefined {
return or(eq(column, 'team'), like(column, 'team_%'))
}
export function sqlIsPaid(column: AnyColumn): SQL | undefined {
return or(sqlIsPro(column)!, sqlIsTeam(column)!, eq(column, 'enterprise'))
}
export function getDisplayPlanName(plan: string | null | undefined): string {
if (!plan || isFree(plan)) return 'Free'
if (isEnterprise(plan)) return 'Enterprise'
const credits = getPlanTierCredits(plan)
const tier = CREDIT_TIERS.find((t) => t.credits === credits)
const isLegacy = plan === 'pro' || plan === 'team'
const tierName = tier?.name ?? (plan === 'team' ? 'Max' : 'Pro')
const prefix = isLegacy ? 'Legacy ' : ''
const suffix = isTeam(plan) ? ' for Teams' : ''
return `${prefix}${tierName}${suffix}`
}