web-next: /transfer-character (transferir personaje por SumUp con todas las condiciones)

- 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>
This commit is contained in:
2026-07-14 12:52:25 +00:00
parent eaae16e33a
commit d95fa932f9
7 changed files with 229 additions and 51 deletions
+13 -4
View File
@@ -1,6 +1,7 @@
import { executeSoapCommand } from './soap'
import { creditDPoints } from './dpoints'
import { checkFactionChangeEligibility } from './change-faction'
import { checkTransferEligibility } from './transfer-character'
import {
getRenamePrice,
getCustomizePrice,
@@ -18,9 +19,16 @@ export interface PaidServiceConfig {
productName: (character: string, meta: Meta) => string
fulfill: (character: string, meta: Meta) => Promise<boolean>
extraFields: string[] // campos (además de character) que el form envía y se guardan en metadata
// Comprobación previa al pago (p.ej. condiciones de cambio de facción). Si `ok`
// es false, se bloquea el checkout y se muestra `message`.
precheck?: (accountId: number, character: string) => Promise<{ ok: boolean; message?: string }>
// Comprobación previa al pago (p.ej. condiciones de cambio de facción / transferencia).
// Si `ok` es false, se bloquea el checkout y se muestra `message`.
precheck?: (ctx: PrecheckCtx) => Promise<{ ok: boolean; message?: string }>
}
export interface PrecheckCtx {
accountId: number
email: string
character: string
body: Record<string, unknown>
}
async function soapFulfill(command: string): Promise<boolean> {
@@ -60,7 +68,7 @@ export const PAID_SERVICES: Record<string, PaidServiceConfig> = {
price: () => getChangeFactionPrice(),
productName: (c) => `Cambiar facción: ${c}`,
fulfill: (c) => soapFulfill(`.char changef ${c}`),
precheck: async (accountId, character) => {
precheck: async ({ accountId, character }) => {
const r = await checkFactionChangeEligibility(accountId, character)
return { ok: r.ok, message: r.ok ? undefined : `No se puede cambiar la facción: el personaje ${r.reasons.join('; ')}.` }
},
@@ -83,6 +91,7 @@ export const PAID_SERVICES: Record<string, PaidServiceConfig> = {
productName: (c, m) => `Transferir ${c} a la cuenta ${m.destination_account}`,
fulfill: (c, m) => soapFulfill(`.char changeaccount ${m.destination_account} ${c}`),
extraFields: ['destination_account'],
precheck: ({ accountId, email, character, body }) => checkTransferEligibility(accountId, email, character, body),
},
// Compra de PD (Adquirir PD): acredita puntos a la cuenta que hizo el pago.
// El importe lo elige el usuario, por eso `price` lee metadata.amount.