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>
182 lines
4.9 KiB
JavaScript
182 lines
4.9 KiB
JavaScript
const Database = require("better-sqlite3");
|
|
const config = require("./config");
|
|
|
|
const db = new Database(config.DB_PATH);
|
|
|
|
// Enable WAL mode for better concurrent read performance
|
|
db.pragma("journal_mode = WAL");
|
|
|
|
// Create tables if they don't exist
|
|
db.exec(`
|
|
CREATE TABLE IF NOT EXISTS wallets (
|
|
disk_id TEXT PRIMARY KEY,
|
|
address TEXT NOT NULL,
|
|
private_key TEXT NOT NULL,
|
|
name TEXT,
|
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS contract_state (
|
|
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) {
|
|
const stmt = db.prepare("SELECT * FROM wallets WHERE disk_id = ?");
|
|
return stmt.get(diskId) || null;
|
|
}
|
|
|
|
function createWallet(diskId, address, privateKey, name) {
|
|
const stmt = db.prepare(
|
|
"INSERT INTO wallets (disk_id, address, private_key, name) VALUES (?, ?, ?, ?)"
|
|
);
|
|
return stmt.run(diskId, address, privateKey, name);
|
|
}
|
|
|
|
function getContractAddress() {
|
|
const stmt = db.prepare(
|
|
"SELECT value FROM contract_state WHERE key = 'contract_address'"
|
|
);
|
|
const row = stmt.get();
|
|
return row ? row.value : null;
|
|
}
|
|
|
|
function setContractAddress(address) {
|
|
const stmt = db.prepare(
|
|
"INSERT OR REPLACE INTO contract_state (key, value) VALUES ('contract_address', ?)"
|
|
);
|
|
return stmt.run(address);
|
|
}
|
|
|
|
function getAllWallets() {
|
|
const stmt = db.prepare("SELECT * FROM wallets");
|
|
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,
|
|
createWallet,
|
|
getContractAddress,
|
|
setContractAddress,
|
|
getAllWallets,
|
|
createPayment,
|
|
getPayment,
|
|
updatePayment,
|
|
getPaymentsByDiskId,
|
|
getPaymentByMpPaymentId,
|
|
createRedemption,
|
|
getRedemption,
|
|
getRedemptionsByDiskId,
|
|
getPendingRedemptions,
|
|
updateRedemption,
|
|
};
|