import { getEffectivePlan } from "@/lib/billing/subscription"; import { getUsage, reserveUsage } from "./meter"; import { PLANS, UNLIMITED, withinLimit, type PlanKey, type UsageMetric } from "@/lib/billing/plans"; export interface LimitCheck { allowed: boolean; used: number; limit: number; // UNLIMITED (-1) when uncapped plan: PlanKey; metric: UsageMetric; } /** Thrown by enforceLimit when a metric's monthly cap is reached. */ export class LimitExceededError extends Error { constructor(public check: LimitCheck) { super(`Monthly ${check.metric} limit reached on the ${check.plan} plan`); this.name = "LimitExceededError"; } } /** Read-only check of a metric against the subject's plan + current usage. */ export async function checkLimit( userId: string, metric: UsageMetric, activeOrgId?: string | null ): Promise { const { key, subjectId } = await getEffectivePlan(userId, activeOrgId); const used = await getUsage(subjectId, metric); return { allowed: withinLimit(key, metric, used), used, limit: PLANS[key].limits[metric], plan: key, metric, }; } /** Throw LimitExceededError if the metric is over its cap. */ export async function enforceLimit( userId: string, metric: UsageMetric, activeOrgId?: string | null ): Promise { const check = await checkLimit(userId, metric, activeOrgId); if (!check.allowed) throw new LimitExceededError(check); return check; } /** * Atomically RESERVE one unit of `metric` against the subject's plan cap. * Unlike `enforceLimit` (a read-only check), this consumes quota up front and * is race-safe — the canonical write path for any action that will generate * content. Throws `LimitExceededError` when the cap is already reached. * * Callers that proceed to enqueue/generate must REFUND (see `refundUsage`) if * the downstream work fails, so quota isn't permanently consumed by a failure. */ export async function reserveLimit( userId: string, metric: UsageMetric, activeOrgId?: string | null ): Promise { const { key, subjectId, subjectType } = await getEffectivePlan(userId, activeOrgId); const limit = PLANS[key].limits[metric]; const ok = await reserveUsage(subjectId, subjectType, metric, limit); // Re-read for an accurate `used` in the result; reservation already enforced. const used = await getUsage(subjectId, metric); const check: LimitCheck = { allowed: ok, used, limit, plan: key, metric, }; if (!ok) throw new LimitExceededError(check); return check; } export function isUnlimited(limit: number): boolean { return limit === UNLIMITED; }