tia/src/app/api/admin/seed-plan/route.ts
Mannu 6a1aaa38a2 feat(billing): Tasks 2-3 — config, plan seed, entitlement sync
Task 2:
- lib/billing/config.ts: reads 4 Razorpay env vars (throws at call time, not
  boot), premium grant constants (50GB / 6 members / 3 children / ₹199),
  razorpayAuthHeader() Basic-auth helper
- POST /api/admin/seed-plan: admin-only idempotent upsert of the Premium plan
  row from env + constants (GET shows current plans). Re-runnable, no DB shell

Task 3:
- lib/billing/entitlements.ts: grantPremium() / revokeToFree() sync onto
  families.tier + max_members + max_children. Existing quota.ts guards UNCHANGED
  — they already read these via isPaidFamily(). revoke = limit downgrade only,
  data untouched (freeze-not-demote)
- ENTITLED_STATUSES (active/authenticated/pending=grace) + TERMINAL_STATUSES

No guard refactor needed: chosen "sync to families.tier" approach means the
3 existing guards (storage/member/child) keep working as-is.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 12:14:47 +05:30

59 lines
2 KiB
TypeScript

import { NextResponse } from "next/server";
import { sql } from "@/db";
import { requireAdmin } from "@/lib/admin-auth";
import {
getRazorpayConfig,
PREMIUM_PLAN_NAME,
PREMIUM_PRICE_PAISE,
PREMIUM_STORAGE_BYTES,
PREMIUM_MEMBER_LIMIT,
PREMIUM_CHILD_LIMIT,
} from "@/lib/billing/config";
/**
* POST /api/admin/seed-plan — idempotent upsert of the Tia Premium plan row.
*
* Admin-only. Reads RAZORPAY_PLAN_ID + the premium grant constants and writes
* one subscription_plans row. Re-runnable: ON CONFLICT updates the grant values
* so you can tweak storage/member limits and re-seed without a DB shell.
*
* GET shows the current plan row(s) for verification.
*/
export async function GET(request: Request) {
const auth = await requireAdmin(request);
if (!auth.success) return NextResponse.json({ error: auth.error }, { status: auth.status });
const rows = await sql`SELECT * FROM subscription_plans ORDER BY created_at DESC`;
return NextResponse.json({ plans: rows });
}
export async function POST(request: Request) {
const auth = await requireAdmin(request);
if (!auth.success) return NextResponse.json({ error: auth.error }, { status: auth.status });
let cfg;
try {
cfg = getRazorpayConfig();
} catch (e) {
return NextResponse.json({ error: String(e) }, { status: 500 });
}
const rows = await sql`
INSERT INTO subscription_plans
(razorpay_plan_id, name, price_paise, storage_bytes, member_limit, child_limit, is_active)
VALUES (
${cfg.planId}, ${PREMIUM_PLAN_NAME}, ${PREMIUM_PRICE_PAISE},
${PREMIUM_STORAGE_BYTES}, ${PREMIUM_MEMBER_LIMIT}, ${PREMIUM_CHILD_LIMIT}, true
)
ON CONFLICT (razorpay_plan_id) DO UPDATE SET
name = EXCLUDED.name,
price_paise = EXCLUDED.price_paise,
storage_bytes = EXCLUDED.storage_bytes,
member_limit = EXCLUDED.member_limit,
child_limit = EXCLUDED.child_limit,
is_active = true
RETURNING *
`;
return NextResponse.json({ success: true, plan: rows[0] });
}