import { NextResponse } from "next/server" import { createSupabaseServerClient } from "@/lib/supabase/server" import { createSupabaseAdminClient } from "@/lib/supabase/admin" import { getStripe } from "@/lib/stripe" import { rateLimit } from "@/lib/rate-limit" import { checkBotId } from "botid/server" export async function POST(request: Request) { const botVerification = await checkBotId() if (botVerification.isBot) { return NextResponse.json({ error: "access denied" }, { status: 403 }) } const supabaseClient = await createSupabaseServerClient() const { data: { user }, } = await supabaseClient.auth.getUser() if (!user) { return NextResponse.json({ error: "not authenticated" }, { status: 401 }) } const rateLimitResult = await rateLimit(`checkout:${user.id}`, 10, 60_000) if (!rateLimitResult.success) { return NextResponse.json({ error: "too many requests" }, { status: 429 }) } const body = await request.json().catch(() => ({})) const billingInterval = body.billingInterval === "yearly" ? "yearly" : "monthly" const targetTier = body.targetTier === "developer" ? "developer" : "pro" const priceIdentifierMap: Record = { "pro:monthly": process.env.STRIPE_PRO_MONTHLY_PRICE_IDENTIFIER, "pro:yearly": process.env.STRIPE_PRO_YEARLY_PRICE_IDENTIFIER, "developer:monthly": process.env.STRIPE_DEVELOPER_MONTHLY_PRICE_IDENTIFIER, "developer:yearly": process.env.STRIPE_DEVELOPER_YEARLY_PRICE_IDENTIFIER, } const stripePriceIdentifier = priceIdentifierMap[`${targetTier}:${billingInterval}`] if (!stripePriceIdentifier) { return NextResponse.json( { error: "invalid plan configuration" }, { status: 500 } ) } const { data: profile, error: profileError } = await supabaseClient .from("user_profiles") .select("tier, stripe_customer_identifier, stripe_subscription_identifier") .eq("id", user.id) .single() if (profileError || !profile) { return NextResponse.json( { error: "failed to load profile" }, { status: 500 } ) } const tierRank: Record = { free: 0, pro: 1, developer: 2 } const currentRank = tierRank[profile.tier] ?? 0 const targetRank = tierRank[targetTier] ?? 0 if (currentRank >= targetRank) { return NextResponse.json( { error: `already on ${profile.tier} plan` }, { status: 400 } ) } if (profile.stripe_subscription_identifier && currentRank > 0) { const subscription = await getStripe().subscriptions.retrieve( profile.stripe_subscription_identifier ) const existingItemIdentifier = subscription.items.data[0]?.id if (!existingItemIdentifier) { return NextResponse.json( { error: "could not find existing subscription item" }, { status: 500 } ) } await getStripe().subscriptions.update( profile.stripe_subscription_identifier, { items: [ { id: existingItemIdentifier, price: stripePriceIdentifier, }, ], proration_behavior: "always_invoice", metadata: { supabase_user_identifier: user.id }, } ) return NextResponse.json({ upgraded: true }) } let stripeCustomerIdentifier = profile.stripe_customer_identifier if (!stripeCustomerIdentifier) { const customer = await getStripe().customers.create({ email: user.email, metadata: { supabase_user_identifier: user.id }, }) stripeCustomerIdentifier = customer.id const adminClient = createSupabaseAdminClient() const { error: updateError } = await adminClient .from("user_profiles") .update({ stripe_customer_identifier: stripeCustomerIdentifier }) .eq("id", user.id) if (updateError) { console.error("failed to save stripe customer identifier:", updateError) return NextResponse.json( { error: "failed to save customer" }, { status: 500 } ) } } const origin = process.env.NEXT_PUBLIC_APP_URL?.replace(/\/$/, "") || new URL(request.url).origin const checkoutSession = await getStripe().checkout.sessions.create({ customer: stripeCustomerIdentifier, mode: "subscription", line_items: [ { price: stripePriceIdentifier, quantity: 1, }, ], success_url: `${origin}/reader/settings?billing=success`, cancel_url: `${origin}/reader/settings?billing=cancelled`, subscription_data: { metadata: { supabase_user_identifier: user.id }, }, client_reference_id: user.id, allow_promotion_codes: true, }) return NextResponse.json({ url: checkoutSession.url }) }