import type { RowDataPacket, ResultSetHeader } from 'mysql2' import { db, DB } from './db' /** * Códigos de promoción: se canjean por PD y/o PV. Cada código tiene usos totales * limitados (`max_uses`) y solo puede canjearse una vez por cuenta. Los códigos * son sensibles a mayúsculas/minúsculas (columna con collation utf8mb4_bin). */ export interface RedeemResult { success: boolean error?: string pd?: number pv?: number } export async function redeemPromoCode(accountId: number, rawCode: string): Promise { const code = rawCode.trim() if (!accountId) return { success: false, error: 'notAuthenticated' } if (!code) return { success: false, error: 'missingCode' } if (code.length > 64) return { success: false, error: 'notFound' } const conn = await db(DB.default).getConnection() try { await conn.beginTransaction() // Bloquea la fila del código (comparación binaria => sensible a may/min). const [rows] = await conn.query( 'SELECT id, pd, pv, max_uses, uses, active, expires_at FROM promocode WHERE code = ? COLLATE utf8mb4_bin LIMIT 1 FOR UPDATE', [code], ) const promo = rows[0] if (!promo || promo.active !== 1) { await conn.rollback() return { success: false, error: 'notFound' } } if (promo.expires_at && new Date(promo.expires_at).getTime() <= Date.now()) { await conn.rollback() return { success: false, error: 'expired' } } if (Number(promo.uses) >= Number(promo.max_uses)) { await conn.rollback() return { success: false, error: 'exhausted' } } // Registra el canje (la clave única promo+cuenta evita el doble uso). try { await conn.query('INSERT INTO promoredemption (promo_id, account_id, redeemed_at) VALUES (?, ?, NOW())', [ promo.id, accountId, ]) } catch (e) { await conn.rollback() if ((e as { code?: string }).code === 'ER_DUP_ENTRY') return { success: false, error: 'alreadyUsed' } return { success: false, error: 'genericError' } } // Consume un uso (condicional, por si hubo carrera con otro canje). const [upd] = await conn.query( 'UPDATE promocode SET uses = uses + 1 WHERE id = ? AND uses < max_uses', [promo.id], ) if (upd.affectedRows !== 1) { await conn.rollback() return { success: false, error: 'exhausted' } } // Acredita PD/PV a la cuenta (upsert). const pd = Number(promo.pd) const pv = Number(promo.pv) await conn.query( 'INSERT INTO api_points (accountID, vp, dp) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE vp = vp + VALUES(vp), dp = dp + VALUES(dp)', [accountId, pv, pd], ) await conn.commit() return { success: true, pd, pv } } catch { try { await conn.rollback() } catch { /* ignore */ } return { success: false, error: 'genericError' } } finally { conn.release() } }