diff --git a/web-next/app/[locale]/points-history/page.tsx b/web-next/app/[locale]/points-history/page.tsx index 0fb2380..f8c41a8 100644 --- a/web-next/app/[locale]/points-history/page.tsx +++ b/web-next/app/[locale]/points-history/page.tsx @@ -89,12 +89,14 @@ export default async function PointsHistoryPage({ params }: { params: Promise<{ {movements.map((m, i) => ( {fmt(m.date)} - {m.concept} - {m.method} + {m.conceptKey ? t(`points.concept.${m.conceptKey}`, m.conceptArgs) : m.conceptRaw} + + {m.method === 'vote' || m.method === 'promo' ? t(`points.method.${m.method}`) : m.method} + {m.pd != null ? `+${m.pd}` : '—'} {m.pv != null ? `+${m.pv}` : '—'} {m.amount != null ? `${m.amount.toFixed(2)} €` : '—'} - {m.status} + {t(`points.status.${m.status}`)} ))} diff --git a/web-next/lib/points-history.ts b/web-next/lib/points-history.ts index 56e12d5..d7d0ddd 100644 --- a/web-next/lib/points-history.ts +++ b/web-next/lib/points-history.ts @@ -1,15 +1,35 @@ import type { RowDataPacket } from 'mysql2' import { db, DB } from './db' -/** Un movimiento del historial de PD/PV (pagos, votos, promociones). */ +/** + * Un movimiento del historial de PD/PV. Los textos visibles se devuelven como + * CLAVES para que la página los traduzca (i18n): `method`, `status` y el concepto + * (`conceptKey` + `conceptArgs`, con `conceptRaw` de reserva para textos libres + * heredados que no tienen clave). + */ export interface PointsMovement { date: Date - concept: string - method: 'Stripe' | 'SumUp' | 'Voto' | 'Promoción' - pd: number | null // PD del movimiento (+), null si no aplica - pv: number | null // PV del movimiento (+), null si no aplica - amount: number | null // importe en € (pagos), null si no aplica - status: string + conceptKey: string | null // p.ej. 'pd','vote','promo','rename','gold'… + conceptArgs: Record + conceptRaw: string // reserva (product_name) cuando conceptKey es null + method: 'Stripe' | 'SumUp' | 'vote' | 'promo' + pd: number | null + pv: number | null + amount: number | null // € (pagos), null si no aplica + status: 'delivered' | 'pending' | 'credited' +} + +/** Slug de servicio (home_stripelog.service) -> clave de concepto traducible. */ +const SERVICE_CONCEPT: Record = { + rename: 'rename', + customize: 'customize', + 'change-race': 'changeRace', + 'change-faction': 'changeFaction', + 'level-up': 'levelUp', + gold: 'gold', + transfer: 'transfer', + 'restore-item': 'restoreItem', + 'send-gift': 'sendGift', } /** Saldos actuales de PD y PV de una cuenta de juego. */ @@ -46,8 +66,7 @@ function purchasedPD(productName: string, service: string | null, metadata: stri * - `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. - * Se ordena por fecha descendente. Cada consulta es tolerante a fallos (devuelve - * [] si la tabla no existe) para no romper la página. + * Ordenado por fecha descendente; cada consulta es tolerante a fallos. */ export async function getPointsHistory(accountId: number, limit = 100): Promise { if (!accountId) return [] @@ -56,20 +75,37 @@ export async function getPointsHistory(accountId: number, limit = 100): Promise< // 1) Pagos (Stripe + SumUp). try { const [rows] = await db(DB.default).query( - 'SELECT product_name, amount, mode, service, metadata, fulfilled, timestamp ' + + 'SELECT product_name, character_name, amount, mode, service, metadata, fulfilled, timestamp ' + 'FROM home_stripelog WHERE account_id = ? ORDER BY timestamp DESC LIMIT ?', [accountId, limit], ) for (const r of rows) { const service = r.service ? String(r.service) : null + const productName = String(r.product_name || '—') + const pd = purchasedPD(productName, service, r.metadata ? String(r.metadata) : null) + + // Concepto traducible: compra de PD -> "{n} PD"; servicio conocido -> + // "{etiqueta}: {personaje}"; en otro caso, texto libre heredado. + let conceptKey: string | null = null + let conceptArgs: Record = {} + if (pd != null && (service === 'dpoints' || service === null)) { + conceptKey = 'pd' + conceptArgs = { n: pd } + } else if (service && SERVICE_CONCEPT[service]) { + conceptKey = SERVICE_CONCEPT[service] + conceptArgs = { detail: String(r.character_name || '') } + } + movements.push({ date: new Date(r.timestamp), - concept: String(r.product_name || '—'), + conceptKey, + conceptArgs, + conceptRaw: productName, method: r.mode === 'sumup' ? 'SumUp' : 'Stripe', - pd: purchasedPD(String(r.product_name || ''), service, r.metadata ? String(r.metadata) : null), + pd, pv: null, amount: Number(r.amount), - status: r.fulfilled ? 'Entregado' : 'Pendiente', + status: r.fulfilled ? 'delivered' : 'pending', }) } } catch { @@ -86,12 +122,14 @@ export async function getPointsHistory(accountId: number, limit = 100): Promise< for (const r of rows) { movements.push({ date: new Date(r.created_at), - concept: `Voto en ${r.name}`, - method: 'Voto', + conceptKey: 'vote', + conceptArgs: { detail: String(r.name || '') }, + conceptRaw: `Voto en ${r.name}`, + method: 'vote', pd: null, pv: Number(r.points) || 0, amount: null, - status: 'Acreditado', + status: 'credited', }) } } catch { @@ -108,12 +146,14 @@ export async function getPointsHistory(accountId: number, limit = 100): Promise< for (const r of rows) { movements.push({ date: new Date(r.redeemed_at), - concept: `Código ${r.code}`, - method: 'Promoción', + conceptKey: 'promo', + conceptArgs: { detail: String(r.code || '') }, + conceptRaw: `Código ${r.code}`, + method: 'promo', pd: Number(r.pd) > 0 ? Number(r.pd) : null, pv: Number(r.pv) > 0 ? Number(r.pv) : null, amount: null, - status: 'Acreditado', + status: 'credited', }) } } catch { diff --git a/web-next/messages/en.json b/web-next/messages/en.json index d344b85..d516a4f 100644 --- a/web-next/messages/en.json +++ b/web-next/messages/en.json @@ -980,7 +980,30 @@ "thConcept": "Concept", "thMethod": "Method", "thAmount": "Amount", - "thStatus": "Status" + "thStatus": "Status", + "concept": { + "pd": "{n} PD", + "vote": "Vote on {detail}", + "promo": "Code {detail}", + "rename": "Rename character: {detail}", + "customize": "Customize character: {detail}", + "changeRace": "Change race: {detail}", + "changeFaction": "Change faction: {detail}", + "levelUp": "Level up to 80: {detail}", + "gold": "Acquire gold: {detail}", + "transfer": "Transfer character: {detail}", + "restoreItem": "Restore item: {detail}", + "sendGift": "Gift for {detail}" + }, + "method": { + "vote": "Vote", + "promo": "Promotion" + }, + "status": { + "delivered": "Delivered", + "pending": "Pending", + "credited": "Credited" + } }, "trans": { "pageTitle": "Transaction history", diff --git a/web-next/messages/es.json b/web-next/messages/es.json index 526fb01..f7ee013 100644 --- a/web-next/messages/es.json +++ b/web-next/messages/es.json @@ -980,7 +980,30 @@ "thConcept": "Concepto", "thMethod": "Método", "thAmount": "Importe", - "thStatus": "Estado" + "thStatus": "Estado", + "concept": { + "pd": "{n} PD", + "vote": "Voto en {detail}", + "promo": "Código {detail}", + "rename": "Renombrar personaje: {detail}", + "customize": "Personalizar personaje: {detail}", + "changeRace": "Cambiar raza: {detail}", + "changeFaction": "Cambiar facción: {detail}", + "levelUp": "Subir a nivel 80: {detail}", + "gold": "Adquirir oro: {detail}", + "transfer": "Transferir personaje: {detail}", + "restoreItem": "Recuperar ítem: {detail}", + "sendGift": "Regalo para {detail}" + }, + "method": { + "vote": "Voto", + "promo": "Promoción" + }, + "status": { + "delivered": "Entregado", + "pending": "Pendiente", + "credited": "Acreditado" + } }, "trans": { "pageTitle": "Historial de transacciones",