Initial commit: PodcastYes — AI podcast platform
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
import { getEffectivePlan } from "@/lib/billing/subscription";
|
||||
import { getUsage } 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<LimitCheck> {
|
||||
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<LimitCheck> {
|
||||
const check = await checkLimit(userId, metric, activeOrgId);
|
||||
if (!check.allowed) throw new LimitExceededError(check);
|
||||
return check;
|
||||
}
|
||||
|
||||
export function isUnlimited(limit: number): boolean {
|
||||
return limit === UNLIMITED;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { prisma } from "@/lib/db";
|
||||
import { periodKey } from "@/lib/utils";
|
||||
import type { UsageMetric } from "@/lib/billing/plans";
|
||||
|
||||
export type OwnerType = "user" | "organization";
|
||||
|
||||
/** Increment a monthly usage counter for a billing subject. */
|
||||
export async function incrementUsage(
|
||||
ownerId: string,
|
||||
ownerType: OwnerType,
|
||||
metric: UsageMetric,
|
||||
by = 1
|
||||
): Promise<void> {
|
||||
const key = periodKey(new Date());
|
||||
await prisma.usageRecord.upsert({
|
||||
where: { ownerId_periodKey_metric: { ownerId, periodKey: key, metric } },
|
||||
create: { ownerId, ownerType, periodKey: key, metric, count: by },
|
||||
update: { count: { increment: by } },
|
||||
});
|
||||
}
|
||||
|
||||
/** Current-period count for a single metric. */
|
||||
export async function getUsage(
|
||||
ownerId: string,
|
||||
metric: UsageMetric,
|
||||
date = new Date()
|
||||
): Promise<number> {
|
||||
const rec = await prisma.usageRecord.findUnique({
|
||||
where: { ownerId_periodKey_metric: { ownerId, periodKey: periodKey(date), metric } },
|
||||
});
|
||||
return rec?.count ?? 0;
|
||||
}
|
||||
|
||||
/** Current-period counts for all metrics. */
|
||||
export async function getUsageSummary(
|
||||
ownerId: string,
|
||||
date = new Date()
|
||||
): Promise<Record<UsageMetric, number>> {
|
||||
const rows = await prisma.usageRecord.findMany({
|
||||
where: { ownerId, periodKey: periodKey(date) },
|
||||
});
|
||||
const summary: Record<UsageMetric, number> = { script: 0, audio: 0, art: 0, repurpose: 0 };
|
||||
for (const row of rows) {
|
||||
if (row.metric in summary) summary[row.metric as UsageMetric] = row.count;
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
Reference in New Issue
Block a user