Files
Epicure/apps/web/lib/tiers.ts
T
Arnaud 5b40968c9d feat(web): Next.js 15 app shell, auth, i18n, base UI
App Router setup with next-intl (en/fr), Better Auth wiring, shadcn/ui components,
Tailwind, AES-256-GCM encrypt util, Redis rate limiter, tier limit checker,
site_settings helper for runtime .env overrides.
2026-07-01 08:09:31 +02:00

83 lines
2.1 KiB
TypeScript

import { db } from "@epicure/db";
import { tierDefinitions, userUsage } from "@epicure/db";
import { eq, and, sql } from "@epicure/db";
function currentMonth() {
const now = new Date();
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`;
}
export type LimitKey = "recipe" | "aiCall" | "storage";
export class TierLimitError extends Error {
constructor(public readonly limit: LimitKey, public readonly tier: string) {
super(`Tier limit reached: ${limit} (tier: ${tier})`);
this.name = "TierLimitError";
}
}
export async function checkTierLimit(
userId: string,
userTier: "free" | "pro",
key: LimitKey
): Promise<void> {
const [tierDef] = await db
.select()
.from(tierDefinitions)
.where(eq(tierDefinitions.tier, userTier));
if (!tierDef) return;
const month = currentMonth();
const [usage] = await db
.select()
.from(userUsage)
.where(and(eq(userUsage.userId, userId), eq(userUsage.month, month)));
const current = usage ?? {
aiCallsUsed: 0,
recipeCount: 0,
storageUsedMb: 0,
};
if (key === "recipe" && current.recipeCount >= tierDef.maxRecipes) {
throw new TierLimitError("recipe", userTier);
}
if (key === "aiCall" && current.aiCallsUsed >= tierDef.aiCallsPerMonth) {
throw new TierLimitError("aiCall", userTier);
}
}
export async function incrementUsage(
userId: string,
key: LimitKey,
amount = 1
): Promise<void> {
const month = currentMonth();
const id = `${userId}-${month}`;
const initialValues = {
id,
userId,
month,
aiCallsUsed: key === "aiCall" ? amount : 0,
recipeCount: key === "recipe" ? amount : 0,
storageUsedMb: key === "storage" ? amount : 0,
};
const incrementSet =
key === "aiCall"
? { aiCallsUsed: sql`${userUsage.aiCallsUsed} + ${amount}` }
: key === "recipe"
? { recipeCount: sql`${userUsage.recipeCount} + ${amount}` }
: { storageUsedMb: sql`${userUsage.storageUsedMb} + ${amount}` };
await db
.insert(userUsage)
.values(initialValues)
.onConflictDoUpdate({
target: [userUsage.userId, userUsage.month],
set: incrementSet,
});
}