import { NextResponse } from "next/server"; import { sql } from "@/db"; import { requireFamily } from "@/lib/auth"; import { getRazorpayConfig, razorpayAuthHeader, RAZORPAY_API_BASE } from "@/lib/billing/config"; /** * POST /api/subscriptions/create * * Creates a Razorpay subscription for the caller's family and returns the * subscription_id + key_id for Razorpay Checkout. Grants NOTHING — entitlement * is applied only by the webhook (subscription.charged/activated). * * Security: * - family_id comes from the session (requireFamily), never from the request * body — so a user can only create a sub for their own family (IDOR-safe). * - key_secret never leaves the server; only key_id is returned. */ export async function POST() { const auth = await requireFamily(); if (!auth.success) return NextResponse.json({ error: auth.error }, { status: auth.status }); const familyId = auth.session!.familyId!; const userId = auth.session!.userId; let cfg; try { cfg = getRazorpayConfig(); } catch (e) { return NextResponse.json({ error: String(e) }, { status: 500 }); } // Resolve the plan row (must be seeded — see /api/admin/seed-plan). const planRows = await sql` SELECT id, razorpay_plan_id FROM subscription_plans WHERE razorpay_plan_id = ${cfg.planId} AND is_active = true LIMIT 1 `; if (!planRows[0]) { return NextResponse.json( { error: "Plan not seeded. Run POST /api/admin/seed-plan first." }, { status: 500 }, ); } const planRowId = planRows[0].id as string; // Inspect any existing non-terminal subscription for this family. const live = await sql` SELECT id, razorpay_subscription_id, status FROM family_subscriptions WHERE family_id = ${familyId} AND status IN ('created','authenticated','active','pending','halted') ORDER BY created_at DESC LIMIT 1 `; if (live[0]) { const status = live[0].status as string; // Genuinely subscribed → block (they don't need a second subscription). if (status === "authenticated" || status === "active" || status === "pending") { return NextResponse.json( { error: "You already have an active subscription.", status }, { status: 409 }, ); } // Abandoned checkout: a 'created' row whose payment never completed. The // Razorpay subscription is still payable, so REUSE it — reopen checkout for // the same sub instead of locking the user out forever. if (status === "created") { return NextResponse.json({ subscriptionId: live[0].razorpay_subscription_id, keyId: cfg.keyId, reused: true, }); } // 'halted' = retries exhausted, family already downgraded to free. Let them // re-subscribe: retire the halted row (leaves the partial unique index) so // a fresh subscription can be created below. if (status === "halted") { await sql` UPDATE family_subscriptions SET status = 'expired', ended_at = COALESCE(ended_at, NOW()), updated_at = NOW() WHERE id = ${live[0].id} `; } } // Create the subscription at Razorpay. let rzpSub: { id?: string; status?: string; error?: { description?: string } }; try { const res = await fetch(`${RAZORPAY_API_BASE}/subscriptions`, { method: "POST", headers: { Authorization: razorpayAuthHeader(cfg), "Content-Type": "application/json", }, body: JSON.stringify({ plan_id: cfg.planId, total_count: 120, // ~10 yrs of monthly cycles; avoids auto-complete customer_notify: 1, quantity: 1, notes: { family_id: familyId, user_id: userId }, // correlation backup for webhook }), }); rzpSub = await res.json(); if (!res.ok || !rzpSub.id) { console.error("Razorpay create sub failed:", rzpSub); return NextResponse.json( { error: rzpSub?.error?.description || "Failed to create subscription" }, { status: 502 }, ); } } catch (e) { console.error("Razorpay create sub error:", e); return NextResponse.json({ error: "Failed to reach payment provider" }, { status: 502 }); } // Record our side as 'created'. The webhook drives all later state. try { await sql` INSERT INTO family_subscriptions (family_id, plan_id, razorpay_subscription_id, status) VALUES (${familyId}, ${planRowId}, ${rzpSub.id}, 'created') `; } catch (e) { // Unique index race (double-click) — treat as the existing-sub case. console.error("Insert family_subscriptions failed:", e); return NextResponse.json( { error: "Subscription already in progress for this family." }, { status: 409 }, ); } return NextResponse.json({ subscriptionId: rzpSub.id, keyId: cfg.keyId }); }