feat: add AFC Store with MercadoPago purchases and prize redemption
Some checks failed
Deploy / deploy (push) Has been cancelled

Players can now buy AfterCoin with real money (MercadoPago Checkout Pro,
$15 MXN/AFC) and redeem AFC for gift cards or cash withdrawals. Admin
fulfills redemptions manually.

- Bridge: payments + redemptions tables, CRUD routes, PATCH auth
- Next.js API: verify-disk, balance, create-preference, webhook (idempotent
  minting with HMAC signature verification), redeem, payment/redemption history
- Frontend: hub, buy flow (4 packages + custom), redeem flow (gift cards +
  cash out), success/failure/pending pages, history with tabs, 8 components
- i18n: full English + Spanish translations
- Infra: nginx /api/afc/ → Next.js, docker-compose env vars, .env.example

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
consultoria-as
2026-02-26 02:26:13 +00:00
parent 7dc1d2e0e5
commit a76d513659
38 changed files with 2142 additions and 5 deletions

View File

@@ -0,0 +1,71 @@
import { NextRequest, NextResponse } from "next/server";
import { randomUUID } from "crypto";
import { preferenceClient } from "../lib/mercadopago";
import { bridgePost, bridgePatch } from "../lib/bridge";
const PRICE_MXN = Number(process.env.AFC_PRICE_MXN) || 15;
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL || "http://localhost:3000";
export async function POST(req: NextRequest) {
try {
const { diskId, amountAfc } = await req.json();
if (!diskId || !amountAfc || amountAfc < 1) {
return NextResponse.json(
{ error: "diskId and amountAfc (>=1) required" },
{ status: 400 }
);
}
const amountMxn = amountAfc * PRICE_MXN;
const paymentId = randomUUID();
// Create payment record in bridge
await bridgePost("/api/payments", {
id: paymentId,
diskId,
amountAfc,
amountMxn,
});
// Create MercadoPago preference
const preference = await preferenceClient.create({
body: {
items: [
{
id: paymentId,
title: `${amountAfc} AfterCoin (AFC)`,
quantity: 1,
unit_price: amountMxn,
currency_id: "MXN",
},
],
external_reference: paymentId,
back_urls: {
success: `${BASE_URL}/afc/buy/success?payment_id=${paymentId}`,
failure: `${BASE_URL}/afc/buy/failure?payment_id=${paymentId}`,
pending: `${BASE_URL}/afc/buy/pending?payment_id=${paymentId}`,
},
auto_return: "approved",
notification_url:
process.env.MERCADOPAGO_WEBHOOK_URL ||
`${BASE_URL}/api/afc/webhook`,
},
});
// Store the MP preference ID
await bridgePatch(`/api/payments/${paymentId}`, {
mp_preference_id: preference.id,
});
return NextResponse.json({
paymentId,
initPoint: preference.init_point,
sandboxInitPoint: preference.sandbox_init_point,
});
} catch (e: unknown) {
const message = e instanceof Error ? e.message : "Unknown error";
console.error("create-preference error:", e);
return NextResponse.json({ error: message }, { status: 500 });
}
}