import type { RowDataPacket } from 'mysql2' import { db, DB } from './db' import { executeSoapCommand } from './soap' export interface GiftItem { id: number // PK en home_item (identificador del carrito) itemId: number // id del ítem en el juego name: string price: number // precio en euros imageUrl: string } export interface GiftCategory { id: number name: string items: GiftItem[] } /** Nombre de personaje válido de WoW (letras, 1-12). Evita inyección en el SOAP. */ const SAFE_NAME = /^[A-Za-z]{1,12}$/ /** Cantidad máxima por línea del carrito. */ export const MAX_QTY = 100 /** AzerothCore entrega como máximo 12 ítems por correo. */ const MAIL_ITEM_LIMIT = 12 export function isValidCharName(name: string): boolean { return SAFE_NAME.test(name) } /** Catálogo de regalos (= ítems de la tienda), agrupado por categoría. */ export async function getGiftCatalog(): Promise { try { const [rows] = await db(DB.default).query( `SELECT c.id AS cat_id, c.name AS cat_name, i.id, i.item_id, i.name, i.price, i.image_url FROM home_category c JOIN home_item i ON i.category_id = c.id ORDER BY c.name, i.name`, ) const byCat = new Map() for (const r of rows) { let cat = byCat.get(r.cat_id) if (!cat) { cat = { id: r.cat_id, name: r.cat_name, items: [] } byCat.set(r.cat_id, cat) } cat.items.push({ id: r.id, itemId: r.item_id, name: r.name, price: Number(r.price), imageUrl: r.image_url }) } return [...byCat.values()] } catch { return [] } } export interface CartLine { id: number qty: number } export interface PricedLine { itemId: number name: string qty: number unit: number subtotal: number } export interface PricedCart { lines: PricedLine[] total: number } /** * Valida un carrito contra los PRECIOS REALES de la BD y calcula el total. * Nunca se confía en el precio que envía el cliente. Devuelve null si el * carrito está vacío, tiene cantidades no válidas o algún ítem no existe. */ export async function priceCart(cart: CartLine[]): Promise { const qtyById = new Map() for (const l of cart) { const id = Math.floor(Number(l?.id)) const qty = Math.floor(Number(l?.qty)) if (!Number.isInteger(id) || id <= 0) return null if (!Number.isInteger(qty) || qty < 1 || qty > MAX_QTY) return null qtyById.set(id, Math.min(MAX_QTY, (qtyById.get(id) ?? 0) + qty)) } if (qtyById.size === 0) return null const ids = [...qtyById.keys()] let rows: RowDataPacket[] try { ;[rows] = await db(DB.default).query( `SELECT id, item_id, name, price FROM home_item WHERE id IN (${ids.map(() => '?').join(',')})`, ids, ) } catch { return null } if (rows.length !== ids.length) return null // algún id no existe en el catálogo const lines: PricedLine[] = [] let total = 0 for (const r of rows) { const qty = qtyById.get(Number(r.id))! const unit = Number(r.price) const subtotal = Math.round(unit * qty * 100) / 100 total += subtotal lines.push({ itemId: Number(r.item_id), name: r.name, qty, unit, subtotal }) } return { lines, total: Math.round(total * 100) / 100 } } /** * Entrega el regalo: envía los ítems por correo al personaje destino con SOAP * (`.send items`, igual que la tienda). Trocea en correos de máximo 12 ítems * (límite de AzerothCore). `items` = pares { i: item_id, q: cantidad }. * Requiere el worldserver arriba; devuelve false si algún correo falla. */ export async function sendGiftByMail( destination: string, sender: string, items: { i: number; q: number }[], ): Promise { if (!SAFE_NAME.test(destination)) return false const clean = (Array.isArray(items) ? items : []) .map((it) => ({ i: Math.floor(Number(it?.i)), q: Math.floor(Number(it?.q)) })) .filter((it) => Number.isInteger(it.i) && it.i > 0 && Number.isInteger(it.q) && it.q >= 1 && it.q <= MAX_QTY) if (clean.length === 0) return false const subject = 'Has recibido un regalo' const fromLabel = SAFE_NAME.test(sender) ? sender : 'un amigo' const body = `Regalo enviado por ${fromLabel}. Que lo disfrutes.` for (let n = 0; n < clean.length; n += MAIL_ITEM_LIMIT) { const chunk = clean.slice(n, n + MAIL_ITEM_LIMIT) const itemsStr = chunk.map((it) => `${it.i}:${it.q}`).join(' ') const res = await executeSoapCommand(`.send items "${destination}" "${subject}" "${body}" ${itemsStr}`) if (res === null) return false } return true }