chore: import upstream snapshot with attribution
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
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
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
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
/**
|
||||
* Helper functions for subscription-related computations
|
||||
* These are pure functions that compute values from subscription data
|
||||
*/
|
||||
|
||||
import { DEFAULT_FREE_CREDITS } from '@/lib/billing/constants'
|
||||
import { getPlanTierCredits, isEnterprise, isFree, isPro } from '@/lib/billing/plan-helpers'
|
||||
import { hasUsableSubscriptionAccess } from '@/lib/billing/subscriptions/utils'
|
||||
import { USAGE_PILL_COLORS } from './consts'
|
||||
import type { BillingStatus, SubscriptionData, UsageData } from './types'
|
||||
|
||||
const defaultUsage: UsageData = {
|
||||
current: 0,
|
||||
limit: DEFAULT_FREE_CREDITS,
|
||||
percentUsed: 0,
|
||||
isWarning: false,
|
||||
isExceeded: false,
|
||||
billingPeriodStart: null,
|
||||
billingPeriodEnd: null,
|
||||
lastPeriodCost: 0,
|
||||
}
|
||||
|
||||
/**
|
||||
* Get subscription status flags from subscription data
|
||||
*/
|
||||
export function getSubscriptionStatus(
|
||||
subscriptionData: Partial<SubscriptionData> | null | undefined
|
||||
) {
|
||||
return {
|
||||
isPaid: subscriptionData?.isPaid ?? false,
|
||||
isPro: subscriptionData?.isPro ?? false,
|
||||
isTeam: subscriptionData?.isTeam ?? false,
|
||||
isEnterprise: subscriptionData?.isEnterprise ?? false,
|
||||
isOrgScoped: subscriptionData?.isOrgScoped ?? false,
|
||||
organizationId: subscriptionData?.organizationId ?? null,
|
||||
isFree: !(subscriptionData?.isPaid ?? false),
|
||||
plan: subscriptionData?.plan ?? 'free',
|
||||
status: subscriptionData?.status ?? null,
|
||||
seats: subscriptionData?.seats ?? null,
|
||||
metadata: subscriptionData?.metadata ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
export function getSubscriptionAccessState(
|
||||
subscriptionData: Partial<SubscriptionData> | null | undefined
|
||||
) {
|
||||
const status = getSubscriptionStatus(subscriptionData)
|
||||
const billingBlocked = Boolean(subscriptionData?.billingBlocked)
|
||||
const hasUsablePaidAccess = hasUsableSubscriptionAccess(status.status, billingBlocked)
|
||||
// Team-management features (invitations, seats, roles) are available on
|
||||
// any paid subscription attached to an organization — including `pro_*`
|
||||
// plans that have been transferred to an org. Plan-name gating would
|
||||
// miss those.
|
||||
const hasUsableTeamAccess =
|
||||
hasUsablePaidAccess && (status.isOrgScoped || status.isTeam || status.isEnterprise)
|
||||
const hasUsableEnterpriseAccess = hasUsablePaidAccess && status.isEnterprise
|
||||
const hasUsableMaxAccess =
|
||||
hasUsablePaidAccess && (getPlanTierCredits(status.plan) >= 25000 || isEnterprise(status.plan))
|
||||
|
||||
return {
|
||||
...status,
|
||||
billingBlocked,
|
||||
hasUsablePaidAccess,
|
||||
hasUsableTeamAccess,
|
||||
hasUsableEnterpriseAccess,
|
||||
hasUsableMaxAccess,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get usage data from subscription data
|
||||
* Validates and sanitizes all numeric values to prevent crashes from malformed data
|
||||
*/
|
||||
export function getUsage(
|
||||
subscriptionData: Partial<SubscriptionData> | null | undefined
|
||||
): UsageData {
|
||||
const usage = subscriptionData?.usage
|
||||
|
||||
if (!usage) {
|
||||
return defaultUsage
|
||||
}
|
||||
|
||||
return {
|
||||
current:
|
||||
typeof usage.current === 'number' && Number.isFinite(usage.current) ? usage.current : 0,
|
||||
limit:
|
||||
typeof usage.limit === 'number' && Number.isFinite(usage.limit)
|
||||
? usage.limit
|
||||
: DEFAULT_FREE_CREDITS,
|
||||
percentUsed:
|
||||
typeof usage.percentUsed === 'number' && Number.isFinite(usage.percentUsed)
|
||||
? usage.percentUsed
|
||||
: 0,
|
||||
isWarning: Boolean(usage.isWarning),
|
||||
isExceeded: Boolean(usage.isExceeded),
|
||||
billingPeriodStart: usage.billingPeriodStart ?? null,
|
||||
billingPeriodEnd: usage.billingPeriodEnd ?? null,
|
||||
lastPeriodCost:
|
||||
typeof usage.lastPeriodCost === 'number' && Number.isFinite(usage.lastPeriodCost)
|
||||
? usage.lastPeriodCost
|
||||
: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get billing status based on usage and blocked state
|
||||
*/
|
||||
export function getBillingStatus(
|
||||
subscriptionData: Partial<SubscriptionData> | null | undefined
|
||||
): BillingStatus {
|
||||
const usage = getUsage(subscriptionData)
|
||||
const blocked = subscriptionData?.billingBlocked
|
||||
if (blocked) return 'blocked'
|
||||
if (usage.isExceeded) return 'exceeded'
|
||||
if (usage.isWarning) return 'warning'
|
||||
return 'ok'
|
||||
}
|
||||
|
||||
/**
|
||||
* Get remaining budget
|
||||
*/
|
||||
export function getRemainingBudget(
|
||||
subscriptionData: Partial<SubscriptionData> | null | undefined
|
||||
): number {
|
||||
const usage = getUsage(subscriptionData)
|
||||
return Math.max(0, usage.limit - usage.current)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get days remaining in billing period
|
||||
*/
|
||||
export function getDaysRemainingInPeriod(
|
||||
subscriptionData: Partial<SubscriptionData> | null | undefined
|
||||
): number | null {
|
||||
const usage = getUsage(subscriptionData)
|
||||
if (!usage.billingPeriodEnd) return null
|
||||
|
||||
const now = new Date()
|
||||
const endDate =
|
||||
typeof usage.billingPeriodEnd === 'string'
|
||||
? new Date(usage.billingPeriodEnd)
|
||||
: usage.billingPeriodEnd
|
||||
const diffTime = endDate.getTime() - now.getTime()
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
|
||||
|
||||
return Math.max(0, diffDays)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if subscription is at least Pro tier
|
||||
*/
|
||||
export function isAtLeastPro(
|
||||
subscriptionData: Partial<SubscriptionData> | null | undefined
|
||||
): boolean {
|
||||
const status = getSubscriptionStatus(subscriptionData)
|
||||
return status.isPro || status.isTeam || status.isEnterprise
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if subscription is at least Team tier
|
||||
*/
|
||||
export function isAtLeastTeam(
|
||||
subscriptionData: Partial<SubscriptionData> | null | undefined
|
||||
): boolean {
|
||||
const status = getSubscriptionStatus(subscriptionData)
|
||||
return status.isTeam || status.isEnterprise
|
||||
}
|
||||
|
||||
export function canUpgrade(
|
||||
subscriptionData: Partial<SubscriptionData> | null | undefined
|
||||
): boolean {
|
||||
const status = getSubscriptionStatus(subscriptionData)
|
||||
return isFree(status.plan) || isPro(status.plan)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the appropriate filled pill color based on usage thresholds.
|
||||
*
|
||||
* @param isCritical - Whether usage is at critical level (blocked or >= 90%)
|
||||
* @param isWarning - Whether usage is at warning level (>= 75% but < critical)
|
||||
* @returns CSS color value for filled pills
|
||||
*/
|
||||
export function getFilledPillColor(isCritical: boolean, isWarning: boolean): string {
|
||||
if (isCritical) return USAGE_PILL_COLORS.AT_LIMIT
|
||||
if (isWarning) return USAGE_PILL_COLORS.WARNING
|
||||
return USAGE_PILL_COLORS.FILLED
|
||||
}
|
||||
Reference in New Issue
Block a user