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:
@@ -1,7 +1,7 @@
|
||||
import { randomUUID } from 'crypto'
|
||||
import { getSession } from '@/lib/session'
|
||||
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 { createCheckoutSession } from '@/lib/stripe'
|
||||
import { createSumUpCheckout, sumupConfigured } from '@/lib/sumup'
|
||||
@@ -56,10 +56,13 @@ export async function POST(request: Request) {
|
||||
|
||||
const site = process.env.SITE_URL || ''
|
||||
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
|
||||
// mismo ítem son "15 objetos", no "1 objeto".
|
||||
const units = cart.lines.reduce((s, l) => s + l.copies, 0)
|
||||
const productName = `Compra en la tienda (${units} objeto${units === 1 ? '' : 's'})`
|
||||
// Concepto con quién compra y qué: es lo único que verá la pasarela en su
|
||||
// panel para identificar el pago (ver `storeConcept`).
|
||||
const productName = storeConcept(cart, {
|
||||
bnetEmail: session.bnetEmail,
|
||||
account: session.username,
|
||||
character,
|
||||
})
|
||||
const metadata = { service: 'store', order_ref: ref }
|
||||
|
||||
if (provider === 'sumup') {
|
||||
|
||||
+42
-2
@@ -197,11 +197,50 @@ export interface StoreCartLine {
|
||||
export interface PricedCart {
|
||||
// `qty` = lote que entrega CADA copia (home_store_item.quantity);
|
||||
// `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
|
||||
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
|
||||
* 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[] = []
|
||||
try {
|
||||
;[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,
|
||||
)
|
||||
} catch {
|
||||
@@ -234,6 +273,7 @@ export async function priceStoreCart(cart: StoreCartLine[]): Promise<PricedCart
|
||||
const lines = rows.map((r) => ({
|
||||
id: Number(r.id),
|
||||
itemId: Number(r.item_id),
|
||||
name: String(r.name),
|
||||
qty: Number(r.quantity),
|
||||
currency: r.currency,
|
||||
price: Number(r.price),
|
||||
|
||||
Reference in New Issue
Block a user