91 lines
2.9 KiB
TypeScript
91 lines
2.9 KiB
TypeScript
/**
|
|
* Client-triggered claim: user polls their invoice status and calls this
|
|
* when it shows "Settled". Server re-verifies with BTCPay before crediting.
|
|
*
|
|
* POST /api/credits/claim { invoiceId, handle }
|
|
*/
|
|
|
|
import { NextResponse } from "next/server";
|
|
import { getBtcPayInvoiceStatus } from "@/lib/btcpay";
|
|
import {
|
|
creditInvoice,
|
|
getInvoiceHandle,
|
|
isInvoiceClaimed,
|
|
satsToVoid,
|
|
} from "@/lib/serverLedger";
|
|
|
|
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 { invoiceId, handle } = (body as Record<string, unknown>) ?? {};
|
|
|
|
if (typeof invoiceId !== "string" || !invoiceId.trim()) {
|
|
return NextResponse.json({ ok: false, error: "invoiceId is required" }, { status: 400 });
|
|
}
|
|
if (typeof handle !== "string" || !handle.trim()) {
|
|
return NextResponse.json({ ok: false, error: "handle is required" }, { status: 400 });
|
|
}
|
|
|
|
const cleanInvoice = invoiceId.trim();
|
|
const cleanHandle = handle.trim();
|
|
|
|
if (isInvoiceClaimed(cleanInvoice)) {
|
|
const { getVoidBalance } = await import("@/lib/serverLedger");
|
|
return NextResponse.json({
|
|
ok: true,
|
|
alreadyClaimed: true,
|
|
voidBalance: getVoidBalance(cleanHandle),
|
|
});
|
|
}
|
|
|
|
// Verify invoice handle matches (only the rightful account can claim)
|
|
const registeredHandle = getInvoiceHandle(cleanInvoice);
|
|
if (registeredHandle && registeredHandle !== cleanHandle.toLowerCase()) {
|
|
return NextResponse.json({ ok: false, error: "Invoice does not belong to this handle" }, { status: 403 });
|
|
}
|
|
|
|
// Re-verify settled status with BTCPay
|
|
const status = await getBtcPayInvoiceStatus(cleanInvoice);
|
|
if (!status.ok) {
|
|
return NextResponse.json({ ok: false, error: status.error }, { status: 502 });
|
|
}
|
|
if (status.status !== "Settled") {
|
|
return NextResponse.json({
|
|
ok: false,
|
|
error: `Invoice not settled yet (status: ${status.status})`,
|
|
status: status.status,
|
|
});
|
|
}
|
|
|
|
// Convert USD → VOID credits
|
|
let voidToCredit = 0;
|
|
try {
|
|
const priceRes = await fetch(
|
|
"https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd",
|
|
{ next: { revalidate: 60 } },
|
|
);
|
|
const priceJson = (await priceRes.json()) as { bitcoin?: { usd?: number } };
|
|
const btcUsd = priceJson.bitcoin?.usd ?? 0;
|
|
if (btcUsd > 0) {
|
|
const sats = Math.floor((status.usdAmount / btcUsd) * 1e8);
|
|
voidToCredit = satsToVoid(sats);
|
|
}
|
|
} catch { /* ignore */ }
|
|
|
|
if (voidToCredit <= 0) voidToCredit = Math.max(1, Math.floor(status.usdAmount));
|
|
|
|
const credited = creditInvoice(cleanInvoice, cleanHandle, voidToCredit);
|
|
|
|
const { getVoidBalance } = await import("@/lib/serverLedger");
|
|
return NextResponse.json({
|
|
ok: true,
|
|
credited,
|
|
voidCredited: voidToCredit,
|
|
voidBalance: getVoidBalance(cleanHandle),
|
|
usdAmount: status.usdAmount,
|
|
});
|
|
}
|