Faltaba la otra mitad de 0be3dc5: con saldo ya se reembolsaba lo que no llegaba
a enviarse, pero con tarjeta el dinero lo tiene la pasarela y el cliente se
quedaba pagando de más. Ambas tienen API de reembolso parcial, así que ahora
`fulfill` puede pedir que se devuelva un importe y lo hace quien conoce la
pasarela (lib/fulfill para Stripe, lib/sumup para SumUp).
Para saber CUÁNTO devolver hay que saber qué costó cada entrada, así que el
pedido pasa a guardarse como `itemId:qty:precio:moneda`. Releer el precio del
catálogo al entregar no vale por dos razones: 114 item_id son ambiguos (el mismo
objeto se vende a 50 PV y a 100 PD), y hay que devolver lo que se COBRÓ, no lo
que valga el ítem el día de la entrega. Los pedidos con el formato viejo se
entregan igual, pero no se puede calcular su reembolso: se registra para hacerlo
a mano.
`fulfill` devuelve ahora `boolean | {ok, refundEur}`; los servicios que no
entregan a medias siguen devolviendo boolean y no se tocan.
Sobre SumUp, todo comprobado contra su API en sandbox y nada de esto está en
sitios obvios:
- El reembolso NO va por referencia de checkout sino por transacción.
- Hay que usar el endpoint de v1.0; el viejo `/v0.1/me/refund/{txn}` responde
409 a CUALQUIER reembolso parcial (el total sí funciona).
- v1.0 quiere el importe en CÉNTIMOS y ENTERO, al revés que el resto de la API
v0.1, que usa euros. Y mandar 0.10 en vez de 10 no da error: se trunca a 0,
SumUp lo lee como "sin importe" y DEVUELVE EL PAGO ENTERO. Casi me lo comí.
- Hay un mínimo por reembolso (20 cts en esta cuenta, la API lo dice en
`min_refundable_amount`): por debajo responde 400 y se registra para hacerlo
a mano.
El host sale de SUMUP_API_BASE (por defecto api.sumup.com) en vez de ir a fuego.
Verificado de punta a punta con pagos reales de prueba, parcheando el SOAP para
que solo saliera el primer correo:
- Stripe (sk_test): pagados 1,30 €, entregadas 12/13 -> reembolso de 0,10 €,
status succeeded en la API de Stripe.
- SumUp (sandbox): pagados 1,50 €, entregadas 12/15 -> evento REFUND 0.3
REFUNDED en la API de SumUp.
De paso: el concepto del cobro decía "1 objeto" al comprar 15 copias (contaba
líneas, no copias).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Los 9 servicios con precio en euros (rename, customize, change-race,
change-faction, level-up, gold, transfer, restore-item, send-gift) ahora
ofrecen un selector de forma de pago con las 3 opciones: saldo PD, tarjeta
(Stripe) o tarjeta (SumUp).
- lib/dpoints: spendDPoints() descuenta PD de forma atómica (FOR UPDATE).
- lib/pay-with-dpoints: paga con saldo PD, ejecuta la acción al momento y
reembolsa si la ejecución falla; 100 PD = 1 €.
- rutas checkout (character/[service] y gift): rama provider='pd' que valida
saldo, descuenta, ejecuta fulfill y devuelve la URL de éxito (sin pasarela).
- service-success: rama provider='pd' (la entrega ya se hizo en el checkout).
- PaymentMethodSelect: componente compartido con coste por método y saldo;
desactiva PD si no hay saldo suficiente.
- Formularios (PaidServiceForm, Gold, Transfer, RestoreItems, SendGift) y sus
páginas pasan el saldo PD y envían el método elegido + locale.
- i18n: namespace Pay (es/en); añadidas claves Paid.restore-item que faltaban.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Nueva ruta /transfer-character (10€ SumUp); borra /transfer (Stripe) y actualiza
el link del panel.
- lib/transfer-character.ts: precheck con todas las condiciones — personaje offline,
contraseña, token de seguridad, 2FA activo, cuenta destino válida (≠ propia) y,
para Caballeros de la Muerte, la cuenta destino debe tener un personaje nivel >=55
y no tener ya un DK; + bloqueo de 5s tras transferencia reciente.
- El hook precheck de PaidServiceConfig ahora recibe {accountId,email,character,body}.
- TransferForm: contraseña + token (con ojos), provider sumup, confirm.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- /change-race-character (3€), /change-faction-character (5€, valida condiciones:
sin hermandad/correo/subastas/capitán-arena y oro <= cap por nivel),
/level-up-character (10€), /gold-character (oro por correo SOAP). Cada uno borra
su ruta antigua y actualiza el link del panel.
- SumUp: hook precheck en PaidServiceConfig (condiciones cambio de facción);
columna home_stripelog.metadata para campos extra (gold_amount).
- /quest-character: rastreador de estado de misiones/cadenas (clase, Hijos de
Hodir, misión por ID) en personaje offline; cooldowns por categoría, Turnstile,
IDs de misión verificados en wotlkdb 3.3.5a. Tabla home_questsearchhistory.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- /rename-character y /customize-character: renombrar/personalizar pagados por SumUp
(getRenamePrice/getCustomizePrice). SumUp ahora es service-aware: columna
home_stripelog.service; createSumUpCheckout guarda service+character;
fulfillSumUpCheckout despacha a la acción del servicio (.char rename/customi) o,
sin service, acredita PD. [service]/checkout acepta provider:'sumup';
PaidServiceForm acepta provider+confirmText; service-success maneja el return SumUp.
Se eliminan las antiguas /rename y /customize (Stripe).
- /revive-character (comparte ReviveServiceContent); se elimina /revive.
- Renombres de ruta + todos sus enlaces:
/account -> /my-account, /recruit -> /recruit-a-friend, /unstuck -> /unstuck-character.
(/select-account y /api/account/* intactos.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- app/api/stripe/webhook: verifica firma (constructEventAsync) y en
checkout.session.completed registra la IP de Stripe (como Django) y ENTREGA
el servicio. Idempotente frente a la página de éxito.
- lib/stripe.ts: claimPaidCheckout ahora es ATÓMICO (UPDATE ... WHERE fulfilled=0
+ affectedRows) para evitar doble entrega entre webhook y página de éxito;
nuevo recordStripeIp.
- lib/fulfill.ts: fulfillCheckoutSession compartido (reclama + resuelve servicio
desde metadata.service + ejecuta). checkout guarda `service` en metadata.
- service-success: usa la entrega compartida y distingue entregado/ya-procesado/error.
- i18n: Paid.alreadyProcessed.
Requiere configurar STRIPE_WEBHOOK_SECRET en .env.local (endpoint apuntando a
/api/stripe/webhook, evento checkout.session.completed).
Verificado: build OK, webhook 400 sin firma / firma inválida.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- lib/stripe.ts: createCheckoutSession acepta metadata; claimPaidCheckout la
recupera de la sesión Stripe y la devuelve.
- lib/paid-services.ts: config unificado con fulfill(character, meta) + extraFields.
gold -> UPDATE characters money (BD directa, no SOAP), transfer -> SOAP
.char changeaccount. Los 5 simples usan soapFulfill.
- checkout [service]: lee extraFields -> metadata, precio depende de metadata (gold),
valida precio>0. service-success llama cfg.fulfill(name, metadata).
- prices.ts: getTransferPrice, getGoldOptions/goldPriceFor (home_goldprice).
- GoldForm (personaje + cantidad) y TransferForm (personaje + destino); páginas
/gold y /transfer. Catálogos Paid.gold/transfer.
Verificado: /gold /transfer redirigen a login, checkout 401 sin sesión, home OK.
NOTA: transfer no valida aún el token de seguridad ni reglas AC (nivel>=55, DK).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- lib/paid-services.ts: config PAID_SERVICES (rename/customize/change-race/
change-faction/level-up) con comando SOAP, precio y productName por servicio.
- app/api/character/[service]/checkout: ruta de checkout GENÉRICA (valida servicio +
personaje, crea la sesión Stripe con successUrl /service-success?service=...).
- app/[locale]/service-success: página de éxito ÚNICA que verifica el pago
(claimPaidCheckout) y ejecuta el comando SOAP del servicio.
- components/ServicePageContent: contenido común (guardas + PaidServiceForm).
- 5 páginas mínimas /rename /customize /change-race /change-faction /level-up.
- Catálogo Paid (por servicio: title/pay/success) sustituye a Rename.
Verificado: 5 páginas redirigen a login, checkout 401 sin sesión, servicio inválido
404, service-success con pago inválido no entrega (muestra error). Pendiente: gold
(cantidad) y transfer (destino+token), variantes del patrón.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>