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

@@ -20,6 +20,33 @@ db.exec(`
key TEXT PRIMARY KEY,
value TEXT
);
CREATE TABLE IF NOT EXISTS payments (
id TEXT PRIMARY KEY,
disk_id TEXT NOT NULL,
amount_afc INTEGER NOT NULL,
amount_mxn REAL NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
mp_preference_id TEXT,
mp_payment_id TEXT,
tx_hash TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS redemptions (
id TEXT PRIMARY KEY,
disk_id TEXT NOT NULL,
amount_afc INTEGER NOT NULL,
prize_type TEXT NOT NULL,
prize_detail TEXT NOT NULL,
delivery_info TEXT,
status TEXT NOT NULL DEFAULT 'pending',
burn_tx_hash TEXT,
admin_notes TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
);
`);
function getWallet(diskId) {
@@ -54,6 +81,86 @@ function getAllWallets() {
return stmt.all();
}
// Payments
function createPayment(id, diskId, amountAfc, amountMxn) {
const stmt = db.prepare(
"INSERT INTO payments (id, disk_id, amount_afc, amount_mxn) VALUES (?, ?, ?, ?)"
);
return stmt.run(id, diskId, amountAfc, amountMxn);
}
function getPayment(id) {
const stmt = db.prepare("SELECT * FROM payments WHERE id = ?");
return stmt.get(id) || null;
}
function updatePayment(id, fields) {
const allowed = ["status", "mp_preference_id", "mp_payment_id", "tx_hash"];
const sets = [];
const values = [];
for (const key of allowed) {
if (fields[key] !== undefined) {
sets.push(`${key} = ?`);
values.push(fields[key]);
}
}
if (sets.length === 0) return null;
sets.push("updated_at = CURRENT_TIMESTAMP");
values.push(id);
const stmt = db.prepare(`UPDATE payments SET ${sets.join(", ")} WHERE id = ?`);
return stmt.run(...values);
}
function getPaymentsByDiskId(diskId) {
const stmt = db.prepare("SELECT * FROM payments WHERE disk_id = ? ORDER BY created_at DESC");
return stmt.all(diskId);
}
function getPaymentByMpPaymentId(mpPaymentId) {
const stmt = db.prepare("SELECT * FROM payments WHERE mp_payment_id = ?");
return stmt.get(mpPaymentId) || null;
}
// Redemptions
function createRedemption(id, diskId, amountAfc, prizeType, prizeDetail, deliveryInfo, burnTxHash) {
const stmt = db.prepare(
"INSERT INTO redemptions (id, disk_id, amount_afc, prize_type, prize_detail, delivery_info, burn_tx_hash) VALUES (?, ?, ?, ?, ?, ?, ?)"
);
return stmt.run(id, diskId, amountAfc, prizeType, prizeDetail, deliveryInfo, burnTxHash);
}
function getRedemption(id) {
const stmt = db.prepare("SELECT * FROM redemptions WHERE id = ?");
return stmt.get(id) || null;
}
function getRedemptionsByDiskId(diskId) {
const stmt = db.prepare("SELECT * FROM redemptions WHERE disk_id = ? ORDER BY created_at DESC");
return stmt.all(diskId);
}
function getPendingRedemptions() {
const stmt = db.prepare("SELECT * FROM redemptions WHERE status = 'pending' ORDER BY created_at ASC");
return stmt.all();
}
function updateRedemption(id, fields) {
const allowed = ["status", "admin_notes"];
const sets = [];
const values = [];
for (const key of allowed) {
if (fields[key] !== undefined) {
sets.push(`${key} = ?`);
values.push(fields[key]);
}
}
if (sets.length === 0) return null;
sets.push("updated_at = CURRENT_TIMESTAMP");
values.push(id);
const stmt = db.prepare(`UPDATE redemptions SET ${sets.join(", ")} WHERE id = ?`);
return stmt.run(...values);
}
module.exports = {
db,
getWallet,
@@ -61,4 +168,14 @@ module.exports = {
getContractAddress,
setContractAddress,
getAllWallets,
createPayment,
getPayment,
updatePayment,
getPaymentsByDiskId,
getPaymentByMpPaymentId,
createRedemption,
getRedemption,
getRedemptionsByDiskId,
getPendingRedemptions,
updateRedemption,
};