diff --git a/web-next/app/[locale]/trans-history/page.tsx b/web-next/app/[locale]/trans-history/page.tsx index dbf5294..88e719d 100644 --- a/web-next/app/[locale]/trans-history/page.tsx +++ b/web-next/app/[locale]/trans-history/page.tsx @@ -73,22 +73,22 @@ async function PlatformBox({ {t('trans.thCreatedAt')} {t('trans.thAmount')} - {txs.map((t) => ( - + {txs.map((tx) => ( + - {t.id} + {tx.id} - + - {t.concept} + {tx.conceptKey ? t(`points.concept.${tx.conceptKey}`, tx.conceptArgs) : tx.conceptRaw} - {fmt(t.createdAt)} + {fmt(tx.createdAt)} - {t.amount.toFixed(2)} € + {tx.amount.toFixed(2)} € ))} diff --git a/web-next/lib/points-history.ts b/web-next/lib/points-history.ts index d7d0ddd..8dd907b 100644 --- a/web-next/lib/points-history.ts +++ b/web-next/lib/points-history.ts @@ -1,5 +1,6 @@ import type { RowDataPacket } from 'mysql2' import { db, DB } from './db' +import { buildConcept, purchasedPD } from './tx-concept' /** * Un movimiento del historial de PD/PV. Los textos visibles se devuelven como @@ -19,19 +20,6 @@ export interface PointsMovement { 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. */ export async function getPointsBalances(accountId: number): Promise<{ dp: number; pv: number }> { if (!accountId) return { dp: 0, pv: 0 } @@ -46,21 +34,6 @@ export async function getPointsBalances(accountId: number): Promise<{ dp: number } } -/** PD acreditados por un pago (si es una compra de PD), o null si es un servicio/ítem. */ -function purchasedPD(productName: string, service: string | null, metadata: string | null): number | null { - const m = /(\d+)\s*PD\b/i.exec(productName || '') - if (m) return Number(m[1]) - if (service === 'dpoints') { - try { - const p = metadata ? JSON.parse(metadata).points : null - return p ? Number(p) : null - } catch { - return null - } - } - return null -} - /** * Historial unificado de PD/PV de una cuenta, combinando las fuentes reales: * - `home_stripelog`: pagos por **Stripe** (mode test/live) y **SumUp** (mode sumup). @@ -82,27 +55,15 @@ export async function getPointsHistory(accountId: number, limit = 100): Promise< 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 || '') } - } - + const metadata = r.metadata ? String(r.metadata) : null + const c = buildConcept(productName, service, String(r.character_name || ''), metadata) movements.push({ date: new Date(r.timestamp), - conceptKey, - conceptArgs, - conceptRaw: productName, + conceptKey: c.conceptKey, + conceptArgs: c.conceptArgs, + conceptRaw: c.conceptRaw, method: r.mode === 'sumup' ? 'SumUp' : 'Stripe', - pd, + pd: purchasedPD(productName, service, metadata), pv: null, amount: Number(r.amount), status: r.fulfilled ? 'delivered' : 'pending', diff --git a/web-next/lib/trans-history.ts b/web-next/lib/trans-history.ts index 297f2f1..e7b4201 100644 --- a/web-next/lib/trans-history.ts +++ b/web-next/lib/trans-history.ts @@ -1,5 +1,6 @@ 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. */ export interface PaymentTx { @@ -8,7 +9,10 @@ export interface PaymentTx { status: 'PAID' | 'PENDING' createdAt: Date amount: number // importe en € - concept: string + // Concepto traducible (clave + args) con reserva de texto libre. + conceptKey: string | null + conceptArgs: Record + conceptRaw: string } /** @@ -20,18 +24,28 @@ export async function getPaymentTransactions(accountId: number, limit = 200): Pr if (!accountId) return [] try { const [rows] = await db(DB.default).query( - "SELECT session_id, product_name, amount, mode, fulfilled, timestamp FROM home_stripelog " + + 'SELECT session_id, product_name, character_name, amount, mode, service, metadata, fulfilled, timestamp FROM home_stripelog ' + "WHERE account_id = ? AND mode IN ('sumup', 'test', 'live') ORDER BY timestamp DESC LIMIT ?", [accountId, limit], ) - return rows.map((r) => ({ - id: String(r.session_id || ''), - platform: r.mode === 'sumup' ? 'SumUp' : 'Stripe', - status: r.fulfilled ? 'PAID' : 'PENDING', - createdAt: new Date(r.timestamp), - amount: Number(r.amount), - concept: String(r.product_name || '—'), - })) + return rows.map((r) => { + const c = buildConcept( + String(r.product_name || '—'), + r.service ? String(r.service) : null, + String(r.character_name || ''), + r.metadata ? String(r.metadata) : null, + ) + return { + id: String(r.session_id || ''), + platform: r.mode === 'sumup' ? 'SumUp' : 'Stripe', + status: r.fulfilled ? 'PAID' : 'PENDING', + createdAt: new Date(r.timestamp), + amount: Number(r.amount), + conceptKey: c.conceptKey, + conceptArgs: c.conceptArgs, + conceptRaw: c.conceptRaw, + } + }) } catch { return [] } diff --git a/web-next/lib/tx-concept.ts b/web-next/lib/tx-concept.ts new file mode 100644 index 0000000..a26637b --- /dev/null +++ b/web-next/lib/tx-concept.ts @@ -0,0 +1,58 @@ +// Concepto traducible de una transacción de home_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.*). */ +export 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', +} + +/** PD acreditados por un pago (si es una compra de PD), o null si es un servicio/ítem. */ +export function purchasedPD(productName: string, service: string | null, metadata: string | null): number | null { + const m = /(\d+)\s*PD\b/i.exec(productName || '') + if (m) return Number(m[1]) + if (service === 'dpoints') { + try { + const p = metadata ? JSON.parse(metadata).points : null + return p ? Number(p) : null + } catch { + return null + } + } + return null +} + +export interface Concept { + conceptKey: string | null + conceptArgs: Record + conceptRaw: string +} + +/** + * Concepto de una fila de home_stripelog: compra de PD -> "{n} PD"; servicio + * conocido -> "{etiqueta}: {personaje}"; en otro caso, texto libre heredado. + */ +export function buildConcept( + productName: string, + service: string | null, + characterName: string, + metadata: string | null, +): Concept { + const raw = productName || '—' + const pd = purchasedPD(raw, service, metadata) + if (pd != null && (service === 'dpoints' || service === null)) { + return { conceptKey: 'pd', conceptArgs: { n: pd }, conceptRaw: raw } + } + if (service && SERVICE_CONCEPT[service]) { + return { conceptKey: SERVICE_CONCEPT[service], conceptArgs: { detail: characterName }, conceptRaw: raw } + } + return { conceptKey: null, conceptArgs: {}, conceptRaw: raw } +}