/** * 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | null | undefined ): boolean { const status = getSubscriptionStatus(subscriptionData) return status.isTeam || status.isEnterprise } export function canUpgrade( subscriptionData: Partial | 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 }