import { NextRequest, NextResponse } from "next/server"; import { CREDIT_PRICE_CENTS, FREE_EXTRA_CREDITS_MAX, FREE_TOP_UP_MAX, FREE_TOP_UP_MIN, formatCreditPrice, validateFreeTopUpAmount, } from "@/lib/credits"; import { prisma } from "@/lib/db"; import { getSessionUser } from "@/lib/session"; import { checkoutTaxAndReferenceOptions, ensureStripeCustomer, priceDataTaxFields, productDataWithTaxCode, } from "@/lib/stripe-checkout"; import { getStripe, isBillingDevMock, isStripeConfigured } from "@/lib/stripe"; import { grantExtraCredits } from "@/lib/billing"; export async function GET() { const user = await getSessionUser(); if (!user) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } const dbUser = await prisma.user.findUniqueOrThrow({ where: { id: user.id }, select: { extraCredits: true, plan: true, monthlyCredits: true, videosUsed: true, }, }); const purchases = await prisma.creditPurchase.findMany({ where: { userId: user.id, status: "COMPLETED" }, orderBy: { completedAt: "desc" }, take: 10, }); const cap = FREE_EXTRA_CREDITS_MAX; const room = Math.max(0, cap - dbUser.extraCredits); return NextResponse.json({ extraCredits: dbUser.extraCredits, videoCredits: dbUser.extraCredits, topUpMin: FREE_TOP_UP_MIN, topUpMax: FREE_TOP_UP_MAX, priceCentsPerCredit: CREDIT_PRICE_CENTS, priceLabelPerCredit: formatCreditPrice(1), creditBalanceMax: cap, monthlyCredits: dbUser.monthlyCredits, creditsUsed: dbUser.videosUsed, plan: dbUser.plan, stripeConfigured: isStripeConfigured() || isBillingDevMock(), room, atExtraCap: room === 0, purchases: purchases.map((p) => ({ id: p.id, credits: p.credits, amountCents: p.amountCents, completedAt: p.completedAt?.toISOString() ?? null, })), }); } /** Free tier: top up 1–15 extra credits (price = credits × €0.25). */ export async function POST(req: NextRequest) { const user = await getSessionUser(); if (!user) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } if (user.plan === "PREMIUM") { return NextResponse.json( { error: "Credit top-ups are for the Free plan. Pro includes 50 monthly credits; existing extras never expire.", }, { status: 403 }, ); } let credits = 0; try { const body = await req.json(); credits = Math.floor(Number(body.credits)); } catch { return NextResponse.json({ error: "Invalid request body" }, { status: 400 }); } const dbUser = await prisma.user.findUniqueOrThrow({ where: { id: user.id }, select: { extraCredits: true, email: true, monthlyCredits: true, videosUsed: true, bonusQuota: true }, }); const monthlyRemaining = Math.max( 0, dbUser.monthlyCredits + dbUser.bonusQuota - dbUser.videosUsed, ); const validation = validateFreeTopUpAmount(credits, dbUser.extraCredits, monthlyRemaining); if (!validation.ok) { return NextResponse.json({ error: validation.error }, { status: 400 }); } const { credits: amount, amountCents } = validation; if (isBillingDevMock()) { const purchase = await prisma.creditPurchase.create({ data: { userId: user.id, credits: amount, amountCents, status: "COMPLETED", completedAt: new Date(), stripeSessionId: `dev_topup_${Date.now()}`, }, }); await grantExtraCredits(user.id, amount); return NextResponse.json({ mocked: true, granted: amount, amountCents, purchaseId: purchase.id, }); } if (!isStripeConfigured()) { return NextResponse.json( { error: "Payments are not configured yet. Set STRIPE_SECRET_KEY." }, { status: 503 }, ); } const origin = process.env.NEXTAUTH_URL || req.nextUrl.origin; const purchase = await prisma.creditPurchase.create({ data: { userId: user.id, credits: amount, amountCents, status: "PENDING", }, }); try { const stripe = getStripe(); const customerId = await ensureStripeCustomer(user.id, dbUser.email); const session = await stripe.checkout.sessions.create({ mode: "payment", customer: customerId, line_items: [ { quantity: amount, price_data: { currency: "eur", unit_amount: CREDIT_PRICE_CENTS, ...priceDataTaxFields(), product_data: productDataWithTaxCode( "Songs2VID video credit", `1 credit = 1 video upload (${formatCreditPrice(1)} each)`, ), }, }, ], metadata: { type: "free_top_up", userId: user.id, purchaseId: purchase.id, credits: String(amount), }, ...checkoutTaxAndReferenceOptions(user.id), success_url: `${origin}/dashboard/settings?credits=success`, cancel_url: `${origin}/dashboard/settings?credits=cancelled`, }); await prisma.creditPurchase.update({ where: { id: purchase.id }, data: { stripeSessionId: session.id }, }); return NextResponse.json({ url: session.url, sessionId: session.id }); } catch (err) { await prisma.creditPurchase.update({ where: { id: purchase.id }, data: { status: "FAILED" }, }); const message = err instanceof Error ? err.message : "Checkout failed"; return NextResponse.json({ error: message }, { status: 500 }); } }