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
156 lines
5.1 KiB
TypeScript
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}`
|
|
}
|