send-gift: rediseño al original — es la tienda, pero regalando
El original lo dice en su propio texto ("los objetos disponibles son los mismos
de la tienda") y lo confirma su JS, que usa #store-list y .store-add-button: el
regalo NO tiene catálogo propio, es la tienda con el correo a otro personaje.
Nosotros teníamos una tabla aparte (home_item) con 8 objetos y precios en euros.
Ahora send-gift monta StoreBrowser en modo `gift`: mismo catálogo (3068 ítems,
PD/PV), mismo carrito con cantidades y las cuatro formas de pago. Antes NO tenía
Stripe: solo SumUp, PD y PV.
Dos pasos, como el original: primero #char-select-div (personaje de origen,
destino, confirmación y token) y solo al pulsar "Mostrar Regalos" aparece el
catálogo. Se borran SendGiftForm, /api/gift/checkout y lib/gift (ya no los usa
nadie); la tabla home_item se queda en la BD, sin usar.
Tres cosas que el usuario pidió y que estaban mal:
- El formulario va con `noValidate`: los avisos los damos nosotros en rojo
(#show-gif-response), no el navegador.
- "Mostrar Regalos" ahora valida contra el SERVIDOR (que el personaje de origen
sea tuyo, que el destino exista y que el token sea correcto). Antes elegías
objetos para descubrir al final que el destino no existía.
- BUG REAL: el nombre del destino distinguía mayúsculas. `characters.name` es
`utf8mb4_bin`, así que "innakh" NO encontraba a "Innakh" (comprobado: 0
resultados; con COLLATE, 1). `findCharacterByName` busca sin distinguir y
devuelve el nombre CANÓNICO, que es el que va al comando SOAP.
La validación vive en lib/gift-check y la usan las dos rutas: /api/gift/check (el
botón) y /api/gift/send, que revalida porque el cliente puede saltarse el paso 1.
También se quita del texto "El pago se realiza mediante SumUp": el original no lo
dice y además ya era falso con cuatro formas de pago.
Verificado: innakh / INNAKH / InNaKh resuelven a "Innakh"; token malo y destino
inexistente salen en rojo sin pasar al catálogo. Con 500 PD de saldo de prueba,
2 copias de un ítem de 200 pasan el cobro (deliveryFailed por el worldserver
caído, con su reembolso) y 3 dan insufficientPd: el servidor cobra por copias.
Datos de prueba borrados.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+33
-5
@@ -14,6 +14,11 @@ import { MAX_COPIES, PD_PER_EUR, PV_PER_EUR, storeEuroTotal as pureEuroTotal } f
|
||||
|
||||
export type Currency = 'pd' | 'pv'
|
||||
const SAFE_NAME = /^[A-Za-z]{1,12}$/
|
||||
|
||||
/** Nombre de personaje válido de WoW. Evita inyección en el comando SOAP. */
|
||||
export function isValidCharName(name: string): boolean {
|
||||
return SAFE_NAME.test(name)
|
||||
}
|
||||
const MAIL_ITEM_LIMIT = 12 // máximo de ítems por correo en AzerothCore
|
||||
|
||||
export interface StoreItem {
|
||||
@@ -291,6 +296,23 @@ export async function priceStoreCart(cart: StoreCartLine[]): Promise<PricedCart
|
||||
return { lines, pdTotal: sum('pd'), vpTotal: sum('pv') }
|
||||
}
|
||||
|
||||
/**
|
||||
* Texto del correo. El regalo lo cambia para decir quién lo envía; sin esto el
|
||||
* destinatario recibiría un correo de "Tienda" sin saber de quién viene.
|
||||
*/
|
||||
export interface Mail {
|
||||
subject: string
|
||||
body: string
|
||||
}
|
||||
|
||||
/** Correo de un regalo: dice quién lo manda (como el original). */
|
||||
export function giftMail(sender: string): Mail {
|
||||
return {
|
||||
subject: 'Regalo',
|
||||
body: `¡Has recibido un regalo de ${sender}! ¡Que lo disfrutes!`,
|
||||
}
|
||||
}
|
||||
|
||||
/** Entrada de correo: un lote de un ítem, con lo que costó (para reembolsar). */
|
||||
interface CartEntry {
|
||||
i: number
|
||||
@@ -320,6 +342,7 @@ export async function purchaseStoreCart(
|
||||
accountId: number,
|
||||
character: string,
|
||||
cart: PricedCart,
|
||||
mail?: Mail,
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
if (!SAFE_NAME.test(character)) return { success: false, error: 'invalidCharacter' }
|
||||
if (cart.pdTotal <= 0 && cart.vpTotal <= 0) return { success: false, error: 'emptyCart' }
|
||||
@@ -351,7 +374,7 @@ export async function purchaseStoreCart(
|
||||
// lo que no se envió: los correos que ya salieron no se pueden recuperar, y
|
||||
// devolver el carrito entero regalaría lo ya entregado.
|
||||
const entries = cartEntries(cart)
|
||||
const sent = await sendStoreItems(character, entries)
|
||||
const sent = await sendStoreItems(character, entries, mail)
|
||||
if (sent < entries.length) {
|
||||
const left = entries.slice(sent)
|
||||
const back = (c: Currency) => left.filter((e) => e.currency === c).reduce((s, e) => s + e.price, 0)
|
||||
@@ -429,6 +452,7 @@ export async function createStoreOrder(
|
||||
export async function fulfillStoreOrder(
|
||||
character: string,
|
||||
ref: string,
|
||||
mail?: Mail,
|
||||
): Promise<{ ok: boolean; refundEur?: number }> {
|
||||
if (!ref) return { ok: false }
|
||||
let rows: RowDataPacket[] = []
|
||||
@@ -457,7 +481,7 @@ export async function fulfillStoreOrder(
|
||||
if (entries.length === 0) return { ok: false }
|
||||
const priced = entries.every((e) => e.price > 0 && (e.currency === 'pd' || e.currency === 'pv'))
|
||||
|
||||
const sent = await sendStoreItems(target, entries)
|
||||
const sent = await sendStoreItems(target, entries, mail)
|
||||
if (sent === entries.length) return { ok: true }
|
||||
|
||||
if (!priced) {
|
||||
@@ -486,13 +510,17 @@ export async function fulfillStoreOrder(
|
||||
* resto. Cada correo es un `.send items`, así que el troceo es atómico por
|
||||
* correo: un chunk sale entero o no sale.
|
||||
*/
|
||||
async function sendStoreItems(character: string, items: { i: number; q: number }[]): Promise<number> {
|
||||
async function sendStoreItems(
|
||||
character: string,
|
||||
items: { i: number; q: number }[],
|
||||
mail?: Mail,
|
||||
): Promise<number> {
|
||||
if (!SAFE_NAME.test(character)) return 0
|
||||
// Los datos salen de la BD: si algo no cuadra es un bug, y se prefiere no
|
||||
// enviar nada a enviar de menos y descuadrar el reembolso por índice.
|
||||
if (items.length === 0 || items.some((it) => !(Number(it.i) > 0) || !(Number(it.q) >= 1))) return 0
|
||||
const subject = 'Tienda'
|
||||
const body = 'Has recibido los objetos de la tienda. ¡Que los disfrutes!'
|
||||
const subject = mail?.subject ?? 'Tienda'
|
||||
const body = mail?.body ?? 'Has recibido los objetos de la tienda. ¡Que los disfrutes!'
|
||||
let sent = 0
|
||||
for (let n = 0; n < items.length; n += MAIL_ITEM_LIMIT) {
|
||||
const chunk = items.slice(n, n + MAIL_ITEM_LIMIT)
|
||||
|
||||
Reference in New Issue
Block a user