web-next: change-race/faction/level-up/gold por SumUp + rastreador de misiones
- /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>
This commit is contained in:
@@ -27,6 +27,12 @@ export async function POST(request: Request, { params }: { params: Promise<{ ser
|
||||
return Response.json({ success: false, error: 'invalidCharacter' })
|
||||
}
|
||||
|
||||
// Comprobación previa al pago (condiciones del servicio, p.ej. cambio de facción).
|
||||
if (cfg.precheck) {
|
||||
const pc = await cfg.precheck(session.accountId, character)
|
||||
if (!pc.ok) return Response.json({ success: false, error: 'ineligible', message: pc.message })
|
||||
}
|
||||
|
||||
// Campos extra del servicio (gold_amount, destination_account...) -> metadata.
|
||||
// `service` va en metadata para que el webhook sepa qué entregar sin la URL.
|
||||
const metadata: Record<string, string> = { service }
|
||||
@@ -60,6 +66,7 @@ export async function POST(request: Request, { params }: { params: Promise<{ ser
|
||||
returnUrl: `${site}/service-success?service=${service}&provider=sumup&ref=${reference}`,
|
||||
service,
|
||||
characterName: character,
|
||||
metadata,
|
||||
})
|
||||
if (!r.success || !r.url) return Response.json({ success: false, error: r.error || 'sumupError' })
|
||||
return Response.json({ success: true, url: r.url })
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import type { RowDataPacket } from 'mysql2'
|
||||
import { getSession } from '@/lib/session'
|
||||
import { db, DB } from '@/lib/db'
|
||||
import { getClassCss } from '@/lib/character-info'
|
||||
import { verifyTurnstile } from '@/lib/turnstile'
|
||||
import { QUEST_OPTIONS, questStatuses, cooldownRemainingMs, recordSearch, type QuestCategory } from '@/lib/quest-tracker'
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const session = await getSession()
|
||||
if (!session.accountId || !session.username) {
|
||||
return Response.json({ success: false, error: 'notAuthenticated' }, { status: 401 })
|
||||
}
|
||||
|
||||
let body: Record<string, unknown> = {}
|
||||
try {
|
||||
body = await request.json()
|
||||
} catch {
|
||||
return Response.json({ success: false, error: 'invalidRequest' }, { status: 400 })
|
||||
}
|
||||
|
||||
const character = String(body.character ?? '').trim()
|
||||
const optionKey = String(body.option ?? '').trim()
|
||||
if (!character || !optionKey) return Response.json({ success: false, error: 'missingFields' })
|
||||
|
||||
const ip = (request.headers.get('x-forwarded-for') || '').split(',')[0].trim() || undefined
|
||||
if (!(await verifyTurnstile(String(body.turnstile ?? ''), ip))) {
|
||||
return Response.json({ success: false, error: 'captcha' })
|
||||
}
|
||||
|
||||
// Personaje: debe ser de la cuenta y estar desconectado.
|
||||
let guid = 0
|
||||
let level = 0
|
||||
let classCss = ''
|
||||
try {
|
||||
const [rows] = await db(DB.characters).query<RowDataPacket[]>(
|
||||
'SELECT guid, level, class, online FROM characters WHERE name = ? AND account = ? LIMIT 1',
|
||||
[character, session.accountId],
|
||||
)
|
||||
const c = rows[0]
|
||||
if (!c) return Response.json({ success: false, error: 'characterNotOwned' })
|
||||
if (c.online === 1) return Response.json({ success: false, error: 'characterOnline' })
|
||||
guid = Number(c.guid)
|
||||
level = Number(c.level)
|
||||
classCss = getClassCss(Number(c.class))
|
||||
} catch {
|
||||
return Response.json({ success: false, error: 'genericError' })
|
||||
}
|
||||
|
||||
// Resuelve categoría + misiones a comprobar.
|
||||
let category: QuestCategory
|
||||
let quests: number[]
|
||||
let label: string
|
||||
if (optionKey === 'specific') {
|
||||
const qid = Number(body.questId)
|
||||
if (!Number.isInteger(qid) || qid <= 0) return Response.json({ success: false, error: 'invalidQuestId' })
|
||||
if (level < 10) return Response.json({ success: false, error: 'levelTooLow' })
|
||||
category = 'specific'
|
||||
quests = [qid]
|
||||
label = `Misión #${qid}`
|
||||
} else {
|
||||
const opt = QUEST_OPTIONS.find((o) => o.key === optionKey)
|
||||
if (!opt) return Response.json({ success: false, error: 'invalidOption' })
|
||||
if (opt.classCss && opt.classCss !== classCss) return Response.json({ success: false, error: 'wrongClass' })
|
||||
if (level < opt.minLevel) return Response.json({ success: false, error: 'levelTooLow' })
|
||||
if (opt.quests.length === 0) return Response.json({ success: false, error: 'noData' })
|
||||
category = opt.category
|
||||
quests = opt.quests
|
||||
label = opt.label
|
||||
}
|
||||
|
||||
const remaining = await cooldownRemainingMs(character, category)
|
||||
if (remaining > 0) {
|
||||
return Response.json({ success: false, error: 'cooldown', minutes: Math.ceil(remaining / 60000) })
|
||||
}
|
||||
|
||||
const statuses = await questStatuses(guid, quests)
|
||||
await recordSearch(character, category)
|
||||
return Response.json({ success: true, label, statuses })
|
||||
}
|
||||
Reference in New Issue
Block a user