rename: "Team" billing tier to "Family"
All literal "team" tier-value references renamed to "family" across API routes, admin UI, OpenAPI schemas, and lib/tiers.ts. The DB enum value itself is renamed in place via ALTER TYPE ... RENAME VALUE (migration 0044) rather than drizzle-kit's auto-generated drop-and-recreate-the-enum migration, which would have failed against any existing row still holding 'team' — RENAME VALUE preserves existing data with no cast/backfill needed. Also adds STRIPE_PLAN.md — a full Stripe billing integration plan (Checkout+Portal, tier→Price mapping, admin billing dashboard, and a multi-user Family-group design since Family is meant to cover several accounts under one subscription, not one payer). Planning only, no Stripe code yet. v0.47.0
This commit is contained in:
@@ -52,7 +52,7 @@ export async function POST(req: NextRequest, { params }: Params) {
|
||||
if (!configResult.ok) return configResult.response;
|
||||
const aiConfig = configResult.data;
|
||||
|
||||
const result = await withAiQuota(userId, session!.user.tier as "free" | "pro" | "team", () =>
|
||||
const result = await withAiQuota(userId, session!.user.tier as "free" | "pro" | "family", () =>
|
||||
adaptRecipe(
|
||||
{
|
||||
title: recipe.title,
|
||||
@@ -80,7 +80,7 @@ export async function POST(req: NextRequest, { params }: Params) {
|
||||
}
|
||||
|
||||
try {
|
||||
await checkAndIncrementTierLimit(userId, session!.user.tier as "free" | "pro" | "team", "recipe");
|
||||
await checkAndIncrementTierLimit(userId, session!.user.tier as "free" | "pro" | "family", "recipe");
|
||||
} catch (err) {
|
||||
if (err instanceof TierLimitError) {
|
||||
return NextResponse.json({ error: "Recipe limit reached for your tier" }, { status: 403 });
|
||||
|
||||
@@ -43,7 +43,7 @@ export async function POST(req: NextRequest) {
|
||||
if (!configResult.ok) return configResult.response;
|
||||
const config = configResult.data;
|
||||
|
||||
const result = await withAiQuota(userId, session!.user.tier as "free" | "pro" | "team", () =>
|
||||
const result = await withAiQuota(userId, session!.user.tier as "free" | "pro" | "family", () =>
|
||||
generateBatchCook(
|
||||
{
|
||||
dinners: parsed.data.dinners,
|
||||
|
||||
@@ -42,7 +42,7 @@ export async function POST(req: NextRequest) {
|
||||
const locale = (session!.user as { locale?: string }).locale ?? "en";
|
||||
const lang = LANG[locale] ?? "English";
|
||||
|
||||
const result = await withAiQuota(session!.user.id, session!.user.tier as "free" | "pro" | "team", () =>
|
||||
const result = await withAiQuota(session!.user.id, session!.user.tier as "free" | "pro" | "family", () =>
|
||||
generateText({
|
||||
model,
|
||||
system: `You are Epicure, a helpful culinary assistant answering general cooking questions — not tied to any specific recipe (techniques, substitutions, timing, equipment, food safety, etc). If asked who you are or what model/AI you're built on, say you're Epicure — never name the underlying model or provider. If a question has nothing to do with cooking or food, politely redirect. Keep answers under 200 words. Respond in ${lang}.\n\nYou have two tools. Using one only drafts something for the user to review — it never saves by itself.\n- createRecipe: the user is asking you to create, save, or write down a recipe (e.g. "make me a recipe for X", "give me a recipe for Y", "write that down"). This includes any request for a full recipe, not only ones that say the word "create" or "save".\n- addToShoppingList: the user is asking to add ingredients/items to a shopping list.\n\nIMPORTANT: when the user's request matches createRecipe, you MUST call that tool instead of writing the recipe's ingredients or steps directly in your text reply. Never output a full ingredient list or numbered steps as plain text — that content belongs in the tool call, not the message. Your text reply in that case should just be a short line like "Here's a draft — check it below and confirm if it looks right." Only skip the tool if the user is asking a general question (no specific recipe requested) or explicitly wants prose, not a structured recipe.${bioContext}`,
|
||||
|
||||
@@ -43,7 +43,7 @@ export async function POST(req: NextRequest, { params }: Params) {
|
||||
if (!configResult.ok) return configResult.response;
|
||||
const aiConfig = configResult.data;
|
||||
|
||||
const result = await withAiQuota(session!.user.id, session!.user.tier as "free" | "pro" | "team", () =>
|
||||
const result = await withAiQuota(session!.user.id, session!.user.tier as "free" | "pro" | "family", () =>
|
||||
suggestDrinks(
|
||||
{
|
||||
title: recipe.title,
|
||||
|
||||
@@ -39,7 +39,7 @@ export async function POST(req: NextRequest) {
|
||||
if (!configResult.ok) return configResult.response;
|
||||
const aiConfig = configResult.data;
|
||||
|
||||
const result = await withAiQuota(session!.user.id, session!.user.tier as "free" | "pro" | "team", () =>
|
||||
const result = await withAiQuota(session!.user.id, session!.user.tier as "free" | "pro" | "family", () =>
|
||||
generateRecipe(parsed.data.title, {
|
||||
...aiConfig,
|
||||
userContext: privateBio ?? undefined,
|
||||
|
||||
@@ -36,7 +36,7 @@ export async function POST(req: NextRequest) {
|
||||
if (!configResult.ok) return configResult.response;
|
||||
const aiConfig = configResult.data;
|
||||
|
||||
const result = await withAiQuota(session!.user.id, session!.user.tier as "free" | "pro" | "team", () =>
|
||||
const result = await withAiQuota(session!.user.id, session!.user.tier as "free" | "pro" | "family", () =>
|
||||
generateRecipe(parsed.data.prompt, {
|
||||
...aiConfig,
|
||||
language: parsed.data.language,
|
||||
|
||||
@@ -42,7 +42,7 @@ export async function POST(req: NextRequest) {
|
||||
if (!textConfigResult.ok) return textConfigResult.response;
|
||||
const textConfig = textConfigResult.data;
|
||||
|
||||
const result = await withAiQuota(userId, session!.user.tier as "free" | "pro" | "team", () =>
|
||||
const result = await withAiQuota(userId, session!.user.tier as "free" | "pro" | "family", () =>
|
||||
importFromPhoto(parsed.data.imageBase64, parsed.data.mimeType, visionConfig, textConfig, locale),
|
||||
{ skipQuota: visionConfig.isByok && textConfig.isByok }
|
||||
);
|
||||
|
||||
@@ -37,7 +37,7 @@ export async function POST(req: NextRequest) {
|
||||
if (!configResult.ok) return configResult.response;
|
||||
const aiConfig = configResult.data;
|
||||
|
||||
const result = await withAiQuota(session!.user.id, session!.user.tier as "free" | "pro" | "team", () =>
|
||||
const result = await withAiQuota(session!.user.id, session!.user.tier as "free" | "pro" | "family", () =>
|
||||
importFromUrl(parsed.data.url, aiConfig), { skipQuota: aiConfig.isByok }
|
||||
);
|
||||
if (!result.ok) return result.response;
|
||||
|
||||
@@ -75,7 +75,7 @@ export async function POST(req: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
const result = await withAiQuota(userId, session!.user.tier as "free" | "pro" | "team", () =>
|
||||
const result = await withAiQuota(userId, session!.user.tier as "free" | "pro" | "family", () =>
|
||||
generateMealPlan(
|
||||
{
|
||||
dietaryPrefs: parsed.data.dietaryPrefs,
|
||||
@@ -99,7 +99,7 @@ export async function POST(req: NextRequest) {
|
||||
let chargedRecipes = 0;
|
||||
try {
|
||||
for (let i = 0; i < plan.entries.length; i++) {
|
||||
await checkAndIncrementTierLimit(userId, session!.user.tier as "free" | "pro" | "team", "recipe");
|
||||
await checkAndIncrementTierLimit(userId, session!.user.tier as "free" | "pro" | "family", "recipe");
|
||||
chargedRecipes++;
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
@@ -44,7 +44,7 @@ export async function POST(req: NextRequest, { params }: Params) {
|
||||
if (!configResult.ok) return configResult.response;
|
||||
const aiConfig = configResult.data;
|
||||
|
||||
const result = await withAiQuota(session!.user.id, session!.user.tier as "free" | "pro" | "team", () =>
|
||||
const result = await withAiQuota(session!.user.id, session!.user.tier as "free" | "pro" | "family", () =>
|
||||
suggestPairings(
|
||||
{
|
||||
title: recipe.title,
|
||||
|
||||
@@ -74,7 +74,7 @@ ${stepList || "None listed"}
|
||||
const locale = (session!.user as { locale?: string }).locale ?? "en";
|
||||
const lang = LANG[locale] ?? "English";
|
||||
|
||||
const result = await withAiQuota(session!.user.id, session!.user.tier as "free" | "pro" | "team", () =>
|
||||
const result = await withAiQuota(session!.user.id, session!.user.tier as "free" | "pro" | "family", () =>
|
||||
generateText({
|
||||
model,
|
||||
system: `You are Epicure, a helpful culinary assistant. Answer questions about the following recipe concisely and accurately. If asked who you are or what model/AI you're built on, say you're Epicure — never name the underlying model or provider. If a question is not related to the recipe or cooking, politely redirect. Keep answers under 200 words. Respond in ${lang}.
|
||||
|
||||
@@ -57,7 +57,7 @@ export async function POST(req: NextRequest) {
|
||||
? `${userContext}Generate 6 diverse recipe ideas based on: "${parsed.data.prompt}". Include a mix of difficulty levels.`
|
||||
: `${userContext}Generate 6 diverse, creative recipe ideas. Include different cuisines, difficulty levels, and meal types.`;
|
||||
|
||||
const result = await withAiQuota(session!.user.id, session!.user.tier as "free" | "pro" | "team", () =>
|
||||
const result = await withAiQuota(session!.user.id, session!.user.tier as "free" | "pro" | "family", () =>
|
||||
generateObject({
|
||||
model,
|
||||
schema: IdeasSchema,
|
||||
|
||||
@@ -47,7 +47,7 @@ export async function POST(req: NextRequest) {
|
||||
|
||||
const { instruction, language, ...current } = parsed.data;
|
||||
|
||||
const result = await withAiQuota(session!.user.id, session!.user.tier as "free" | "pro" | "team", () =>
|
||||
const result = await withAiQuota(session!.user.id, session!.user.tier as "free" | "pro" | "family", () =>
|
||||
regenerateRecipe(current, instruction, { ...aiConfig, language }), { skipQuota: aiConfig.isByok }
|
||||
);
|
||||
if (!result.ok) return result.response;
|
||||
|
||||
@@ -44,7 +44,7 @@ export async function POST(req: NextRequest) {
|
||||
if (!configResult.ok) return configResult.response;
|
||||
const aiConfig = configResult.data;
|
||||
|
||||
const result = await withAiQuota(session!.user.id, session!.user.tier as "free" | "pro" | "team", () =>
|
||||
const result = await withAiQuota(session!.user.id, session!.user.tier as "free" | "pro" | "family", () =>
|
||||
scaleRecipe(
|
||||
{
|
||||
title: recipe.title,
|
||||
|
||||
@@ -40,7 +40,7 @@ export async function POST(req: NextRequest) {
|
||||
|
||||
const locale = (session!.user as { locale?: string }).locale ?? "en";
|
||||
|
||||
const result = await withAiQuota(session!.user.id, session!.user.tier as "free" | "pro" | "team", () =>
|
||||
const result = await withAiQuota(session!.user.id, session!.user.tier as "free" | "pro" | "family", () =>
|
||||
substituteIngredient(parsed.data.ingredient, context, aiConfig, locale),
|
||||
{ skipQuota: aiConfig.isByok }
|
||||
);
|
||||
|
||||
@@ -42,7 +42,7 @@ export async function POST(req: NextRequest, { params }: Params) {
|
||||
if (!configResult.ok) return configResult.response;
|
||||
const aiConfig = configResult.data;
|
||||
|
||||
const result = await withAiQuota(session!.user.id, session!.user.tier as "free" | "pro" | "team", () =>
|
||||
const result = await withAiQuota(session!.user.id, session!.user.tier as "free" | "pro" | "family", () =>
|
||||
translateRecipe(
|
||||
{
|
||||
title: recipe.title,
|
||||
|
||||
@@ -47,7 +47,7 @@ export async function POST(req: NextRequest, { params }: Params) {
|
||||
if (!configResult.ok) return configResult.response;
|
||||
const aiConfig = configResult.data;
|
||||
|
||||
const result = await withAiQuota(session!.user.id, session!.user.tier as "free" | "pro" | "team", () =>
|
||||
const result = await withAiQuota(session!.user.id, session!.user.tier as "free" | "pro" | "family", () =>
|
||||
suggestVariations(
|
||||
{
|
||||
title: recipe.title,
|
||||
|
||||
Reference in New Issue
Block a user