20ee5a83c8
Renombrar hermandad ya no es solo PD: ahora ofrece el mismo selector de forma de pago que el resto de servicios (saldo PD / tarjeta Stripe / tarjeta SumUp). El coste es equivalente: 1000 PD = 10 €. - lib/guild: se separa la validación (checkGuildRenameEligibility) del renombrado por SOAP (renameGuildBySoap); nuevo GUILD_RENAME_EUR (10 €). - paid-services: entrada 'rename-guild' para que la reconciliación/webhook entregue el servicio tras el pago con tarjeta. - nueva ruta /api/guild/rename/checkout con las 3 pasarelas (valida elegibilidad antes de cobrar); se elimina la antigua /api/guild/rename. - RenameGuildForm usa PaymentMethodSelect y redirige a service-success. - i18n: Paid.rename-guild (title/success) para la página de éxito. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
104 lines
3.8 KiB
TypeScript
104 lines
3.8 KiB
TypeScript
import type { RowDataPacket } from 'mysql2'
|
|
import { db, DB } from './db'
|
|
import { checkSecurityToken } from './security-token'
|
|
import { executeSoapCommand } from './soap'
|
|
|
|
/**
|
|
* Renombrar hermandad: el usuario elige una hermandad de la que sea Maestro
|
|
* (guild_member.rank = 0) y le pone un nombre nuevo. Cuesta 1000 PD y requiere
|
|
* el token de seguridad. El renombrado se aplica por SOAP (.guild rename).
|
|
*/
|
|
|
|
export const GUILD_RENAME_PD = 1000
|
|
/** Precio equivalente en euros (100 PD = 1 €) para pagar con Stripe/SumUp. */
|
|
export const GUILD_RENAME_EUR = GUILD_RENAME_PD / 100
|
|
/** Nombre de hermandad: A-Za-z y espacio, máx. 24 (regla del juego / diseño). */
|
|
const NAME_RE = /^[A-Za-z ]{1,24}$/
|
|
|
|
export interface GmGuild {
|
|
guildid: number
|
|
name: string
|
|
}
|
|
|
|
/** Hermandades de las que la cuenta tiene un personaje Maestro (rank 0). */
|
|
export async function getGuildMasterGuilds(accountId: number): Promise<GmGuild[]> {
|
|
if (!accountId) return []
|
|
try {
|
|
const [rows] = await db(DB.characters).query<RowDataPacket[]>(
|
|
`SELECT g.guildid, g.name
|
|
FROM guild g
|
|
JOIN guild_member gm ON g.guildid = gm.guildid
|
|
JOIN characters c ON c.guid = gm.guid
|
|
WHERE c.account = ? AND gm.rank = 0
|
|
ORDER BY g.name`,
|
|
[accountId],
|
|
)
|
|
return rows.map((r) => ({ guildid: Number(r.guildid), name: r.name }))
|
|
} catch {
|
|
return []
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Comprueba que la cuenta puede renombrar la hermandad (antes de cobrar):
|
|
* nombre válido, token de seguridad correcto, es Maestro de esa hermandad y el
|
|
* nuevo nombre no está en uso. Devuelve el nombre antiguo si todo es correcto.
|
|
*/
|
|
export async function checkGuildRenameEligibility(params: {
|
|
accountId: number
|
|
guildId: number
|
|
newName: string
|
|
token: string
|
|
}): Promise<{ ok: boolean; error?: string; oldName?: string }> {
|
|
const { accountId, guildId, token } = params
|
|
const newName = (params.newName || '').trim()
|
|
|
|
if (!accountId || !guildId || !newName || !token) return { ok: false, error: 'missingFields' }
|
|
if (!NAME_RE.test(newName)) return { ok: false, error: 'invalidName' }
|
|
if (!(await checkSecurityToken(accountId, token))) return { ok: false, error: 'invalidToken' }
|
|
|
|
// La hermandad debe ser una de las que la cuenta dirige (Maestro).
|
|
const guilds = await getGuildMasterGuilds(accountId)
|
|
const guild = guilds.find((g) => g.guildid === guildId)
|
|
if (!guild) return { ok: false, error: 'notGuildMaster' }
|
|
const oldName = guild.name
|
|
if (/["\r\n]/.test(oldName)) return { ok: false, error: 'genericError' } // nombre antiguo no seguro para SOAP
|
|
|
|
// El nuevo nombre no puede coincidir con otra hermandad existente.
|
|
try {
|
|
const [dup] = await db(DB.characters).query<RowDataPacket[]>(
|
|
'SELECT guildid FROM guild WHERE name = ? AND guildid <> ? LIMIT 1',
|
|
[newName, guildId],
|
|
)
|
|
if (dup[0]) return { ok: false, error: 'nameTaken' }
|
|
} catch {
|
|
return { ok: false, error: 'genericError' }
|
|
}
|
|
|
|
return { ok: true, oldName }
|
|
}
|
|
|
|
/**
|
|
* Aplica el renombrado de hermandad por SOAP (post-pago). Busca el nombre
|
|
* antiguo por `guildId` y ejecuta `.guild rename`. Convención del proyecto:
|
|
* respuesta no nula = aplicado (igual que soapFulfill en paid-services).
|
|
*/
|
|
export async function renameGuildBySoap(guildId: number, newNameRaw: string): Promise<boolean> {
|
|
const newName = (newNameRaw || '').trim()
|
|
if (!guildId || !NAME_RE.test(newName)) return false
|
|
let oldName: string
|
|
try {
|
|
const [rows] = await db(DB.characters).query<RowDataPacket[]>(
|
|
'SELECT name FROM guild WHERE guildid = ? LIMIT 1',
|
|
[guildId],
|
|
)
|
|
if (!rows[0]) return false
|
|
oldName = String(rows[0].name)
|
|
} catch {
|
|
return false
|
|
}
|
|
if (/["\r\n]/.test(oldName)) return false
|
|
const soap = await executeSoapCommand(`.guild rename "${oldName}" "${newName}"`)
|
|
return soap !== null
|
|
}
|