feat: add unlimited option to tier limits

Use -1 as a sentinel meaning "no cap" in tier_definitions numeric
columns. checkAndIncrementTierLimit skips the usage-cap WHERE clause
when the limit is -1. Admin tier UI gets an Unlimited switch per
field that disables the input and sends -1.
This commit is contained in:
Arnaud
2026-07-03 20:19:43 +02:00
parent 487e7755be
commit 9412529120
3 changed files with 58 additions and 17 deletions
+7 -2
View File
@@ -9,6 +9,9 @@ function currentMonth() {
export type LimitKey = "recipe" | "aiCall" | "storage";
/** Sentinel stored in tier_definitions numeric columns to mean "no cap". */
export const UNLIMITED = -1;
export class TierLimitError extends Error {
constructor(public readonly limit: LimitKey, public readonly tier: string) {
super(`Tier limit reached: ${limit} (tier: ${tier})`);
@@ -42,12 +45,13 @@ export async function checkAndIncrementTierLimit(
if (key === "aiCall") {
const limit = tierDef.aiCallsPerMonth;
const cap = limit === UNLIMITED ? sql`true` : sql`user_usage.ai_calls_used < ${limit}`;
const result = await db.execute(sql`
INSERT INTO user_usage (id, user_id, month, ai_calls_used, recipe_count, storage_used_mb)
VALUES (${id}, ${userId}, ${month}, 1, 0, 0)
ON CONFLICT (user_id, month) DO UPDATE
SET ai_calls_used = user_usage.ai_calls_used + 1
WHERE user_usage.ai_calls_used < ${limit}
WHERE ${cap}
RETURNING ai_calls_used
`);
if (result.length === 0) {
@@ -55,12 +59,13 @@ export async function checkAndIncrementTierLimit(
}
} else {
const limit = tierDef.maxRecipes;
const cap = limit === UNLIMITED ? sql`true` : sql`user_usage.recipe_count < ${limit}`;
const result = await db.execute(sql`
INSERT INTO user_usage (id, user_id, month, ai_calls_used, recipe_count, storage_used_mb)
VALUES (${id}, ${userId}, ${month}, 0, 1, 0)
ON CONFLICT (user_id, month) DO UPDATE
SET recipe_count = user_usage.recipe_count + 1
WHERE user_usage.recipe_count < ${limit}
WHERE ${cap}
RETURNING recipe_count
`);
if (result.length === 0) {