import type { RowDataPacket } from 'mysql2' import { db, DB } from './db' /** Ratio de conversión: cada 1 unidad de moneda = 100 PD. */ export const PD_PER_UNIT = 100 /** Límites de compra por transacción (en unidades de moneda). */ export const DP_MIN_AMOUNT = 1 export const DP_MAX_AMOUNT = 200 /** Acredita PD (donate points) a una cuenta de juego en `api_points` (upsert). */ export async function creditDPoints(accountId: number, points: number): Promise { if (!accountId || !(points > 0)) return false try { const [rows] = await db(DB.default).query( 'SELECT id FROM api_points WHERE accountID = ?', [accountId], ) if (rows[0]) { await db(DB.default).query('UPDATE api_points SET dp = dp + ? WHERE accountID = ?', [points, accountId]) } else { await db(DB.default).query('INSERT INTO api_points (accountID, vp, dp) VALUES (?, 0, ?)', [accountId, points]) } return true } catch { return false } } /** Saldo de PD (donate points) de una cuenta de juego. 0 si no tiene fila. */ export async function getDPointsBalance(accountId: number): Promise { if (!accountId) return 0 try { const [rows] = await db(DB.default).query( 'SELECT dp FROM api_points WHERE accountID = ?', [accountId], ) return rows[0] ? Number(rows[0].dp) : 0 } catch { return 0 } } /** Saldo de PV (vote points, ganados al votar) de una cuenta. 0 si no tiene fila. */ export async function getVPointsBalance(accountId: number): Promise { if (!accountId) return 0 try { const [rows] = await db(DB.default).query( 'SELECT vp FROM api_points WHERE accountID = ?', [accountId], ) return rows[0] ? Number(rows[0].vp) : 0 } catch { return 0 } } /** Acredita PV (vote points) a una cuenta (upsert). Se usa para reembolsos. */ export async function creditVPoints(accountId: number, points: number): Promise { if (!accountId || !(points > 0)) return false try { await db(DB.default).query( 'INSERT INTO api_points (accountID, vp, dp) VALUES (?, ?, 0) ON DUPLICATE KEY UPDATE vp = vp + ?', [accountId, points, points], ) return true } catch { return false } } /** * Descuenta `points` PV del saldo de una cuenta de forma atómica (bloqueo de * fila). Devuelve `insufficientFunds` si no hay saldo suficiente. Si la acción * posterior falla, reembolsa con `creditVPoints`. */ export async function spendVPoints( accountId: number, points: number, ): Promise<{ success: boolean; error?: string }> { if (!accountId || !Number.isInteger(points) || points <= 0) { return { success: false, error: 'invalidRequest' } } const conn = await db(DB.default).getConnection() try { await conn.beginTransaction() const [src] = await conn.query( 'SELECT vp FROM api_points WHERE accountID = ? FOR UPDATE', [accountId], ) const balance = src[0] ? Number(src[0].vp) : 0 if (balance < points) { await conn.rollback() return { success: false, error: 'insufficientFunds' } } await conn.query('UPDATE api_points SET vp = vp - ? WHERE accountID = ?', [points, accountId]) await conn.commit() return { success: true } } catch { try { await conn.rollback() } catch { /* ignore */ } return { success: false, error: 'genericError' } } finally { conn.release() } } /** * Transfiere `points` PD desde una cuenta de origen a una de destino de forma * atómica (transacción con bloqueo de fila del origen). Es irreversible. */ export async function transferDPoints( fromAccountId: number, toAccountId: number, points: number, ): Promise<{ success: boolean; error?: string }> { if (!fromAccountId || !toAccountId || !Number.isInteger(points) || points <= 0) { return { success: false, error: 'invalidRequest' } } if (fromAccountId === toAccountId) return { success: false, error: 'sameAccount' } const conn = await db(DB.default).getConnection() try { await conn.beginTransaction() // Bloquea la fila del origen para evitar dobles gastos concurrentes. const [src] = await conn.query( 'SELECT dp FROM api_points WHERE accountID = ? FOR UPDATE', [fromAccountId], ) const balance = src[0] ? Number(src[0].dp) : 0 if (balance < points) { await conn.rollback() return { success: false, error: 'insufficientFunds' } } await conn.query('UPDATE api_points SET dp = dp - ? WHERE accountID = ?', [points, fromAccountId]) await conn.query( 'INSERT INTO api_points (accountID, vp, dp) VALUES (?, 0, ?) ON DUPLICATE KEY UPDATE dp = dp + ?', [toAccountId, points, points], ) await conn.commit() return { success: true } } catch { try { await conn.rollback() } catch { /* ignore */ } return { success: false, error: 'genericError' } } finally { conn.release() } } /** * Descuenta `points` PD del saldo de una cuenta de forma atómica (bloqueo de * fila para evitar dobles gastos concurrentes). Se usa para pagar servicios con * el saldo PD. Devuelve `insufficientFunds` si no hay saldo suficiente. * Si la acción posterior falla, reembolsa con `creditDPoints`. */ export async function spendDPoints( accountId: number, points: number, ): Promise<{ success: boolean; error?: string }> { if (!accountId || !Number.isInteger(points) || points <= 0) { return { success: false, error: 'invalidRequest' } } const conn = await db(DB.default).getConnection() try { await conn.beginTransaction() const [src] = await conn.query( 'SELECT dp FROM api_points WHERE accountID = ? FOR UPDATE', [accountId], ) const balance = src[0] ? Number(src[0].dp) : 0 if (balance < points) { await conn.rollback() return { success: false, error: 'insufficientFunds' } } await conn.query('UPDATE api_points SET dp = dp - ? WHERE accountID = ?', [points, accountId]) await conn.commit() return { success: true } } catch { try { await conn.rollback() } catch { /* ignore */ } return { success: false, error: 'genericError' } } finally { conn.release() } }