Files
democratic-money/src/app/api/spotlight/route.ts
2026-05-16 00:47:44 +00:00

110 lines
3.5 KiB
TypeScript

import { NextResponse } from "next/server";
import { z } from "zod";
import { auth } from "@/auth";
import { isAdminRole } from "@/lib/admin";
import { creditDisplayName } from "@/lib/credits-brand";
import { prisma } from "@/lib/prisma";
import { debitWalletCredits, INSUFFICIENT_CREDITS } from "@/lib/wallet-safety";
const ISSUE_CATALOG = [
{ slug: "voting-rights", title: "Voting Rights & Access" },
{ slug: "healthcare", title: "Affordable Healthcare" },
{ slug: "climate", title: "Climate & Clean Energy Jobs" },
{ slug: "education", title: "Public Education Funding" },
{ slug: "gun-safety", title: "Common-Sense Gun Safety" },
{ slug: "workers-rights", title: "Workers Rights & Wages" },
{ slug: "housing", title: "Affordable Housing" },
{ slug: "democracy-reform", title: "Campaign Finance Reform" },
] as const;
function currentWeekOf(): string {
const d = new Date();
const day = d.getUTCDay();
const diff = d.getUTCDate() - day + (day === 0 ? -6 : 1);
const mon = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), diff));
return mon.toISOString().split("T")[0];
}
const postSchema = z.object({
issueSlug: z.string().min(2).max(80),
creditsSpent: z.number().int().min(5).max(10000),
});
export async function GET() {
const weekOf = currentWeekOf();
const bids = await prisma.spotlightBid.groupBy({
by: ["issueSlug", "issueTitle"],
where: { weekOf },
_sum: { creditsSpent: true },
_count: true,
orderBy: { _sum: { creditsSpent: "desc" } },
});
const totals = bids.map((b) => ({
issueSlug: b.issueSlug,
issueTitle: b.issueTitle,
total: b._sum.creditsSpent ?? 0,
backers: b._count,
}));
const leader = totals[0] ?? null;
return NextResponse.json({ weekOf, totals, leader, catalog: ISSUE_CATALOG });
}
export async function POST(req: Request) {
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json({ error: "Sign in to bid on a Spotlight." }, { status: 401 });
}
const admin = isAdminRole(session.user.role);
const creditName = creditDisplayName();
try {
const json = await req.json();
const { issueSlug, creditsSpent } = postSchema.parse(json);
const issue = ISSUE_CATALOG.find((i) => i.slug === issueSlug);
if (!issue) return NextResponse.json({ error: "Unknown issue." }, { status: 400 });
const weekOf = currentWeekOf();
const spent = admin ? 0 : creditsSpent;
await prisma.$transaction(async (tx) => {
const bid = await tx.spotlightBid.create({
data: {
userId: session.user.id,
issueSlug: issue.slug,
issueTitle: issue.title,
creditsSpent: spent,
weekOf,
},
});
if (!admin) {
await debitWalletCredits(tx, session.user.id, creditsSpent);
await tx.ledgerEntry.create({
data: {
userId: session.user.id,
delta: -creditsSpent,
type: "DEBIT_SPOTLIGHT",
spotlightBidId: bid.id,
memo: `Spotlight bid: ${issue.title}`,
},
});
}
});
return NextResponse.json({ ok: true, issue, weekOf, spent });
} catch (e) {
if (e instanceof z.ZodError) return NextResponse.json({ error: "Invalid input", issues: e.issues }, { status: 400 });
if (e instanceof Error && e.message === INSUFFICIENT_CREDITS) {
return NextResponse.json({ error: `Not enough ${creditName}.` }, { status: 402 });
}
console.error(e);
return NextResponse.json({ error: "Could not place bid." }, { status: 500 });
}
}