store: el concepto del cobro dice quién compra y qué

Ponía "Compra en la tienda (1 objeto)", que no identifica nada. Ahora:

  Tienda · BNet inna@inna.cl · Cuenta 15#1 · Innadin · Grito de la sirena (45327) x2

Cuenta Battle.net, cuenta de juego, personaje, y el nombre de cada ítem con su
id. Es lo único que Stripe/SumUp enseñan en su panel, así que es lo que hay para
identificar un pago cuando toca reclamar o devolver algo a mano.

Se recorta a 200 caracteres porque las dos pasarelas limitan el campo (~250) y
un carrito grande se pasaría. Se recortan solo los ÍTEMS ("+37 más"), nunca la
cabecera: quién ha pagado importa más que el listado, que además queda entero en
home_store_order. El recorte quita ítems hasta que quepa TODO con el sufijo
incluido; comprobarlo antes de añadir el "+N más" se pasaba igual (206 chars con
12 ítems, visto en un checkout real).

priceStoreCart trae ahora también el nombre del ítem, que no leía.

Verificado contra checkouts reales de Stripe (test) con 1, 12 y 40 ítems: 82,
167 y 168 caracteres.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-15 11:36:14 +00:00
parent 457dac83f0
commit 18fe2489d0
2 changed files with 50 additions and 7 deletions
+8 -5
View File
@@ -1,7 +1,7 @@
import { randomUUID } from 'crypto' import { randomUUID } from 'crypto'
import { getSession } from '@/lib/session' import { getSession } from '@/lib/session'
import { getGameCharacters } from '@/lib/characters' import { getGameCharacters } from '@/lib/characters'
import { priceStoreCart, purchaseStoreCart, storeEuroTotal, createStoreOrder, type StoreCartLine } from '@/lib/store' import { priceStoreCart, purchaseStoreCart, storeEuroTotal, createStoreOrder, storeConcept, type StoreCartLine } from '@/lib/store'
import { CARD_MIN_EUR } from '@/lib/store-pricing' import { CARD_MIN_EUR } from '@/lib/store-pricing'
import { createCheckoutSession } from '@/lib/stripe' import { createCheckoutSession } from '@/lib/stripe'
import { createSumUpCheckout, sumupConfigured } from '@/lib/sumup' import { createSumUpCheckout, sumupConfigured } from '@/lib/sumup'
@@ -56,10 +56,13 @@ export async function POST(request: Request) {
const site = process.env.SITE_URL || '' const site = process.env.SITE_URL || ''
const ip = (request.headers.get('x-forwarded-for') || '').split(',')[0].trim() || '0.0.0.0' const ip = (request.headers.get('x-forwarded-for') || '').split(',')[0].trim() || '0.0.0.0'
// Objetos comprados = suma de copias, no número de líneas: 15 copias de un // Concepto con quién compra y qué: es lo único que verá la pasarela en su
// mismo ítem son "15 objetos", no "1 objeto". // panel para identificar el pago (ver `storeConcept`).
const units = cart.lines.reduce((s, l) => s + l.copies, 0) const productName = storeConcept(cart, {
const productName = `Compra en la tienda (${units} objeto${units === 1 ? '' : 's'})` bnetEmail: session.bnetEmail,
account: session.username,
character,
})
const metadata = { service: 'store', order_ref: ref } const metadata = { service: 'store', order_ref: ref }
if (provider === 'sumup') { if (provider === 'sumup') {
+42 -2
View File
@@ -197,11 +197,50 @@ export interface StoreCartLine {
export interface PricedCart { export interface PricedCart {
// `qty` = lote que entrega CADA copia (home_store_item.quantity); // `qty` = lote que entrega CADA copia (home_store_item.quantity);
// `copies` = cuántas veces se compra la línea. Total de unidades = qty*copies. // `copies` = cuántas veces se compra la línea. Total de unidades = qty*copies.
lines: { id: number; itemId: number; qty: number; currency: Currency; price: number; copies: number }[] // `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 }[]
pdTotal: number pdTotal: number
vpTotal: number vpTotal: number
} }
/**
* Concepto del cobro con tarjeta: quién compra y qué. Sale en la pasarela y en
* su panel, así que es lo único que tendrán Stripe/SumUp para identificar un
* pago si hay que reclamar o devolver algo a mano.
*
* Formato: `Tienda · BNet <email> · Cuenta <cuenta> · <personaje> · Nombre (id) x2, …`
*
* 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`.
*/
const MAX_CONCEPT = 200
export function storeConcept(
cart: PricedCart,
opts: { bnetEmail?: string; account?: string; character: string },
): string {
const head = [
'Tienda',
opts.bnetEmail ? `BNet ${opts.bnetEmail}` : null,
opts.account ? `Cuenta ${opts.account}` : null,
opts.character,
]
.filter(Boolean)
.join(' · ')
const parts = cart.lines.map((l) => `${l.name} (${l.itemId})${l.copies > 1 ? ` x${l.copies}` : ''}`)
// Se van quitando ítems por el final hasta que quepa TODO, sufijo incluido: si
// se comprueba antes de añadir el "+N más" el resultado se pasa igualmente.
const fits = (s: string) => `${head} · ${s}`.length <= MAX_CONCEPT
let list = parts.join(', ')
for (let n = parts.length - 1; n >= 0 && !fits(list); n--) {
list = n > 0 ? `${parts.slice(0, n).join(', ')}, +${parts.length - n} más` : `${parts.length} ítems`
}
return fits(list) ? `${head} · ${list}` : head
}
/** /**
* Valida el carrito contra la BD (precios/cantidades/moneda REALES, nunca del * 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 `home_store_item.id` y las copias pedidas.
@@ -223,7 +262,7 @@ export async function priceStoreCart(cart: StoreCartLine[]): Promise<PricedCart
let rows: ItemRow[] = [] let rows: ItemRow[] = []
try { try {
;[rows] = await db(DB.default).query<ItemRow[]>( ;[rows] = await db(DB.default).query<ItemRow[]>(
`SELECT id, item_id, quantity, currency, price FROM home_store_item WHERE id IN (${ids.map(() => '?').join(',')})`, `SELECT id, item_id, name, quantity, currency, price FROM home_store_item WHERE id IN (${ids.map(() => '?').join(',')})`,
ids, ids,
) )
} catch { } catch {
@@ -234,6 +273,7 @@ export async function priceStoreCart(cart: StoreCartLine[]): Promise<PricedCart
const lines = rows.map((r) => ({ const lines = rows.map((r) => ({
id: Number(r.id), id: Number(r.id),
itemId: Number(r.item_id), itemId: Number(r.item_id),
name: String(r.name),
qty: Number(r.quantity), qty: Number(r.quantity),
currency: r.currency, currency: r.currency,
price: Number(r.price), price: Number(r.price),