web-next: herramientas de PD (transferir, comercio, códigos de promoción)

- /transfer-d-points: transferir PD a la cuenta dueña de un personaje
  (requiere token de seguridad; movimiento atómico con FOR UPDATE).
- /trade-points: comercio PD<->oro mediante códigos de 1 solo uso
  (contraseña + token + Turnstile; oro al creador por SOAP; expira 1h;
  lock de 5s tras canjear; rollback total si falla la entrega).
- /promo-code: canje de códigos por PD/PV (sensibles a mayúsculas/minúsculas,
  usos limitados, 1 canje por cuenta).
- /admin/promo: panel para crear / editar / activar / desactivar / borrar
  códigos de promoción.
- Tablas nuevas (sql/): home_tradecode, home_promocode, home_promoredemption.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-14 09:27:11 +00:00
parent 22ae0ee278
commit 66d056c04b
25 changed files with 1711 additions and 2 deletions
+60
View File
@@ -25,3 +25,63 @@ export async function creditDPoints(accountId: number, points: number): Promise<
return false
}
}
/** Saldo de PD (donate points) de una cuenta de juego. 0 si no tiene fila. */
export async function getDPointsBalance(accountId: number): Promise<number> {
if (!accountId) return 0
try {
const [rows] = await db(DB.default).query<RowDataPacket[]>(
'SELECT dp FROM home_api_points WHERE accountID = ?',
[accountId],
)
return rows[0] ? Number(rows[0].dp) : 0
} catch {
return 0
}
}
/**
* 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<RowDataPacket[]>(
'SELECT dp FROM home_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 home_api_points SET dp = dp - ? WHERE accountID = ?', [points, fromAccountId])
await conn.query(
'INSERT INTO home_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()
}
}