feat: per-tier feature toggles for recipe variations/pairings (v0.50.0)
Admins can now disable specific AI features per tier from Admin > Tier Limits — new feature_flags table (feature x tier -> enabled, defaulting to true so adding a new gated feature never needs a backfill). Covers recipe variations, drink pairing, and meal pairing to start. When disabled for a user's tier, the button stays visible (with a small lock badge) but opens an upgrade dialog instead of running; the API route rejects the call server-side either way (requireFeatureEnabled, re-reads tier from the DB rather than trusting the session's cache, same rationale as checkAndIncrementTierLimit). The upgrade dialog is informational only — no Stripe checkout exists yet (STRIPE_PLAN.md is still just a plan) — its CTA links to /support prefilled as an upgrade-interest suggestion. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ import { withAiQuota, resolveAiConfigOrError } from "@/lib/ai/ai-error";
|
||||
import { suggestDrinks } from "@/lib/ai/features/suggest-drinks";
|
||||
import { withUserKey } from "@/lib/ai/resolve-user-key";
|
||||
import { getUserPrivateBio } from "@/lib/ai/user-bio";
|
||||
import { requireFeatureEnabled, FeatureDisabledError } from "@/lib/feature-flags";
|
||||
|
||||
const Schema = z.object({
|
||||
count: z.number().int().min(1).max(6).default(4),
|
||||
@@ -20,6 +21,18 @@ export async function POST(req: NextRequest, { params }: Params) {
|
||||
const { session, response } = await requireSessionOrApiKey(req);
|
||||
if (response) return response;
|
||||
|
||||
try {
|
||||
await requireFeatureEnabled(session!.user.id, "drink_pairing");
|
||||
} catch (err) {
|
||||
if (err instanceof FeatureDisabledError) {
|
||||
return NextResponse.json(
|
||||
{ error: "This feature isn't available on your plan", code: "FEATURE_DISABLED", featureKey: err.featureKey },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
|
||||
const recipe = await db.query.recipes.findFirst({
|
||||
|
||||
@@ -7,6 +7,7 @@ import { withAiQuota, resolveAiConfigOrError } from "@/lib/ai/ai-error";
|
||||
import { suggestPairings } from "@/lib/ai/features/suggest-pairings";
|
||||
import { withUserKey } from "@/lib/ai/resolve-user-key";
|
||||
import { getUserPrivateBio } from "@/lib/ai/user-bio";
|
||||
import { requireFeatureEnabled, FeatureDisabledError } from "@/lib/feature-flags";
|
||||
|
||||
const Schema = z.object({
|
||||
count: z.number().int().min(1).max(6).default(4),
|
||||
@@ -20,6 +21,18 @@ export async function POST(req: NextRequest, { params }: Params) {
|
||||
const { session, response } = await requireSessionOrApiKey(req);
|
||||
if (response) return response;
|
||||
|
||||
try {
|
||||
await requireFeatureEnabled(session!.user.id, "meal_pairing");
|
||||
} catch (err) {
|
||||
if (err instanceof FeatureDisabledError) {
|
||||
return NextResponse.json(
|
||||
{ error: "This feature isn't available on your plan", code: "FEATURE_DISABLED", featureKey: err.featureKey },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
|
||||
// Allow pairings for own recipes or public recipes
|
||||
|
||||
@@ -7,6 +7,7 @@ import { withAiQuota, resolveAiConfigOrError } from "@/lib/ai/ai-error";
|
||||
import { suggestVariations } from "@/lib/ai/features/suggest-variations";
|
||||
import { withUserKey } from "@/lib/ai/resolve-user-key";
|
||||
import { getUserPrivateBio } from "@/lib/ai/user-bio";
|
||||
import { requireFeatureEnabled, FeatureDisabledError } from "@/lib/feature-flags";
|
||||
|
||||
const Schema = z.object({
|
||||
count: z.number().int().min(1).max(5).default(3),
|
||||
@@ -21,6 +22,18 @@ export async function POST(req: NextRequest, { params }: Params) {
|
||||
const { session, response } = await requireSessionOrApiKey(req);
|
||||
if (response) return response;
|
||||
|
||||
try {
|
||||
await requireFeatureEnabled(session!.user.id, "recipe_variations");
|
||||
} catch (err) {
|
||||
if (err instanceof FeatureDisabledError) {
|
||||
return NextResponse.json(
|
||||
{ error: "This feature isn't available on your plan", code: "FEATURE_DISABLED", featureKey: err.featureKey },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
const recipe = await db.query.recipes.findFirst({
|
||||
where: and(eq(recipes.id, id), eq(recipes.authorId, session!.user.id)),
|
||||
|
||||
Reference in New Issue
Block a user