Comprehensive admin + user dashboards (production-ready)
This commit is contained in:
+31
-1
@@ -1,5 +1,5 @@
|
||||
import { getEffectivePlan } from "@/lib/billing/subscription";
|
||||
import { getUsage } from "./meter";
|
||||
import { getUsage, reserveUsage } from "./meter";
|
||||
import { PLANS, UNLIMITED, withinLimit, type PlanKey, type UsageMetric } from "@/lib/billing/plans";
|
||||
|
||||
export interface LimitCheck {
|
||||
@@ -46,6 +46,36 @@ export async function enforceLimit(
|
||||
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<LimitCheck> {
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user