Quitar el prefijo home_ de las 41 tablas del portal

El prefijo venía del nombre de la app Django (`home`), jubilada y borrada hoy,
así que ya no significaba nada: home_stripelog -> stripelog. La BD se sigue
llamando django_wow por historia (renombrarla es otra operación).

Comprobado antes de tocar nada: 0 colisiones con nombres existentes, 0 palabras
reservadas (contrastado contra las 260 de MySQL), sin vistas ni triggers que
dependieran de ellas. El RENAME va en una sola sentencia porque así es atómico,
y MySQL reapunta solo las 4 claves ajenas entre estas tablas.

170 referencias actualizadas en 37 ficheros. Se dejó a propósito
sql/drop_home_securitytoken_authuser_fk.sql sin tocar: es una migración ya
aplicada y reescribirla sería falsear el historial.

De paso salió un fallo previo: prices.ts consultaba `home_restoreitemprice`,
una tabla que NO existe. El try/catch de priceFrom se comía el error y devolvía
siempre el precio por defecto, así que el precio de restaurar objetos nunca fue
configurable. Se deja documentado en el código; crear la tabla es otra decisión.

Verificado tras aplicar: 0 tablas home_, las 51 siguen ahí, y el conteo exacto
de filas cuadra con el volcado previo (store_item 3068, item_data 44873,
stripelog 35, store_order 26, noticia 50). La web responde 200 en todas las
rutas y la portada carga las noticias sin errores.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-15 14:49:19 +00:00
parent 5fd006aca9
commit cccc70338d
39 changed files with 226 additions and 171 deletions
+3 -3
View File
@@ -7,9 +7,9 @@ Portal web para un servidor de World of Warcraft 3.4.3 basado en AzerothCore.
- **Bases de datos (MySQL):** `django_wow` (la del portal), `acore_auth`, `acore_characters`,
`acore_world` y `acore_web` (el foro)
> La base del portal se sigue llamando `django_wow` y sus tablas `home_*` por motivos históricos: el
> portal era una app Django, jubilada el 2026-07-15. El código se borró (está en el historial de git),
> pero **las tablas son las mismas y siguen en uso**: Next las consulta con SQL directo, sin ORM.
> La base del portal se sigue llamando `django_wow` por motivos históricos: el portal era una app
> Django, jubilada el 2026-07-15. Sus tablas llevaban el prefijo `home_` (el nombre de la app) y se
> renombraron sin él (`home_stripelog` → `stripelog`). Next las consulta con SQL directo, sin ORM.
---
+52
View File
@@ -0,0 +1,52 @@
-- Quitar el prefijo `home_` de las tablas del portal (2026-07-15). APLICADO EN PROD.
--
-- El prefijo venía del nombre de la app Django (`home`), jubilada y borrada ese
-- mismo día. La BD se sigue llamando `django_wow` por historia.
--
-- Va en UNA sola sentencia a propósito: RENAME TABLE con varias tablas es
-- atómico, así que o cambian las 41 o no cambia ninguna. MySQL reajusta solo
-- las claves ajenas que apuntaban a los nombres viejos.
--
-- Se comprobó antes: 0 colisiones, 0 palabras reservadas, sin vistas ni triggers.
RENAME TABLE `home_accountactivation` TO `accountactivation`,
`home_api_points` TO `api_points`,
`home_category` TO `category`,
`home_changefactionprice` TO `changefactionprice`,
`home_changeraceprice` TO `changeraceprice`,
`home_claimedreward` TO `claimedreward`,
`home_clientecategoria` TO `clientecategoria`,
`home_contentcreator` TO `contentcreator`,
`home_customizeprice` TO `customizeprice`,
`home_downloadclientpage` TO `downloadclientpage`,
`home_goldprice` TO `goldprice`,
`home_guildrenamesettings` TO `guildrenamesettings`,
`home_item` TO `item`,
`home_item_data` TO `item_data`,
`home_itemsearchhistory` TO `itemsearchhistory`,
`home_levelupprice` TO `levelupprice`,
`home_loginattempt` TO `loginattempt`,
`home_maintenancemode` TO `maintenancemode`,
`home_noticia` TO `noticia`,
`home_passwordreset` TO `passwordreset`,
`home_pedido` TO `pedido`,
`home_promocode` TO `promocode`,
`home_promoredemption` TO `promoredemption`,
`home_questsearchhistory` TO `questsearchhistory`,
`home_recruitafriend` TO `recruitafriend`,
`home_recruitreward` TO `recruitreward`,
`home_renameprice` TO `renameprice`,
`home_restorehistory` TO `restorehistory`,
`home_revivehistory` TO `revivehistory`,
`home_securitytoken` TO `securitytoken`,
`home_serverselection` TO `serverselection`,
`home_sistemaoperativo` TO `sistemaoperativo`,
`home_store_category` TO `store_category`,
`home_store_item` TO `store_item`,
`home_store_order` TO `store_order`,
`home_stripelog` TO `stripelog`,
`home_tradecode` TO `tradecode`,
`home_transferprice` TO `transferprice`,
`home_unstuckhistory` TO `unstuckhistory`,
`home_votelog` TO `votelog`,
`home_votesite` TO `votesite`;
+3 -3
View File
@@ -1,4 +1,4 @@
-- Siembra de las 50 noticias reales de NovaWoW/UltimoWoW en django_wow.home_noticia.
-- Siembra de las 50 noticias reales de NovaWoW/UltimoWoW en django_wow.noticia.
-- Aplicar: mysql django_wow < sql/seed_news.sql (BD por defecto del portal).
-- fecha_publicacion = fecha del post a las 12:00 menos N segundos por índice
-- (preserva el orden de la lista dentro de una misma fecha). enlace = NULL
@@ -7,8 +7,8 @@
-- cualquier noticia añadida después (p. ej. desde /admin/news). Quita esa
-- línea si solo quieres añadir estas sin borrar las existentes.
SET NAMES utf8mb4;
DELETE FROM home_noticia;
INSERT INTO home_noticia (titulo, contenido, fecha_publicacion, enlace) VALUES
DELETE FROM noticia;
INSERT INTO noticia (titulo, contenido, fecha_publicacion, enlace) VALUES
('FESTIVAL DE FUEGO DEL SOLSTICIO DE VERANO 🌞','Ha comenzado el evento Festival de Fuego del Solsticio de Verano que finaliza el día 5 de Julio.<br>Dispones de una guía completa <a href="https://foro.ultimowow.com/topic/17957-gu%C3%ADa-del-evento-festival-de-fuego-del-solsticio-de-veran" target="_blank" rel="noopener noreferrer">aquí</a>.','2026-06-21 12:00:00',NULL),
('10.ª TEMPORADA DE ARENAS ⚔️','¡La 10.ª Temporada de Arenas ⚔️ ya está en marcha y una nueva batalla por la gloria ha comenzado!<br> Durante los próximos meses, los mejores combatientes del reino se enfrentarán en intensos encuentros para escalar posiciones y luchar por las recompensas más prestigiosas.<br><br> ¡Forma equipo, entra en combate y demuestra de qué eres capaz!<br> Toda la información disponible <a href="https://foro.ultimowow.com/topic/46209-10%C2%AA-temporada-de-arenas/" target="_blank" rel="noopener noreferrer">aquí</a>.','2026-06-08 11:59:59',NULL),
('RESULTADOS FINALES DE LA 8.ª TEMPORADA DE ARENAS ⚔️','El pasado 8 de diciembre de 2025 finalizó nuestra 8.ª Temporada de Arenas y ya se encuentran disponibles los resultados oficiales.<br>En la publicación podrás consultar el listado completo de equipos y jugadores clasificados.<br>Toda la información disponible <a href="https://foro.ultimowow.com/topic/46211-a" target="_blank" rel="noopener noreferrer">aquí</a>.','2026-06-08 11:59:58',NULL),
+2 -2
View File
@@ -1,7 +1,7 @@
-- Recompensas de Recluta a un amigo (home_recruitreward) para el panel de /recruit.
-- Recompensas de Recluta a un amigo (recruitreward) para el panel de /recruit.
-- Aplicar: mysql django_wow < sql/seed_recruit_rewards.sql
SET NAMES utf8mb4;
INSERT INTO home_recruitreward (required_friends, reward_name, item_id, item_quantity, item_link, icon_class) VALUES
INSERT INTO recruitreward (required_friends, reward_name, item_id, item_quantity, item_link, icon_class) VALUES
(1,'Medallón de oro',37297,1,'https://wotlk.ultimowow.com/?item=37297','q3 inv_jewelry_amulet_03'),
(2,'Emblema de escarcha',49426,30,'https://wotlk.ultimowow.com/?item=49426','q4 inv_misc_frostemblem_01'),
(4,'Zhebra presta',37719,1,'https://wotlk.ultimowow.com/?item=37719','q4 ability_mount_charger'),
@@ -13,7 +13,7 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s
/**
* Creadores de contenido (el menú VIDEOS) — réplica del partial Django
* `partials/videos.html`. Enseña el primer creador de `home_contentcreator`, y
* `partials/videos.html`. Enseña el primer creador de `contentcreator`, y
* si no hay ninguno, el mismo aviso que Django.
*/
export default async function ContentCreatorsPage({ params }: { params: Promise<{ locale: string }> }) {
@@ -21,7 +21,7 @@ export default async function SecurityTokenPage({ params }: { params: Promise<{
let tokenDate: string | null = null
try {
const [rows] = await db(DB.default).query<RowDataPacket[]>(
'SELECT created_at FROM home_securitytoken WHERE user_id = ? ORDER BY created_at DESC LIMIT 1',
'SELECT created_at FROM securitytoken WHERE user_id = ? ORDER BY created_at DESC LIMIT 1',
[session.accountId ?? 0],
)
if (rows[0]) tokenDate = new Date(rows[0].created_at).toISOString()
+3 -3
View File
@@ -62,7 +62,7 @@ export async function getAccountDashboard(session: SessionData, locale = 'es'):
let vp = 0
try {
const [p] = await db(DB.default).query<RowDataPacket[]>(
'SELECT dp, vp FROM home_api_points WHERE accountID = ?',
'SELECT dp, vp FROM api_points WHERE accountID = ?',
[accountId],
)
if (p[0]) {
@@ -80,7 +80,7 @@ export async function getAccountDashboard(session: SessionData, locale = 'es'):
let donatedPD = 0
try {
const [d] = await db(DB.default).query<RowDataPacket[]>(
"SELECT COALESCE(SUM(amount), 0) AS total FROM home_stripelog WHERE account_id = ? AND fulfilled = 1 AND mode IN ('sumup', 'test', 'live')",
"SELECT COALESCE(SUM(amount), 0) AS total FROM stripelog WHERE account_id = ? AND fulfilled = 1 AND mode IN ('sumup', 'test', 'live')",
[accountId],
)
donatedPD = Math.round(Number(d[0]?.total ?? 0) * PD_PER_UNIT)
@@ -154,7 +154,7 @@ export async function getAccountDashboard(session: SessionData, locale = 'es'):
let securityTokenDate: string | null = null
try {
const [s] = await db(DB.default).query<RowDataPacket[]>(
'SELECT created_at FROM home_securitytoken WHERE user_id = ? ORDER BY created_at DESC LIMIT 1',
'SELECT created_at FROM securitytoken WHERE user_id = ? ORDER BY created_at DESC LIMIT 1',
[accountId],
)
if (s[0]) securityTokenDate = new Date(s[0].created_at).toISOString()
+3 -3
View File
@@ -9,19 +9,19 @@ export interface GoldOption {
export async function listGoldOptions(): Promise<GoldOption[]> {
const [rows] = await db(DB.default).query<RowDataPacket[]>(
'SELECT id, gold_amount, price FROM home_goldprice ORDER BY gold_amount',
'SELECT id, gold_amount, price FROM goldprice ORDER BY gold_amount',
)
return rows.map((r) => ({ id: r.id, gold_amount: r.gold_amount, price: Number(r.price) }))
}
export async function createGoldOption(goldAmount: number, price: number): Promise<number> {
const [res] = await db(DB.default).query<ResultSetHeader>(
'INSERT INTO home_goldprice (gold_amount, price) VALUES (?, ?)',
'INSERT INTO goldprice (gold_amount, price) VALUES (?, ?)',
[goldAmount, price],
)
return res.insertId
}
export async function deleteGoldOption(id: number): Promise<void> {
await db(DB.default).query('DELETE FROM home_goldprice WHERE id = ?', [id])
await db(DB.default).query('DELETE FROM goldprice WHERE id = ?', [id])
}
+4 -4
View File
@@ -13,7 +13,7 @@ export interface NewsItem {
export async function listNews(): Promise<NewsItem[]> {
const [rows] = await db(DB.default).query<RowDataPacket[]>(
'SELECT id, titulo, titulo_en, contenido, contenido_en, fecha_publicacion, enlace FROM home_noticia ORDER BY fecha_publicacion DESC',
'SELECT id, titulo, titulo_en, contenido, contenido_en, fecha_publicacion, enlace FROM noticia ORDER BY fecha_publicacion DESC',
)
return rows.map((r) => ({
id: r.id,
@@ -34,7 +34,7 @@ export async function createNews(
contenidoEn: string | null,
): Promise<number> {
const [res] = await db(DB.default).query<ResultSetHeader>(
'INSERT INTO home_noticia (titulo, titulo_en, contenido, contenido_en, fecha_publicacion, enlace) VALUES (?, ?, ?, ?, NOW(), ?)',
'INSERT INTO noticia (titulo, titulo_en, contenido, contenido_en, fecha_publicacion, enlace) VALUES (?, ?, ?, ?, NOW(), ?)',
[titulo, tituloEn || null, contenido, contenidoEn || null, enlace || null],
)
return res.insertId
@@ -50,11 +50,11 @@ export async function updateNews(
contenidoEn: string | null,
): Promise<void> {
await db(DB.default).query(
'UPDATE home_noticia SET titulo = ?, titulo_en = ?, contenido = ?, contenido_en = ?, enlace = ? WHERE id = ?',
'UPDATE noticia SET titulo = ?, titulo_en = ?, contenido = ?, contenido_en = ?, enlace = ? WHERE id = ?',
[titulo, tituloEn || null, contenido, contenidoEn || null, enlace || null, id],
)
}
export async function deleteNews(id: number): Promise<void> {
await db(DB.default).query('DELETE FROM home_noticia WHERE id = ?', [id])
await db(DB.default).query('DELETE FROM noticia WHERE id = ?', [id])
}
+6 -6
View File
@@ -3,12 +3,12 @@ import { db, DB } from './db'
// Servicios con precio único (una fila) -> tabla django_wow.
export const PRICE_TABLES: Record<string, string> = {
rename: 'home_renameprice',
customize: 'home_customizeprice',
'change-race': 'home_changeraceprice',
'change-faction': 'home_changefactionprice',
'level-up': 'home_levelupprice',
transfer: 'home_transferprice',
rename: 'renameprice',
customize: 'customizeprice',
'change-race': 'changeraceprice',
'change-faction': 'changefactionprice',
'level-up': 'levelupprice',
transfer: 'transferprice',
}
export interface PriceRow {
+6 -6
View File
@@ -23,8 +23,8 @@ function toMysqlDate(v: string): string | null {
export async function listPromoCodes(): Promise<PromoCode[]> {
const [rows] = await db(DB.default).query<RowDataPacket[]>(
`SELECT p.id, p.code, p.pd, p.pv, p.max_uses, p.uses, p.active, p.expires_at,
(SELECT COUNT(*) FROM home_promoredemption r WHERE r.promo_id = p.id) AS redemptions
FROM home_promocode p ORDER BY p.id DESC`,
(SELECT COUNT(*) FROM promoredemption r WHERE r.promo_id = p.id) AS redemptions
FROM promocode p ORDER BY p.id DESC`,
)
return rows.map((r) => ({
id: r.id,
@@ -48,7 +48,7 @@ export async function createPromoCode(
): Promise<{ id?: number; error?: string }> {
try {
const [res] = await db(DB.default).query<ResultSetHeader>(
'INSERT INTO home_promocode (code, pd, pv, max_uses, uses, active, expires_at, created_at) VALUES (?, ?, ?, ?, 0, 1, ?, NOW())',
'INSERT INTO promocode (code, pd, pv, max_uses, uses, active, expires_at, created_at) VALUES (?, ?, ?, ?, 0, 1, ?, NOW())',
[code, pd, pv, maxUses, toMysqlDate(expiresAt)],
)
return { id: res.insertId }
@@ -63,12 +63,12 @@ export async function updatePromoCode(
fields: { pd: number; pv: number; maxUses: number; active: boolean },
): Promise<void> {
await db(DB.default).query(
'UPDATE home_promocode SET pd = ?, pv = ?, max_uses = ?, active = ? WHERE id = ?',
'UPDATE promocode SET pd = ?, pv = ?, max_uses = ?, active = ? WHERE id = ?',
[fields.pd, fields.pv, fields.maxUses, fields.active ? 1 : 0, id],
)
}
export async function deletePromoCode(id: number): Promise<void> {
await db(DB.default).query('DELETE FROM home_promoredemption WHERE promo_id = ?', [id])
await db(DB.default).query('DELETE FROM home_promocode WHERE id = ?', [id])
await db(DB.default).query('DELETE FROM promoredemption WHERE promo_id = ?', [id])
await db(DB.default).query('DELETE FROM promocode WHERE id = ?', [id])
}
+3 -3
View File
@@ -13,7 +13,7 @@ export interface RecruitReward {
export async function listRecruitRewards(): Promise<RecruitReward[]> {
const [rows] = await db(DB.default).query<RowDataPacket[]>(
'SELECT id, required_friends, reward_name, item_id, item_quantity, item_link, icon_class FROM home_recruitreward ORDER BY required_friends, id',
'SELECT id, required_friends, reward_name, item_id, item_quantity, item_link, icon_class FROM recruitreward ORDER BY required_friends, id',
)
return rows.map((r) => ({
id: r.id,
@@ -28,7 +28,7 @@ export async function listRecruitRewards(): Promise<RecruitReward[]> {
export async function createRecruitReward(data: Omit<RecruitReward, 'id'>): Promise<number> {
const [res] = await db(DB.default).query<ResultSetHeader>(
`INSERT INTO home_recruitreward
`INSERT INTO recruitreward
(required_friends, reward_name, item_id, item_quantity, item_link, icon_class)
VALUES (?, ?, ?, ?, ?, ?)`,
[data.required_friends, data.reward_name, data.item_id, data.item_quantity, data.item_link, data.icon_class],
@@ -37,5 +37,5 @@ export async function createRecruitReward(data: Omit<RecruitReward, 'id'>): Prom
}
export async function deleteRecruitReward(id: number): Promise<void> {
await db(DB.default).query('DELETE FROM home_recruitreward WHERE id = ?', [id])
await db(DB.default).query('DELETE FROM recruitreward WHERE id = ?', [id])
}
+3 -3
View File
@@ -11,19 +11,19 @@ export interface VoteSite {
export async function listVoteSites(): Promise<VoteSite[]> {
const [rows] = await db(DB.default).query<RowDataPacket[]>(
'SELECT id, name, url, image_url, points FROM home_votesite ORDER BY id',
'SELECT id, name, url, image_url, points FROM votesite ORDER BY id',
)
return rows.map((r) => ({ id: r.id, name: r.name, url: r.url, image_url: r.image_url, points: r.points }))
}
export async function createVoteSite(name: string, url: string, imageUrl: string, points: number): Promise<number> {
const [res] = await db(DB.default).query<ResultSetHeader>(
'INSERT INTO home_votesite (name, url, image_url, points, created_at, updated_at) VALUES (?, ?, ?, ?, NOW(), NOW())',
'INSERT INTO votesite (name, url, image_url, points, created_at, updated_at) VALUES (?, ?, ?, ?, NOW(), NOW())',
[name, url, imageUrl, points],
)
return res.insertId
}
export async function deleteVoteSite(id: number): Promise<void> {
await db(DB.default).query('DELETE FROM home_votesite WHERE id = ?', [id])
await db(DB.default).query('DELETE FROM votesite WHERE id = ?', [id])
}
+6 -6
View File
@@ -34,7 +34,7 @@ export async function requestEmailChange(
const oldHash = crypto.randomBytes(16).toString('hex')
const newHash = crypto.randomBytes(16).toString('hex')
await db(DB.default).query(
'INSERT INTO home_accountactivation (username, email, old_email, password, recruiter_id, hash, old_email_hash, is_used, is_new_email_used, created_at) ' +
'INSERT INTO accountactivation (username, email, old_email, password, recruiter_id, hash, old_email_hash, is_used, is_new_email_used, created_at) ' +
'VALUES (?, ?, ?, ?, ?, ?, ?, 0, 0, NOW())',
[session.username ?? null, newEmail, current, curPassword, userId, newHash, oldHash],
)
@@ -51,12 +51,12 @@ export async function requestEmailChange(
export async function confirmOldEmail(hash: string): Promise<Result> {
const [rows] = await db(DB.default).query<RowDataPacket[]>(
'SELECT id, email, hash, username FROM home_accountactivation WHERE old_email_hash = ? AND is_used = 0',
'SELECT id, email, hash, username FROM accountactivation WHERE old_email_hash = ? AND is_used = 0',
[hash],
)
const act = rows[0]
if (!act) return { success: false, error: 'invalidLink' }
await db(DB.default).query('UPDATE home_accountactivation SET is_used = 1 WHERE id = ?', [act.id])
await db(DB.default).query('UPDATE accountactivation SET is_used = 1 WHERE id = ?', [act.id])
const site = process.env.SITE_URL || ''
const link = `${site}/confirm-new-email?hash=${act.hash}`
@@ -70,12 +70,12 @@ export async function confirmOldEmail(hash: string): Promise<Result> {
export async function confirmNewEmail(hash: string): Promise<Result> {
const [rows] = await db(DB.default).query<RowDataPacket[]>(
'SELECT id, email, old_email, password FROM home_accountactivation WHERE hash = ? AND is_new_email_used = 0 AND old_email IS NOT NULL',
'SELECT id, email, old_email, password FROM accountactivation WHERE hash = ? AND is_new_email_used = 0 AND old_email IS NOT NULL',
[hash],
)
const act = rows[0]
if (!act) return { success: false, error: 'invalidLink' }
await db(DB.default).query('UPDATE home_accountactivation SET is_new_email_used = 1 WHERE id = ?', [act.id])
await db(DB.default).query('UPDATE accountactivation SET is_new_email_used = 1 WHERE id = ?', [act.id])
const newNorm = normalizeEmail(act.email)
const oldNorm = normalizeEmail(act.old_email || '')
@@ -99,6 +99,6 @@ export async function confirmNewEmail(hash: string): Promise<Result> {
} catch {
return { success: false, error: 'genericError' }
}
await db(DB.default).query('DELETE FROM home_accountactivation WHERE id = ?', [act.id])
await db(DB.default).query('DELETE FROM accountactivation WHERE id = ?', [act.id])
return { success: true }
}
+2 -2
View File
@@ -43,7 +43,7 @@ async function soapCharacterAction(accountId: number, character: string, cfg: So
export function reviveCharacter(accountId: number, character: string): Promise<Result> {
return soapCharacterAction(accountId, character, {
command: (c) => `.revive ${c}`,
historyTable: 'home_revivehistory',
historyTable: 'revivehistory',
cooldownHours: 12,
})
}
@@ -51,7 +51,7 @@ export function reviveCharacter(accountId: number, character: string): Promise<R
export function unstuckCharacter(accountId: number, character: string): Promise<Result> {
return soapCharacterAction(accountId, character, {
command: (c) => `.unstuck ${c}`,
historyTable: 'home_unstuckhistory',
historyTable: 'unstuckhistory',
cooldownHours: 12,
})
}
+2 -2
View File
@@ -2,7 +2,7 @@ import type { RowDataPacket } from 'mysql2'
import { db, DB } from './db'
/**
* Creador de contenido destacado (`home_contentcreator`, el menú VIDEOS).
* Creador de contenido destacado (`contentcreator`, el menú VIDEOS).
*
* Como en Django, se muestra SOLO el primero: la tabla se maneja desde el admin
* y ahora mismo está vacía, así que la página enseña el aviso de "no hay
@@ -25,7 +25,7 @@ export async function getContentCreator(): Promise<ContentCreator | null> {
const [rows] = await db(DB.default).query<RowDataPacket[]>(
`SELECT title, description, content_type, subtitle, youtube_video_url,
avatar_image_url, youtube_channel_url, facebook_page_url
FROM home_contentcreator ORDER BY id LIMIT 1`,
FROM contentcreator ORDER BY id LIMIT 1`,
)
const r = rows[0]
if (!r) return null
+14 -14
View File
@@ -7,18 +7,18 @@ export const PD_PER_UNIT = 100
export const DP_MIN_AMOUNT = 1
export const DP_MAX_AMOUNT = 200
/** Acredita PD (donate points) a una cuenta de juego en `home_api_points` (upsert). */
/** Acredita PD (donate points) a una cuenta de juego en `api_points` (upsert). */
export async function creditDPoints(accountId: number, points: number): Promise<boolean> {
if (!accountId || !(points > 0)) return false
try {
const [rows] = await db(DB.default).query<RowDataPacket[]>(
'SELECT id FROM home_api_points WHERE accountID = ?',
'SELECT id FROM api_points WHERE accountID = ?',
[accountId],
)
if (rows[0]) {
await db(DB.default).query('UPDATE home_api_points SET dp = dp + ? WHERE accountID = ?', [points, accountId])
await db(DB.default).query('UPDATE api_points SET dp = dp + ? WHERE accountID = ?', [points, accountId])
} else {
await db(DB.default).query('INSERT INTO home_api_points (accountID, vp, dp) VALUES (?, 0, ?)', [accountId, points])
await db(DB.default).query('INSERT INTO api_points (accountID, vp, dp) VALUES (?, 0, ?)', [accountId, points])
}
return true
} catch {
@@ -31,7 +31,7 @@ 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 = ?',
'SELECT dp FROM api_points WHERE accountID = ?',
[accountId],
)
return rows[0] ? Number(rows[0].dp) : 0
@@ -45,7 +45,7 @@ export async function getVPointsBalance(accountId: number): Promise<number> {
if (!accountId) return 0
try {
const [rows] = await db(DB.default).query<RowDataPacket[]>(
'SELECT vp FROM home_api_points WHERE accountID = ?',
'SELECT vp FROM api_points WHERE accountID = ?',
[accountId],
)
return rows[0] ? Number(rows[0].vp) : 0
@@ -59,7 +59,7 @@ export async function creditVPoints(accountId: number, points: number): Promise<
if (!accountId || !(points > 0)) return false
try {
await db(DB.default).query(
'INSERT INTO home_api_points (accountID, vp, dp) VALUES (?, ?, 0) ON DUPLICATE KEY UPDATE vp = vp + ?',
'INSERT INTO api_points (accountID, vp, dp) VALUES (?, ?, 0) ON DUPLICATE KEY UPDATE vp = vp + ?',
[accountId, points, points],
)
return true
@@ -84,7 +84,7 @@ export async function spendVPoints(
try {
await conn.beginTransaction()
const [src] = await conn.query<RowDataPacket[]>(
'SELECT vp FROM home_api_points WHERE accountID = ? FOR UPDATE',
'SELECT vp FROM api_points WHERE accountID = ? FOR UPDATE',
[accountId],
)
const balance = src[0] ? Number(src[0].vp) : 0
@@ -92,7 +92,7 @@ export async function spendVPoints(
await conn.rollback()
return { success: false, error: 'insufficientFunds' }
}
await conn.query('UPDATE home_api_points SET vp = vp - ? WHERE accountID = ?', [points, accountId])
await conn.query('UPDATE api_points SET vp = vp - ? WHERE accountID = ?', [points, accountId])
await conn.commit()
return { success: true }
} catch {
@@ -126,7 +126,7 @@ export async function transferDPoints(
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',
'SELECT dp FROM api_points WHERE accountID = ? FOR UPDATE',
[fromAccountId],
)
const balance = src[0] ? Number(src[0].dp) : 0
@@ -134,9 +134,9 @@ export async function transferDPoints(
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('UPDATE 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 + ?',
'INSERT INTO api_points (accountID, vp, dp) VALUES (?, 0, ?) ON DUPLICATE KEY UPDATE dp = dp + ?',
[toAccountId, points, points],
)
await conn.commit()
@@ -170,7 +170,7 @@ export async function spendDPoints(
try {
await conn.beginTransaction()
const [src] = await conn.query<RowDataPacket[]>(
'SELECT dp FROM home_api_points WHERE accountID = ? FOR UPDATE',
'SELECT dp FROM api_points WHERE accountID = ? FOR UPDATE',
[accountId],
)
const balance = src[0] ? Number(src[0].dp) : 0
@@ -178,7 +178,7 @@ export async function spendDPoints(
await conn.rollback()
return { success: false, error: 'insufficientFunds' }
}
await conn.query('UPDATE home_api_points SET dp = dp - ? WHERE accountID = ?', [points, accountId])
await conn.query('UPDATE api_points SET dp = dp - ? WHERE accountID = ?', [points, accountId])
await conn.commit()
return { success: true }
} catch {
+2 -2
View File
@@ -38,7 +38,7 @@ export function expansionFromGamebuild(gamebuild: number): string {
*/
export async function getNoticias(locale = 'es', limit = 50): Promise<Noticia[]> {
const [rows] = await db(DB.default).query<RowDataPacket[]>(
'SELECT id, titulo, titulo_en, contenido, contenido_en, fecha_publicacion, enlace FROM home_noticia ORDER BY fecha_publicacion DESC LIMIT ?',
'SELECT id, titulo, titulo_en, contenido, contenido_en, fecha_publicacion, enlace FROM noticia ORDER BY fecha_publicacion DESC LIMIT ?',
[limit],
)
const en = locale === 'en'
@@ -81,7 +81,7 @@ function checkServerStatus(host: string, port: number, timeout = 1000): Promise<
export async function getServerStatus(): Promise<ServerStatus | null> {
const [rows] = await db(DB.default).query<RowDataPacket[]>(
'SELECT name, address, port, gamebuild FROM home_serverselection LIMIT 1',
'SELECT name, address, port, gamebuild FROM serverselection LIMIT 1',
)
const s = rows[0]
if (!s) return null
+2 -2
View File
@@ -125,7 +125,7 @@ export const PAID_SERVICES: Record<string, PaidServiceConfig> = {
precheck: ({ accountId, email, character, body }) => checkTransferEligibility(accountId, email, character, body),
},
// Enviar regalo: es una compra de la tienda cuyo correo va a OTRO personaje y
// dice quién lo manda. El carrito se guarda en home_store_order (order_ref) y
// dice quién lo manda. El carrito se guarda en store_order (order_ref) y
// se entrega igual que la tienda, con su reembolso parcial si falla a medias.
// `sender` = personaje de origen, para redactar el correo.
'send-gift': {
@@ -143,7 +143,7 @@ export const PAID_SERVICES: Record<string, PaidServiceConfig> = {
extraFields: ['guildId', 'newName'],
},
// Tienda pagada con tarjeta: al confirmarse el pago, envía por correo los ítems
// del pedido (guardado en home_store_order) referenciado en metadata.order_ref.
// del pedido (guardado en store_order) referenciado en metadata.order_ref.
store: {
price: (m) => Promise.resolve(Number(m.amount)),
productName: () => 'Compra en la tienda',
+9 -9
View File
@@ -25,7 +25,7 @@ export async function getPointsBalances(accountId: number): Promise<{ dp: number
if (!accountId) return { dp: 0, pv: 0 }
try {
const [rows] = await db(DB.default).query<RowDataPacket[]>(
'SELECT dp, vp FROM home_api_points WHERE accountID = ?',
'SELECT dp, vp FROM api_points WHERE accountID = ?',
[accountId],
)
return rows[0] ? { dp: Number(rows[0].dp), pv: Number(rows[0].vp) } : { dp: 0, pv: 0 }
@@ -36,9 +36,9 @@ export async function getPointsBalances(accountId: number): Promise<{ dp: number
/**
* Historial unificado de PD/PV de una cuenta, combinando las fuentes reales:
* - `home_stripelog`: pagos por **Stripe** (mode test/live) y **SumUp** (mode sumup).
* - `home_votelog` (+ `home_votesite`): PV ganados al votar.
* - `home_promoredemption` (+ `home_promocode`): PD/PV canjeados por código.
* - `stripelog`: pagos por **Stripe** (mode test/live) y **SumUp** (mode sumup).
* - `votelog` (+ `votesite`): PV ganados al votar.
* - `promoredemption` (+ `promocode`): PD/PV canjeados por código.
* Ordenado por fecha descendente; cada consulta es tolerante a fallos.
*/
export async function getPointsHistory(accountId: number, limit = 100): Promise<PointsMovement[]> {
@@ -49,7 +49,7 @@ export async function getPointsHistory(accountId: number, limit = 100): Promise<
try {
const [rows] = await db(DB.default).query<RowDataPacket[]>(
'SELECT product_name, character_name, amount, mode, service, metadata, fulfilled, timestamp ' +
'FROM home_stripelog WHERE account_id = ? ORDER BY timestamp DESC LIMIT ?',
'FROM stripelog WHERE account_id = ? ORDER BY timestamp DESC LIMIT ?',
[accountId, limit],
)
for (const r of rows) {
@@ -76,8 +76,8 @@ export async function getPointsHistory(accountId: number, limit = 100): Promise<
// 2) Votos (PV).
try {
const [rows] = await db(DB.default).query<RowDataPacket[]>(
'SELECT v.created_at, s.name, s.points FROM home_votelog v ' +
'JOIN home_votesite s ON s.id = v.vote_site_id WHERE v.account_id = ? ORDER BY v.created_at DESC LIMIT ?',
'SELECT v.created_at, s.name, s.points FROM votelog v ' +
'JOIN votesite s ON s.id = v.vote_site_id WHERE v.account_id = ? ORDER BY v.created_at DESC LIMIT ?',
[accountId, limit],
)
for (const r of rows) {
@@ -100,8 +100,8 @@ export async function getPointsHistory(accountId: number, limit = 100): Promise<
// 3) Promociones (PD/PV).
try {
const [rows] = await db(DB.default).query<RowDataPacket[]>(
'SELECT r.redeemed_at, c.code, c.pd, c.pv FROM home_promoredemption r ' +
'JOIN home_promocode c ON c.id = r.promo_id WHERE r.account_id = ? ORDER BY r.redeemed_at DESC LIMIT ?',
'SELECT r.redeemed_at, c.code, c.pd, c.pv FROM promoredemption r ' +
'JOIN promocode c ON c.id = r.promo_id WHERE r.account_id = ? ORDER BY r.redeemed_at DESC LIMIT ?',
[accountId, limit],
)
for (const r of rows) {
+11 -8
View File
@@ -10,13 +10,16 @@ async function priceFrom(table: string, fallback: number): Promise<number> {
}
}
export const getRenamePrice = () => priceFrom('home_renameprice', 2.0)
export const getCustomizePrice = () => priceFrom('home_customizeprice', 1.0)
export const getChangeRacePrice = () => priceFrom('home_changeraceprice', 10.0)
export const getChangeFactionPrice = () => priceFrom('home_changefactionprice', 10.0)
export const getLevelUpPrice = () => priceFrom('home_levelupprice', 10.0)
export const getTransferPrice = () => priceFrom('home_transferprice', 10.0)
export const getRestoreItemPrice = () => priceFrom('home_restoreitemprice', 1.0)
export const getRenamePrice = () => priceFrom('renameprice', 2.0)
export const getCustomizePrice = () => priceFrom('customizeprice', 1.0)
export const getChangeRacePrice = () => priceFrom('changeraceprice', 10.0)
export const getChangeFactionPrice = () => priceFrom('changefactionprice', 10.0)
export const getLevelUpPrice = () => priceFrom('levelupprice', 10.0)
export const getTransferPrice = () => priceFrom('transferprice', 10.0)
// OJO: la tabla `restoreitemprice` NO existe en la BD, así que esto siempre cae
// al valor por defecto (el catch de priceFrom se come el error). Viene de antes;
// si se quiere un precio configurable, hay que crear la tabla.
export const getRestoreItemPrice = () => priceFrom('restoreitemprice', 1.0)
export interface GoldOption {
gold_amount: number
@@ -26,7 +29,7 @@ export interface GoldOption {
export async function getGoldOptions(): Promise<GoldOption[]> {
try {
const [rows] = await db(DB.default).query<RowDataPacket[]>(
'SELECT gold_amount, price FROM home_goldprice ORDER BY gold_amount',
'SELECT gold_amount, price FROM goldprice ORDER BY gold_amount',
)
return rows.map((r) => ({ gold_amount: r.gold_amount, price: Number(r.price) }))
} catch {
+4 -4
View File
@@ -26,7 +26,7 @@ export async function redeemPromoCode(accountId: number, rawCode: string): Promi
// Bloquea la fila del código (comparación binaria => sensible a may/min).
const [rows] = await conn.query<RowDataPacket[]>(
'SELECT id, pd, pv, max_uses, uses, active, expires_at FROM home_promocode WHERE code = ? COLLATE utf8mb4_bin LIMIT 1 FOR UPDATE',
'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]
@@ -45,7 +45,7 @@ export async function redeemPromoCode(accountId: number, rawCode: string): Promi
// Registra el canje (la clave única promo+cuenta evita el doble uso).
try {
await conn.query('INSERT INTO home_promoredemption (promo_id, account_id, redeemed_at) VALUES (?, ?, NOW())', [
await conn.query('INSERT INTO promoredemption (promo_id, account_id, redeemed_at) VALUES (?, ?, NOW())', [
promo.id,
accountId,
])
@@ -57,7 +57,7 @@ export async function redeemPromoCode(accountId: number, rawCode: string): Promi
// Consume un uso (condicional, por si hubo carrera con otro canje).
const [upd] = await conn.query<ResultSetHeader>(
'UPDATE home_promocode SET uses = uses + 1 WHERE id = ? AND uses < max_uses',
'UPDATE promocode SET uses = uses + 1 WHERE id = ? AND uses < max_uses',
[promo.id],
)
if (upd.affectedRows !== 1) {
@@ -69,7 +69,7 @@ export async function redeemPromoCode(accountId: number, rawCode: string): Promi
const pd = Number(promo.pd)
const pv = Number(promo.pv)
await conn.query(
'INSERT INTO home_api_points (accountID, vp, dp) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE vp = vp + VALUES(vp), dp = dp + VALUES(dp)',
'INSERT INTO api_points (accountID, vp, dp) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE vp = vp + VALUES(vp), dp = dp + VALUES(dp)',
[accountId, pv, pd],
)
+2 -2
View File
@@ -104,7 +104,7 @@ export async function questStatuses(guid: number, quests: number[]): Promise<Que
export async function cooldownRemainingMs(character: string, category: QuestCategory): Promise<number> {
try {
const [rows] = await db(DB.default).query<RowDataPacket[]>(
'SELECT searched_at FROM home_questsearchhistory WHERE character_name = ? AND category = ? ORDER BY searched_at DESC LIMIT 1',
'SELECT searched_at FROM questsearchhistory WHERE character_name = ? AND category = ? ORDER BY searched_at DESC LIMIT 1',
[character, category],
)
if (!rows[0]) return 0
@@ -118,7 +118,7 @@ export async function cooldownRemainingMs(character: string, category: QuestCate
export async function recordSearch(character: string, category: QuestCategory): Promise<void> {
try {
await db(DB.default).query(
'INSERT INTO home_questsearchhistory (character_name, category, searched_at) VALUES (?, ?, NOW())',
'INSERT INTO questsearchhistory (character_name, category, searched_at) VALUES (?, ?, NOW())',
[character, category],
)
} catch {
+5 -5
View File
@@ -45,7 +45,7 @@ export async function requestRecovery(type: string, value: string): Promise<Resu
if (email) {
const token = crypto.randomBytes(32).toString('hex')
await db(DB.default).query(
'INSERT INTO home_passwordreset (email, token, created_at, used) VALUES (?, ?, NOW(), 0)',
'INSERT INTO passwordreset (email, token, created_at, used) VALUES (?, ?, NOW(), 0)',
[email, token],
)
const link = `${site}/reset-password?token=${token}`
@@ -81,7 +81,7 @@ export async function requestRecovery(type: string, value: string): Promise<Resu
if (ids.length) {
const placeholders = ids.map(() => '?').join(',')
const [toks] = await db(DB.default).query<RowDataPacket[]>(
`SELECT user_id, token FROM home_securitytoken WHERE user_id IN (${placeholders})`,
`SELECT user_id, token FROM securitytoken WHERE user_id IN (${placeholders})`,
ids,
)
if (toks.length) {
@@ -132,7 +132,7 @@ export async function requestRecovery(type: string, value: string): Promise<Resu
if (type === 'activation') {
try {
const [rows] = await db(DB.default).query<RowDataPacket[]>(
'SELECT hash, password FROM home_accountactivation WHERE email = ? AND old_email IS NULL ORDER BY created_at DESC LIMIT 1',
'SELECT hash, password FROM accountactivation WHERE email = ? AND old_email IS NULL ORDER BY created_at DESC LIMIT 1',
[email],
)
if (rows[0]) {
@@ -154,7 +154,7 @@ export async function requestRecovery(type: string, value: string): Promise<Resu
export async function resetPassword(token: string, newPassword: string, confPassword: string): Promise<Result> {
const [rows] = await db(DB.default).query<RowDataPacket[]>(
'SELECT id, email, created_at FROM home_passwordreset WHERE token = ? AND used = 0',
'SELECT id, email, created_at FROM passwordreset WHERE token = ? AND used = 0',
[token],
)
const pr = rows[0]
@@ -174,6 +174,6 @@ export async function resetPassword(token: string, newPassword: string, confPass
} catch {
return { success: false, error: 'genericError' }
}
await db(DB.default).query('UPDATE home_passwordreset SET used = 1 WHERE id = ?', [pr.id])
await db(DB.default).query('UPDATE passwordreset SET used = 1 WHERE id = ?', [pr.id])
return { success: true, redirect: '/log-in' }
}
+5 -5
View File
@@ -74,12 +74,12 @@ async function getValidRecruitsAtLevel80(accountId: number, requiredCount: numbe
/** Todas las recompensas con marca de si esta cuenta ya las reclamó. */
export async function getRecruitRewards(accountId: number | null): Promise<RecruitRewardView[]> {
const [rows] = await db(DB.default).query<RowDataPacket[]>(
'SELECT id, required_friends, reward_name, item_id, item_quantity, item_link, icon_class FROM home_recruitreward ORDER BY required_friends, id',
'SELECT id, required_friends, reward_name, item_id, item_quantity, item_link, icon_class FROM recruitreward ORDER BY required_friends, id',
)
let claimedIds = new Set<number>()
if (accountId) {
const [claimed] = await db(DB.default).query<RowDataPacket[]>(
'SELECT recruit_reward_id FROM home_claimedreward WHERE account_id = ?',
'SELECT recruit_reward_id FROM claimedreward WHERE account_id = ?',
[accountId],
)
claimedIds = new Set(claimed.map((c) => Number(c.recruit_reward_id)))
@@ -119,7 +119,7 @@ export async function claimRecruitReward(
ip: string,
): Promise<ClaimResult> {
const [rewardRows] = await db(DB.default).query<RowDataPacket[]>(
'SELECT id, required_friends, reward_name, item_id, item_quantity FROM home_recruitreward WHERE id = ?',
'SELECT id, required_friends, reward_name, item_id, item_quantity FROM recruitreward WHERE id = ?',
[rewardId],
)
const reward = rewardRows[0]
@@ -130,7 +130,7 @@ export async function claimRecruitReward(
}
const [already] = await db(DB.default).query<RowDataPacket[]>(
'SELECT 1 FROM home_claimedreward WHERE account_id = ? AND recruit_reward_id = ? LIMIT 1',
'SELECT 1 FROM claimedreward WHERE account_id = ? AND recruit_reward_id = ? LIMIT 1',
[accountId, rewardId],
)
if (already[0]) return { success: false, message: 'alreadyClaimed' }
@@ -145,7 +145,7 @@ export async function claimRecruitReward(
}
await db(DB.default).query(
'INSERT INTO home_claimedreward (account_id, username, recruit_reward_id, character_name, claimed_at, ip_address) VALUES (?, ?, ?, ?, NOW(), ?)',
'INSERT INTO claimedreward (account_id, username, recruit_reward_id, character_name, claimed_at, ip_address) VALUES (?, ?, ?, ?, NOW(), ?)',
[accountId, username, rewardId, characterName, ip.slice(0, 45)],
)
return { success: true, message: 'claimSuccess' }
+4 -4
View File
@@ -44,7 +44,7 @@ export async function registerAccount(input: RegisterInput): Promise<Result> {
}
// Borrar cualquier activación de registro pendiente para ese email
await db(DB.default).query('DELETE FROM home_accountactivation WHERE email = ? AND old_email IS NULL', [email])
await db(DB.default).query('DELETE FROM accountactivation WHERE email = ? AND old_email IS NULL', [email])
// Reclutador opcional
let recruiterId = 0
@@ -68,7 +68,7 @@ export async function registerAccount(input: RegisterInput): Promise<Result> {
const hash = crypto.randomBytes(16).toString('hex') // 32 hex chars (col hash = CharField(32))
await db(DB.default).query(
'INSERT INTO home_accountactivation (email, password, recruiter_id, hash, created_at, is_used, is_new_email_used) VALUES (?, ?, ?, ?, NOW(), 0, 0)',
'INSERT INTO accountactivation (email, password, recruiter_id, hash, created_at, is_used, is_new_email_used) VALUES (?, ?, ?, ?, NOW(), 0, 0)',
[email, password, recruiterId, hash],
)
@@ -80,7 +80,7 @@ export async function registerAccount(input: RegisterInput): Promise<Result> {
export async function activateAccount(hash: string, lastIp = '0.0.0.0'): Promise<Result> {
const [rows] = await db(DB.default).query<RowDataPacket[]>(
'SELECT id, email, password, recruiter_id, created_at FROM home_accountactivation WHERE hash = ? AND old_email IS NULL',
'SELECT id, email, password, recruiter_id, created_at FROM accountactivation WHERE hash = ? AND old_email IS NULL',
[hash],
)
const act = rows[0]
@@ -107,6 +107,6 @@ export async function activateAccount(hash: string, lastIp = '0.0.0.0'): Promise
[gameUsername, game.salt, game.verifier, emailNorm, emailNorm, act.recruiter_id || 0, lastIp, 2, bnetId],
)
await db(DB.default).query('DELETE FROM home_accountactivation WHERE id = ?', [act.id])
await db(DB.default).query('DELETE FROM accountactivation WHERE id = ?', [act.id])
return { success: true }
}
+2 -2
View File
@@ -83,7 +83,7 @@ export async function restoreCharacter(accountId: number, guid: number): Promise
// Cooldown de 12 h por personaje.
try {
const [last] = await db(DB.default).query<RowDataPacket[]>(
'SELECT used_at FROM home_restorehistory WHERE character_guid = ? ORDER BY used_at DESC LIMIT 1',
'SELECT used_at FROM restorehistory WHERE character_guid = ? ORDER BY used_at DESC LIMIT 1',
[guid],
)
if (last[0] && Date.now() - new Date(last[0].used_at).getTime() < COOLDOWN_MS) {
@@ -106,7 +106,7 @@ export async function restoreCharacter(accountId: number, guid: number): Promise
}
await db(DB.default)
.query('INSERT INTO home_restorehistory (character_guid, used_at) VALUES (?, NOW())', [guid])
.query('INSERT INTO restorehistory (character_guid, used_at) VALUES (?, NOW())', [guid])
.catch(() => {})
return { success: true, name }
}
+2 -2
View File
@@ -52,7 +52,7 @@ export async function listDeletedItems(accountId: number, character: string): Pr
// Cooldown de 8 h por personaje.
try {
const [last] = await db(DB.default).query<RowDataPacket[]>(
'SELECT used_at FROM home_itemsearchhistory WHERE character_name = ? ORDER BY used_at DESC LIMIT 1',
'SELECT used_at FROM itemsearchhistory WHERE character_name = ? ORDER BY used_at DESC LIMIT 1',
[character],
)
if (last[0]) {
@@ -69,7 +69,7 @@ export async function listDeletedItems(accountId: number, character: string): Pr
if (soap === null) return { success: false, error: 'soapError' }
await db(DB.default)
.query('INSERT INTO home_itemsearchhistory (character_name, used_at) VALUES (?, NOW())', [character])
.query('INSERT INTO itemsearchhistory (character_name, used_at) VALUES (?, NOW())', [character])
.catch(() => {})
return { success: true, items: parseRecoverList(soap) }
}
+4 -4
View File
@@ -29,10 +29,10 @@ function webStatusKey(raw: string): 'success' | 'wrongPassword' | 'other' {
/**
* Historial de seguridad de una cuenta, en tres bloques:
* - **Actividad**: acciones de seguridad (solicitudes de token) desde `home_securitytoken`.
* - **Actividad**: acciones de seguridad (solicitudes de token) desde `securitytoken`.
* - **Conexiones (Reino)**: `acore_auth.logs_ip_actions` (histórico de IP) + la última
* conexión conocida (`account.last_ip`/`last_login`) si aporta una IP nueva.
* - **Conexiones (Web)**: cada intento de login web desde `home_loginattempt`.
* - **Conexiones (Web)**: cada intento de login web desde `loginattempt`.
* Cada consulta es tolerante a fallos (devuelve [] si la tabla no existe).
*/
export async function getSecurityHistory(
@@ -49,7 +49,7 @@ export async function getSecurityHistory(
// 1) Actividad: solicitudes de token de seguridad.
try {
const [rows] = await db(DB.default).query<RowDataPacket[]>(
'SELECT created_at, ip_address FROM home_securitytoken WHERE user_id = ? ORDER BY created_at DESC LIMIT ?',
'SELECT created_at, ip_address FROM securitytoken WHERE user_id = ? ORDER BY created_at DESC LIMIT ?',
[accountId, limit],
)
for (const r of rows) {
@@ -90,7 +90,7 @@ export async function getSecurityHistory(
if (email) {
try {
const [rows] = await db(DB.default).query<RowDataPacket[]>(
'SELECT username, status, ip_address, timestamp FROM home_loginattempt WHERE username = ? ORDER BY timestamp DESC LIMIT ?',
'SELECT username, status, ip_address, timestamp FROM loginattempt WHERE username = ? ORDER BY timestamp DESC LIMIT ?',
[email, limit],
)
for (const r of rows) {
+5 -5
View File
@@ -18,7 +18,7 @@ export async function requestSecurityToken(session: SessionData, ip: string): Pr
if (!email) return { success: false, error: 'noEmail' }
const [existing] = await db(DB.default).query<RowDataPacket[]>(
'SELECT id, created_at FROM home_securitytoken WHERE user_id = ? ORDER BY created_at DESC LIMIT 1',
'SELECT id, created_at FROM securitytoken WHERE user_id = ? ORDER BY created_at DESC LIMIT 1',
[userId],
)
if (existing[0]) {
@@ -29,9 +29,9 @@ export async function requestSecurityToken(session: SessionData, ip: string): Pr
const token = crypto.randomBytes(4).toString('base64url').slice(0, 6)
const expiresAt = new Date(Date.now() + 7 * 86400_000)
await db(DB.default).query('DELETE FROM home_securitytoken WHERE user_id = ?', [userId])
await db(DB.default).query('DELETE FROM securitytoken WHERE user_id = ?', [userId])
await db(DB.default).query(
'INSERT INTO home_securitytoken (token, created_at, expires_at, ip_address, user_id) VALUES (?, NOW(), ?, ?, ?)',
'INSERT INTO securitytoken (token, created_at, expires_at, ip_address, user_id) VALUES (?, NOW(), ?, ?, ?)',
[token, expiresAt, ip || '0.0.0.0', userId],
)
@@ -39,7 +39,7 @@ export async function requestSecurityToken(session: SessionData, ip: string): Pr
await sendMail(email, 'Token de seguridad - Nova WoW', securityTokenEmailHtml(email, token, ip || '0.0.0.0'))
const [created] = await db(DB.default).query<RowDataPacket[]>(
'SELECT created_at FROM home_securitytoken WHERE user_id = ? ORDER BY created_at DESC LIMIT 1',
'SELECT created_at FROM securitytoken WHERE user_id = ? ORDER BY created_at DESC LIMIT 1',
[userId],
)
return { success: true, tokenDate: created[0] ? new Date(created[0].created_at).toISOString() : undefined }
@@ -48,7 +48,7 @@ export async function requestSecurityToken(session: SessionData, ip: string): Pr
/** Comprueba que el token coincide con el de la cuenta. */
export async function checkSecurityToken(userId: number, token: string): Promise<boolean> {
const [rows] = await db(DB.default).query<RowDataPacket[]>(
'SELECT token FROM home_securitytoken WHERE user_id = ? ORDER BY created_at DESC LIMIT 1',
'SELECT token FROM securitytoken WHERE user_id = ? ORDER BY created_at DESC LIMIT 1',
[userId],
)
return Boolean(rows[0] && rows[0].token === token)
+1 -1
View File
@@ -25,7 +25,7 @@ export const CARD_MIN_EUR: Record<'stripe' | 'sumup', number> = {
/**
* Máximo de copias de un mismo ítem por línea del carrito (igual que el MAX_QTY
* de send-gift). OJO con la diferencia: `copies` es cuántas VECES compras la
* línea, y no se confunde con `home_store_item.quantity`, que es el tamaño del
* línea, y no se confunde con `store_item.quantity`, que es el tamaño del
* lote que entrega cada copia (Paño de lino = 20). 2 copias de Paño de lino =
* 40 telas, y se envían como dos entradas `2589:20`.
*/
+19 -19
View File
@@ -7,7 +7,7 @@ import { MAX_COPIES, PD_PER_EUR, PV_PER_EUR, storeEuroTotal as pureEuroTotal } f
/**
* Tienda de ítems (transfiguración/equipo), portada del sistema antiguo (BENNU).
* Catálogo en `home_store_category` (árbol por `code` "1-1-1") + `home_store_item`
* Catálogo en `store_category` (árbol por `code` "1-1-1") + `store_item`
* (precio en PD o PV, cantidad, icono). Se paga con el saldo PD/PV de la cuenta y
* los ítems se envían por correo al personaje elegido (SOAP `.send items`).
*/
@@ -22,7 +22,7 @@ export function isValidCharName(name: string): boolean {
const MAIL_ITEM_LIMIT = 12 // máximo de ítems por correo en AzerothCore
export interface StoreItem {
id: number // fila de home_store_item (clave del carrito)
id: number // fila de store_item (clave del carrito)
itemId: number
name: string
icon: string | null
@@ -114,10 +114,10 @@ export function parseCategoryName(html: string): NamePart[] {
/**
* Árbol completo de la tienda (categorías anidadas con sus ítems en las hojas).
*
* El nombre del ítem sale de `home_item_data` (extraído de los DB2 del cliente
* El nombre del ítem sale de `item_data` (extraído de los DB2 del cliente
* 3.4.3, ver sql/db2/), que es el ÚNICO sitio con el nombre en inglés: el
* `home_store_item.name` del catálogo original es solo español. El de la
* categoría sale de `home_store_category.name_en` (ver sql/store_category_en.sql).
* `store_item.name` del catálogo original es solo español. El de la
* categoría sale de `store_category.name_en` (ver sql/store_category_en.sql).
* En ambos casos, si falta el nombre en ese idioma se cae al del catálogo, que
* siempre está.
*/
@@ -132,13 +132,13 @@ export async function getStoreCatalog(locale: string): Promise<StoreCategory[]>
try {
;[cats] = await db(DB.default).query<CatRow[]>(
`SELECT code, parent_code, COALESCE(NULLIF(${catNameCol}, ''), name) AS name, css, depth, sort
FROM home_store_category ORDER BY sort`,
FROM store_category ORDER BY sort`,
)
;[items] = await db(DB.default).query<ItemRow[]>(
`SELECT i.id, i.category_code, i.item_id, COALESCE(NULLIF(d.${nameCol}, ''), i.name) AS name,
i.icon, i.quantity, i.currency, i.price, i.preview, i.sort
FROM home_store_item i
LEFT JOIN home_item_data d ON d.entry = i.item_id
FROM store_item i
LEFT JOIN item_data d ON d.entry = i.item_id
ORDER BY i.sort`,
)
} catch {
@@ -184,7 +184,7 @@ export async function getStoreBalances(accountId: number): Promise<{ dp: number;
if (!accountId) return { dp: 0, vp: 0 }
try {
const [rows] = await db(DB.default).query<RowDataPacket[]>(
'SELECT dp, vp FROM home_api_points WHERE accountID = ?',
'SELECT dp, vp FROM api_points WHERE accountID = ?',
[accountId],
)
return rows[0] ? { dp: Number(rows[0].dp), vp: Number(rows[0].vp) } : { dp: 0, vp: 0 }
@@ -200,7 +200,7 @@ export interface StoreCartLine {
}
export interface PricedCart {
// `qty` = lote que entrega CADA copia (home_store_item.quantity);
// `qty` = lote que entrega CADA copia (store_item.quantity);
// `copies` = cuántas veces se compra la línea. Total de unidades = qty*copies.
// `name` = nombre del catálogo; lo usa el concepto del cobro con tarjeta.
lines: { id: number; itemId: number; name: string; qty: number; currency: Currency; price: number; copies: number }[]
@@ -217,13 +217,13 @@ export interface PricedCart {
*
* OJO: la IP va aquí a propósito (identificar al pagador si hay fraude o una
* devolución a mano), pero esto se la manda a la pasarela; hasta ahora solo se
* guardaba en `home_stripelog.acore_ip`. Se omite si no se conoce, y también el
* guardaba en `stripelog.acore_ip`. Se omite si no se conoce, y también el
* 0.0.0.0 de relleno, que no dice nada.
*
* Se recorta a `MAX_CONCEPT`: Stripe y SumUp limitan el campo (~250-255), y un
* carrito de 100 líneas se pasaría de largo. Se recortan solo los ÍTEMS, nunca
* la cabecera: quién ha pagado importa más que el listado completo, que además
* queda entero en `home_store_order`.
* queda entero en `store_order`.
*/
const MAX_CONCEPT = 200
@@ -255,7 +255,7 @@ export function storeConcept(
/**
* Valida el carrito contra la BD (precios/cantidades/moneda REALES, nunca del
* cliente) a partir de las filas `home_store_item.id` y las copias pedidas.
* cliente) a partir de las filas `store_item.id` y las copias pedidas.
* Devuelve null si está vacío, si alguna copia no es válida o si algún id no
* existe en el catálogo (mismo criterio que `priceCart` de send-gift).
*/
@@ -274,7 +274,7 @@ export async function priceStoreCart(cart: StoreCartLine[]): Promise<PricedCart
let rows: ItemRow[] = []
try {
;[rows] = await db(DB.default).query<ItemRow[]>(
`SELECT id, item_id, name, quantity, currency, price FROM home_store_item WHERE id IN (${ids.map(() => '?').join(',')})`,
`SELECT id, item_id, name, quantity, currency, price FROM store_item WHERE id IN (${ids.map(() => '?').join(',')})`,
ids,
)
} catch {
@@ -353,14 +353,14 @@ export async function purchaseStoreCart(
try {
await conn.beginTransaction()
const [pts] = await conn.query<RowDataPacket[]>(
'SELECT dp, vp FROM home_api_points WHERE accountID = ? FOR UPDATE',
'SELECT dp, vp FROM api_points WHERE accountID = ? FOR UPDATE',
[accountId],
)
const dp = pts[0] ? Number(pts[0].dp) : 0
const vp = pts[0] ? Number(pts[0].vp) : 0
if (dp < cart.pdTotal) { await conn.rollback(); return { success: false, error: 'insufficientPd' } }
if (vp < cart.vpTotal) { await conn.rollback(); return { success: false, error: 'insufficientVp' } }
await conn.query('UPDATE home_api_points SET dp = dp - ?, vp = vp - ? WHERE accountID = ?', [cart.pdTotal, cart.vpTotal, accountId])
await conn.query('UPDATE api_points SET dp = dp - ?, vp = vp - ? WHERE accountID = ?', [cart.pdTotal, cart.vpTotal, accountId])
await conn.commit()
charged = true
} catch {
@@ -382,7 +382,7 @@ export async function purchaseStoreCart(
const vpBack = back('pv')
if (charged && (pdBack > 0 || vpBack > 0)) {
await db(DB.default)
.query('UPDATE home_api_points SET dp = dp + ?, vp = vp + ? WHERE accountID = ?', [pdBack, vpBack, accountId])
.query('UPDATE api_points SET dp = dp + ?, vp = vp + ? WHERE accountID = ?', [pdBack, vpBack, accountId])
.catch(() => {})
}
// Si no salió ni un correo es el fallo de siempre (SOAP caído). Si salió
@@ -430,7 +430,7 @@ export async function createStoreOrder(
const amount = storeEuroTotal(cart.pdTotal, cart.vpTotal)
try {
await db(DB.default).query(
'INSERT INTO home_store_order (ref, account_id, character_name, items, amount) VALUES (?, ?, ?, ?, ?)',
'INSERT INTO store_order (ref, account_id, character_name, items, amount) VALUES (?, ?, ?, ?, ?)',
[ref, accountId, character, items, amount],
)
return true
@@ -458,7 +458,7 @@ export async function fulfillStoreOrder(
let rows: RowDataPacket[] = []
try {
;[rows] = await db(DB.default).query<RowDataPacket[]>(
'SELECT character_name, items, amount FROM home_store_order WHERE ref = ?',
'SELECT character_name, items, amount FROM store_order WHERE ref = ?',
[ref],
)
} catch {
+5 -5
View File
@@ -9,7 +9,7 @@ const stripe = new Stripe(process.env.STRIPE_SECRET_KEY as string)
*
* `amountEur` omitido = todo. Stripe cobra en la unidad mínima, así que el
* importe va en céntimos. El reembolso se pide sobre el PaymentIntent de la
* sesión de checkout, que es lo que guardamos en `home_stripelog`.
* sesión de checkout, que es lo que guardamos en `stripelog`.
*/
export async function refundStripeSession(sessionId: string, amountEur?: number): Promise<boolean> {
if (!sessionId) return false
@@ -77,7 +77,7 @@ export async function createCheckoutSession(
})
const mode = (process.env.STRIPE_SECRET_KEY || '').startsWith('sk_live') ? 'live' : 'test'
await db(DB.default).query(
'INSERT INTO home_stripelog (account_id, username, email, acore_ip, stripe_ip, product_name, amount, session_id, mode, character_name, timestamp, fulfilled) ' +
'INSERT INTO stripelog (account_id, username, email, acore_ip, stripe_ip, product_name, amount, session_id, mode, character_name, timestamp, fulfilled) ' +
'VALUES (?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, NOW(), 0)',
[p.accountId, p.username, p.email, p.acoreIp, p.productName, p.amount, session.id, mode, p.characterName],
)
@@ -105,7 +105,7 @@ export async function claimPaidCheckout(
return null
}
const [rows] = await db(DB.default).query<RowDataPacket[]>(
'SELECT id, character_name FROM home_stripelog WHERE session_id = ?',
'SELECT id, character_name FROM stripelog WHERE session_id = ?',
[sessionId],
)
const log = rows[0]
@@ -113,7 +113,7 @@ export async function claimPaidCheckout(
// Reclamo ATÓMICO: solo gana quien pasa fulfilled 0->1. Evita doble entrega si
// el webhook y la página de éxito corren a la vez.
const [res] = await db(DB.default).query<ResultSetHeader>(
'UPDATE home_stripelog SET fulfilled = 1 WHERE id = ? AND fulfilled = 0',
'UPDATE stripelog SET fulfilled = 1 WHERE id = ? AND fulfilled = 0',
[log.id],
)
if (res.affectedRows === 0) return null
@@ -123,7 +123,7 @@ export async function claimPaidCheckout(
/** Registra la IP de Stripe y actualiza el timestamp del log (como el webhook de Django). */
export async function recordStripeIp(sessionId: string, ip: string): Promise<void> {
if (!sessionId) return
await db(DB.default).query('UPDATE home_stripelog SET stripe_ip = ?, timestamp = NOW() WHERE session_id = ?', [
await db(DB.default).query('UPDATE stripelog SET stripe_ip = ?, timestamp = NOW() WHERE session_id = ?', [
ip.slice(0, 45),
sessionId,
])
+6 -6
View File
@@ -43,7 +43,7 @@ interface CreateParams {
}
/**
* Crea un checkout hospedado de SumUp y registra el pago en `home_stripelog`
* Crea un checkout hospedado de SumUp y registra el pago en `stripelog`
* (reutilizamos la tabla de logs; `mode = 'sumup'`, `session_id = reference`).
* Devuelve la URL del checkout hospedado para redirigir al usuario.
*/
@@ -69,7 +69,7 @@ export async function createSumUpCheckout(p: CreateParams): Promise<{ success: b
if (!url) return { success: false, error: 'noHostedUrl' }
await db(DB.default).query(
'INSERT INTO home_stripelog (account_id, username, email, acore_ip, stripe_ip, product_name, amount, session_id, mode, character_name, service, metadata, timestamp, fulfilled) ' +
'INSERT INTO stripelog (account_id, username, email, acore_ip, stripe_ip, product_name, amount, session_id, mode, character_name, service, metadata, timestamp, fulfilled) ' +
'VALUES (?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, NOW(), 0)',
[
p.accountId,
@@ -170,7 +170,7 @@ export async function fulfillSumUpCheckout(
if (!paid) return { ok: false, paid: false }
const [rows] = await db(DB.default).query<RowDataPacket[]>(
'SELECT id, account_id, amount, service, character_name, metadata FROM home_stripelog WHERE session_id = ? AND mode = ?',
'SELECT id, account_id, amount, service, character_name, metadata FROM stripelog WHERE session_id = ? AND mode = ?',
[reference, 'sumup'],
)
const log = rows[0]
@@ -178,7 +178,7 @@ export async function fulfillSumUpCheckout(
// Reclamo atómico: sólo gana quien pasa fulfilled 0->1.
const [res] = await db(DB.default).query<ResultSetHeader>(
'UPDATE home_stripelog SET fulfilled = 1 WHERE id = ? AND fulfilled = 0',
'UPDATE stripelog SET fulfilled = 1 WHERE id = ? AND fulfilled = 0',
[log.id],
)
if (res.affectedRows === 0) return { ok: false, paid: true }
@@ -215,7 +215,7 @@ export async function fulfillSumUpCheckout(
/**
* Reconciliación: SumUp ya no ofrece webhooks, así que la entrega no puede
* depender de que el navegador vuelva al `return_url`. Esta función recorre los
* checkouts SumUp pendientes (fulfilled=0) registrados en `home_stripelog`,
* checkouts SumUp pendientes (fulfilled=0) registrados en `stripelog`,
* consulta su estado en la API de SumUp y acredita los que estén PAID.
*
* - Sin `accountId`: backstop global (para un cron). Con `accountId`: reconcilia
@@ -227,7 +227,7 @@ export async function reconcileSumUpCheckouts(opts?: { accountId?: number; maxAg
if (!sumupConfigured()) return 0
const maxAgeDays = opts?.maxAgeDays ?? 7
let sql =
"SELECT session_id FROM home_stripelog WHERE mode = 'sumup' AND fulfilled = 0 AND timestamp > (NOW() - INTERVAL ? DAY)"
"SELECT session_id FROM stripelog WHERE mode = 'sumup' AND fulfilled = 0 AND timestamp > (NOW() - INTERVAL ? DAY)"
const params: (string | number)[] = [maxAgeDays]
if (opts?.accountId) {
sql += ' AND account_id = ?'
+9 -9
View File
@@ -54,7 +54,7 @@ function generateCode(): string {
async function expireOldCodes(): Promise<void> {
try {
await db(DB.default).query(
"UPDATE home_tradecode SET status = 'expired' WHERE status = 'active' AND expires_at <= NOW()",
"UPDATE tradecode SET status = 'expired' WHERE status = 'active' AND expires_at <= NOW()",
)
} catch {
/* ignore */
@@ -102,12 +102,12 @@ export async function createTradeCode(params: {
// para no poder crear códigos que no se puedan respaldar.
try {
const [bal] = await db(DB.default).query<RowDataPacket[]>(
'SELECT dp FROM home_api_points WHERE accountID = ?',
'SELECT dp FROM api_points WHERE accountID = ?',
[creatorAccount],
)
const balance = bal[0] ? Number(bal[0].dp) : 0
const [act] = await db(DB.default).query<RowDataPacket[]>(
"SELECT COALESCE(SUM(points), 0) AS reserved FROM home_tradecode WHERE creator_account = ? AND status = 'active' AND expires_at > NOW()",
"SELECT COALESCE(SUM(points), 0) AS reserved FROM tradecode WHERE creator_account = ? AND status = 'active' AND expires_at > NOW()",
[creatorAccount],
)
const reserved = Number(act[0]?.reserved ?? 0)
@@ -120,7 +120,7 @@ export async function createTradeCode(params: {
const expiresAt = new Date(Date.now() + CODE_TTL_MS)
try {
await db(DB.default).query(
`INSERT INTO home_tradecode
`INSERT INTO tradecode
(code, creator_account, creator_character, points, gold, status, created_at, expires_at)
VALUES (?, ?, ?, ?, ?, 'active', NOW(), ?)`,
[code, creatorAccount, creatorCharacter, points, gold, expiresAt],
@@ -138,7 +138,7 @@ export async function checkTradeCode(code: string): Promise<{ success: boolean;
await expireOldCodes()
try {
const [rows] = await db(DB.default).query<RowDataPacket[]>(
"SELECT points, gold, status, expires_at FROM home_tradecode WHERE code = ? LIMIT 1",
"SELECT points, gold, status, expires_at FROM tradecode WHERE code = ? LIMIT 1",
[trimmed],
)
const row = rows[0]
@@ -175,7 +175,7 @@ export async function redeemTradeCode(params: {
// Bloqueo de 5 s: la cuenta no puede canjear si acaba de canjear otro código.
try {
const [recent] = await db(DB.default).query<RowDataPacket[]>(
`SELECT id FROM home_tradecode
`SELECT id FROM tradecode
WHERE redeemer_account = ? AND redeemed_at > (NOW() - INTERVAL ? SECOND) LIMIT 1`,
[buyerAccount, REDEEM_LOCK_SECONDS],
)
@@ -193,7 +193,7 @@ export async function redeemTradeCode(params: {
let gold = 0
try {
const [rows] = await db(DB.default).query<RowDataPacket[]>(
"SELECT creator_account, creator_character, points, gold, status, expires_at FROM home_tradecode WHERE code = ? LIMIT 1",
"SELECT creator_account, creator_character, points, gold, status, expires_at FROM tradecode WHERE code = ? LIMIT 1",
[trimmedCode],
)
const row = rows[0]
@@ -223,7 +223,7 @@ export async function redeemTradeCode(params: {
let claimed = false
try {
const [res] = await db(DB.default).query<ResultSetHeader>(
`UPDATE home_tradecode
`UPDATE tradecode
SET status = 'redeemed', redeemer_account = ?, redeemer_character = ?, redeemed_at = NOW()
WHERE code = ? AND status = 'active' AND expires_at > NOW()`,
[buyerAccount, buyerCharacter, trimmedCode],
@@ -237,7 +237,7 @@ export async function redeemTradeCode(params: {
const revertClaim = async () => {
try {
await db(DB.default).query(
"UPDATE home_tradecode SET status = 'active', redeemer_account = NULL, redeemer_character = NULL, redeemed_at = NULL WHERE code = ?",
"UPDATE tradecode SET status = 'active', redeemer_account = NULL, redeemer_character = NULL, redeemed_at = NULL WHERE code = ?",
[trimmedCode],
)
} catch {
+3 -3
View File
@@ -2,7 +2,7 @@ import type { RowDataPacket } from 'mysql2'
import { db, DB } from './db'
import { buildConcept } from './tx-concept'
/** Una transacción de pago (Stripe o SumUp) registrada en home_stripelog. */
/** Una transacción de pago (Stripe o SumUp) registrada en stripelog. */
export interface PaymentTx {
id: string // referencia de la pasarela (session_id / checkout_reference)
platform: 'Stripe' | 'SumUp'
@@ -16,7 +16,7 @@ export interface PaymentTx {
}
/**
* Transacciones de pago de una cuenta desde `home_stripelog`, limitadas a las
* Transacciones de pago de una cuenta desde `stripelog`, limitadas a las
* dos pasarelas activas: **Stripe** (mode test/live) y **SumUp** (mode sumup).
* `fulfilled` 1 -> PAID (pagado y entregado), 0 -> PENDING. Orden por fecha desc.
*/
@@ -24,7 +24,7 @@ export async function getPaymentTransactions(accountId: number, limit = 200): Pr
if (!accountId) return []
try {
const [rows] = await db(DB.default).query<RowDataPacket[]>(
'SELECT session_id, product_name, character_name, amount, mode, service, metadata, fulfilled, timestamp FROM home_stripelog ' +
'SELECT session_id, product_name, character_name, amount, mode, service, metadata, fulfilled, timestamp FROM stripelog ' +
"WHERE account_id = ? AND mode IN ('sumup', 'test', 'live') ORDER BY timestamp DESC LIMIT ?",
[accountId, limit],
)
+1 -1
View File
@@ -69,7 +69,7 @@ export async function checkTransferEligibility(
// Bloqueo de 5 s tras una transferencia reciente de esta cuenta.
try {
const [recent] = await db(DB.default).query<RowDataPacket[]>(
"SELECT id FROM home_stripelog WHERE account_id = ? AND service = 'transfer' AND fulfilled = 1 AND timestamp > (NOW() - INTERVAL 5 SECOND) LIMIT 1",
"SELECT id FROM stripelog WHERE account_id = ? AND service = 'transfer' AND fulfilled = 1 AND timestamp > (NOW() - INTERVAL 5 SECOND) LIMIT 1",
[accountId],
)
if (recent[0]) return { ok: false, message: 'Espera unos segundos antes de transferir otro personaje.' }
+3 -3
View File
@@ -1,8 +1,8 @@
// Concepto traducible de una transacción de home_stripelog, compartido por los
// Concepto traducible de una transacción de stripelog, compartido por los
// historiales de PD/PV y de transacciones. Devuelve una CLAVE (+ argumentos) que
// la página traduce; `conceptRaw` es la reserva para pagos heredados sin servicio.
/** Slug de servicio (home_stripelog.service) -> clave de concepto (History.points.concept.*). */
/** Slug de servicio (stripelog.service) -> clave de concepto (History.points.concept.*). */
export const SERVICE_CONCEPT: Record<string, string> = {
rename: 'rename',
customize: 'customize',
@@ -37,7 +37,7 @@ export interface Concept {
}
/**
* Concepto de una fila de home_stripelog: compra de PD -> "{n} PD"; servicio
* Concepto de una fila de stripelog: compra de PD -> "{n} PD"; servicio
* conocido -> "{etiqueta}: {personaje}"; en otro caso, texto libre heredado.
*/
export function buildConcept(
+8 -8
View File
@@ -23,7 +23,7 @@ export interface VoteResult {
export async function getVoteSites(): Promise<VoteSite[]> {
try {
const [rows] = await db(DB.default).query<RowDataPacket[]>(
'SELECT id, name, url, image_url, points FROM home_votesite ORDER BY id',
'SELECT id, name, url, image_url, points FROM votesite ORDER BY id',
)
return rows.map((r) => ({ id: r.id, name: r.name, url: r.url, image_url: r.image_url, points: r.points }))
} catch {
@@ -42,7 +42,7 @@ export async function getVoteSitesForAccount(accountId: number): Promise<VoteSit
if (!accountId) return sites.map((s) => ({ ...s, lastVoteAt: null, onCooldown: false }))
try {
const [rows] = await db(DB.default).query<RowDataPacket[]>(
'SELECT vote_site_id, MAX(created_at) AS last FROM home_votelog WHERE account_id = ? GROUP BY vote_site_id',
'SELECT vote_site_id, MAX(created_at) AS last FROM votelog WHERE account_id = ? GROUP BY vote_site_id',
[accountId],
)
const map = new Map(rows.map((r) => [Number(r.vote_site_id), r.last as Date]))
@@ -62,14 +62,14 @@ export async function getVoteSitesForAccount(accountId: number): Promise<VoteSit
export async function registerVote(accountId: number, url: string): Promise<VoteResult> {
const [siteRows] = await db(DB.default).query<RowDataPacket[]>(
'SELECT id, name, points FROM home_votesite WHERE url = ?',
'SELECT id, name, points FROM votesite WHERE url = ?',
[url],
)
const site = siteRows[0]
if (!site) return { success: false, error: 'siteNotFound' }
const [last] = await db(DB.default).query<RowDataPacket[]>(
'SELECT created_at FROM home_votelog WHERE account_id = ? AND vote_site_id = ? ORDER BY created_at DESC LIMIT 1',
'SELECT created_at FROM votelog WHERE account_id = ? AND vote_site_id = ? ORDER BY created_at DESC LIMIT 1',
[accountId, site.id],
)
if (last[0]) {
@@ -81,15 +81,15 @@ export async function registerVote(accountId: number, url: string): Promise<Vote
}
// Acreditar PV
const [pts] = await db(DB.default).query<RowDataPacket[]>('SELECT id FROM home_api_points WHERE accountID = ?', [accountId])
const [pts] = await db(DB.default).query<RowDataPacket[]>('SELECT id FROM api_points WHERE accountID = ?', [accountId])
if (pts[0]) {
await db(DB.default).query('UPDATE home_api_points SET vp = vp + ? WHERE accountID = ?', [site.points, accountId])
await db(DB.default).query('UPDATE api_points SET vp = vp + ? WHERE accountID = ?', [site.points, accountId])
} else {
await db(DB.default).query('INSERT INTO home_api_points (accountID, vp, dp) VALUES (?, ?, 0)', [accountId, site.points])
await db(DB.default).query('INSERT INTO api_points (accountID, vp, dp) VALUES (?, ?, 0)', [accountId, site.points])
}
await db(DB.default).query(
'INSERT INTO home_votelog (account_id, vote_site_id, created_at, last_vote_time, processed) VALUES (?, ?, NOW(), NOW(), 0)',
'INSERT INTO votelog (account_id, vote_site_id, created_at, last_vote_time, processed) VALUES (?, ?, NOW(), NOW(), 0)',
[accountId, site.id],
)