92 lines
3.3 KiB
TypeScript
92 lines
3.3 KiB
TypeScript
/**
|
||
* POST /api/raffle/buy { handle, quantity }
|
||
* Deducts VOID credits (1 VOID = 1 ticket ≈ $1) and registers raffle entries.
|
||
*
|
||
* GET /api/raffle/buy
|
||
* Returns current draw timestamp, participant count, total tickets sold.
|
||
*/
|
||
|
||
import { NextResponse } from "next/server";
|
||
import fs from "fs";
|
||
import path from "path";
|
||
import os from "os";
|
||
import { deductVoid, getVoidBalance } from "@/lib/serverLedger";
|
||
|
||
const TICKET_COST_VOID = 1;
|
||
const MAX_PER_BUY = 100;
|
||
|
||
export type RaffleEntry = { handle: string; tickets: number; boughtAt: number };
|
||
export type RaffleData = { v: 1; entries: RaffleEntry[]; drawAt: number };
|
||
|
||
const VAR_DIR = path.join(process.cwd(), "var");
|
||
export const RAFFLE_PATH = path.join(VAR_DIR, "raffle.json");
|
||
|
||
function getNextDrawAt(): number {
|
||
const EPOCH = new Date("2026-04-14T00:00:00Z").getTime();
|
||
const INTERVAL = 7 * 24 * 60 * 60 * 1000;
|
||
const now = Date.now();
|
||
return EPOCH + (Math.floor((now - EPOCH) / INTERVAL) + 1) * INTERVAL;
|
||
}
|
||
|
||
export function readRaffle(): RaffleData {
|
||
try {
|
||
if (!fs.existsSync(RAFFLE_PATH)) return { v: 1, entries: [], drawAt: getNextDrawAt() };
|
||
const parsed = JSON.parse(fs.readFileSync(RAFFLE_PATH, "utf8")) as RaffleData;
|
||
if (parsed?.v !== 1) return { v: 1, entries: [], drawAt: getNextDrawAt() };
|
||
if (parsed.drawAt < Date.now()) parsed.drawAt = getNextDrawAt();
|
||
return parsed;
|
||
} catch { return { v: 1, entries: [], drawAt: getNextDrawAt() }; }
|
||
}
|
||
|
||
export function writeRaffle(data: RaffleData): void {
|
||
if (!fs.existsSync(VAR_DIR)) fs.mkdirSync(VAR_DIR, { recursive: true });
|
||
const tmp = path.join(os.tmpdir(), `cyberlux-raffle-${Date.now()}.json`);
|
||
fs.writeFileSync(tmp, JSON.stringify(data, null, 2), "utf8");
|
||
fs.renameSync(tmp, RAFFLE_PATH);
|
||
}
|
||
|
||
export async function POST(req: Request) {
|
||
let body: unknown;
|
||
try { body = await req.json(); } catch {
|
||
return NextResponse.json({ ok: false, error: "Invalid JSON" }, { status: 400 });
|
||
}
|
||
|
||
const { handle, quantity } = (body as Record<string, unknown>) ?? {};
|
||
if (typeof handle !== "string" || !handle.trim())
|
||
return NextResponse.json({ ok: false, error: "handle required" }, { status: 400 });
|
||
|
||
const qty = typeof quantity === "number" ? Math.floor(quantity) : 1;
|
||
if (qty < 1 || qty > MAX_PER_BUY)
|
||
return NextResponse.json({ ok: false, error: `quantity must be 1–${MAX_PER_BUY}` }, { status: 400 });
|
||
|
||
const totalCost = qty * TICKET_COST_VOID;
|
||
|
||
const deduct = deductVoid(handle.trim(), totalCost, "raffle tickets");
|
||
if (!deduct.ok) return NextResponse.json({ ok: false, error: deduct.error }, { status: 402 });
|
||
|
||
const raffle = readRaffle();
|
||
const hk = handle.trim().toLowerCase();
|
||
const existing = raffle.entries.find((e) => e.handle === hk);
|
||
if (existing) { existing.tickets += qty; existing.boughtAt = Date.now(); }
|
||
else raffle.entries.push({ handle: hk, tickets: qty, boughtAt: Date.now() });
|
||
writeRaffle(raffle);
|
||
|
||
return NextResponse.json({
|
||
ok: true,
|
||
ticketsBought: qty,
|
||
totalTickets: raffle.entries.find((e) => e.handle === hk)?.tickets ?? qty,
|
||
voidBalance: deduct.newBalance,
|
||
drawAt: raffle.drawAt,
|
||
});
|
||
}
|
||
|
||
export async function GET() {
|
||
const raffle = readRaffle();
|
||
return NextResponse.json({
|
||
ok: true,
|
||
drawAt: raffle.drawAt,
|
||
totalTickets: raffle.entries.reduce((s, e) => s + e.tickets, 0),
|
||
participants: raffle.entries.length,
|
||
});
|
||
}
|