diff --git a/web-next/app/[locale]/send-gift/page.tsx b/web-next/app/[locale]/send-gift/page.tsx index 9478c67..d2e4b47 100644 --- a/web-next/app/[locale]/send-gift/page.tsx +++ b/web-next/app/[locale]/send-gift/page.tsx @@ -4,7 +4,7 @@ import { getTranslations, setRequestLocale } from 'next-intl/server' import { redirect, Link } from '@/i18n/navigation' import { getSession } from '@/lib/session' import { getGameCharacters } from '@/lib/characters' -import { getDPointsBalance } from '@/lib/dpoints' +import { getDPointsBalance, getVPointsBalance } from '@/lib/dpoints' import { getGiftCatalog } from '@/lib/gift' import { PageShell } from '@/components/PageShell' import { ServiceBox } from '@/components/ServiceBox' @@ -27,10 +27,11 @@ export default async function SendGiftPage({ params }: { params: Promise<{ local if (!session.bnetId) redirect({ href: '/log-in', locale }) if (!session.username) redirect({ href: '/select-account', locale }) - const [chars, catalog, pdBalance] = await Promise.all([ + const [chars, catalog, pdBalance, vpBalance] = await Promise.all([ getGameCharacters(session.accountId!), getGiftCatalog(), getDPointsBalance(session.accountId!), + getVPointsBalance(session.accountId!), ]) const currency = process.env.SUMUP_CURRENCY === 'EUR' ? '€' : process.env.SUMUP_CURRENCY || '€' @@ -70,6 +71,7 @@ export default async function SendGiftPage({ params }: { params: Promise<{ local catalog={catalog} currency={currency} pdBalance={pdBalance} + vpBalance={vpBalance} /> diff --git a/web-next/app/[locale]/service-success/page.tsx b/web-next/app/[locale]/service-success/page.tsx index a25a33d..5110290 100644 --- a/web-next/app/[locale]/service-success/page.tsx +++ b/web-next/app/[locale]/service-success/page.tsx @@ -22,8 +22,8 @@ export default async function ServiceSuccessPage({ let status: 'delivered' | 'already' | 'error' = 'error' let okName: string | null = null let service = urlService ?? null - if (provider === 'pd') { - // Pago con saldo PD: la entrega ya se ejecutó en el checkout; aquí solo se muestra. + if (provider === 'pd' || provider === 'vp') { + // Pago con saldo PD/PV: la entrega ya se ejecutó en el checkout; aquí solo se muestra. status = 'delivered' okName = name ?? null } else if (provider === 'sumup' && ref) { diff --git a/web-next/app/api/gift/checkout/route.ts b/web-next/app/api/gift/checkout/route.ts index 7b004ae..70f8568 100644 --- a/web-next/app/api/gift/checkout/route.ts +++ b/web-next/app/api/gift/checkout/route.ts @@ -4,7 +4,7 @@ import { getGameCharacters, getAccountIdByCharacterName } from '@/lib/characters import { checkSecurityToken } from '@/lib/security-token' import { createSumUpCheckout, sumupConfigured } from '@/lib/sumup' import { priceCart, isValidCharName, sendGiftByMail, type CartLine } from '@/lib/gift' -import { payServiceWithDPoints } from '@/lib/pay-with-dpoints' +import { payServiceWithDPoints, payServiceWithVPoints } from '@/lib/pay-with-dpoints' /** * Inicia el pago (SumUp) de un regalo. Condiciones: @@ -62,16 +62,18 @@ export async function POST(request: Request) { const site = process.env.SITE_URL || '' const items = priced.lines.map((l) => ({ i: l.itemId, q: l.qty })) - // Pago con saldo PD: se descuenta el saldo y se envía el regalo al momento. - if (String(body.provider) === 'pd') { - const r = await payServiceWithDPoints(session.accountId, priced.total, () => - sendGiftByMail(destination, source, items), - ) - if (!r.success) return Response.json({ success: false, error: r.error, message: 'No se pudo completar el pago con PD.' }) + // Pago con saldo PD o PV: se descuenta el saldo y se envía el regalo al momento. + // PV es más caro que PD (son puntos gratis por votar). + if (String(body.provider) === 'pd' || String(body.provider) === 'vp') { + const useVp = String(body.provider) === 'vp' + const r = useVp + ? await payServiceWithVPoints(session.accountId, priced.total, () => sendGiftByMail(destination, source, items)) + : await payServiceWithDPoints(session.accountId, priced.total, () => sendGiftByMail(destination, source, items)) + if (!r.success) return Response.json({ success: false, error: r.error, message: 'No se pudo completar el pago.' }) const locale = String(body.locale) === 'en' ? 'en' : 'es' return Response.json({ success: true, - url: `${site}/${locale}/service-success?service=send-gift&provider=pd&name=${encodeURIComponent(destination)}`, + url: `${site}/${locale}/service-success?service=send-gift&provider=${useVp ? 'vp' : 'pd'}&name=${encodeURIComponent(destination)}`, }) } diff --git a/web-next/components/PaymentMethodSelect.tsx b/web-next/components/PaymentMethodSelect.tsx index 0cd88f8..9c4d399 100644 --- a/web-next/components/PaymentMethodSelect.tsx +++ b/web-next/components/PaymentMethodSelect.tsx @@ -2,17 +2,25 @@ import { useTranslations } from 'next-intl' -export type PayMethod = 'pd' | 'stripe' | 'sumup' +export type PayMethod = 'pd' | 'stripe' | 'sumup' | 'vp' // 100 PD = 1 € (igual que PD_PER_UNIT en lib/dpoints, que no se importa aquí // para no arrastrar el acceso a BD al bundle del cliente). const PD_PER_UNIT = 100 +// Los PV cuestan este factor por el coste en PD (igual que VP_PRICE_FACTOR en +// lib/pay-with-dpoints). Mantener sincronizados. +const VP_PRICE_FACTOR = 2 /** Coste en PD (redondeado) de un precio en euros. */ export function pdCostOf(priceEur: number): number { return Math.round(priceEur * PD_PER_UNIT) } +/** Coste en PV de un precio en euros (más caro que en PD). */ +export function vpCostOf(priceEur: number): number { + return pdCostOf(priceEur) * VP_PRICE_FACTOR +} + /** * Selector de forma de pago: saldo PD, tarjeta (Stripe) o tarjeta (SumUp). * Muestra el coste de cada método y desactiva PD si no hay saldo suficiente. @@ -22,11 +30,13 @@ export function PaymentMethodSelect({ onChange, priceEur, pdBalance, + vpBalance, }: { value: PayMethod onChange: (m: PayMethod) => void priceEur: number pdBalance: number + vpBalance?: number // si se pasa, muestra la opción de pagar con PV (solo /send-gift) }) { const t = useTranslations('Pay') const cost = pdCostOf(priceEur) @@ -39,9 +49,24 @@ export function PaymentMethodSelect({ sub: canPd ? t('pdCost', { cost }) : t('insufficient', { cost }), disabled: !canPd, }, + ] + + // Opción PV: solo cuando el formulario la habilita pasando el saldo VP. + if (vpBalance !== undefined) { + const vCost = vpCostOf(priceEur) + const canVp = vpBalance >= vCost && vCost > 0 + options.push({ + id: 'vp', + label: t('vp'), + sub: canVp ? t('vpCost', { cost: vCost }) : t('insufficientVp', { cost: vCost }), + disabled: !canVp, + }) + } + + options.push( { id: 'stripe', label: t('stripe'), sub: t('eur', { price: priceEur }) }, { id: 'sumup', label: t('sumup'), sub: t('eur', { price: priceEur }) }, - ] + ) return (
@@ -65,7 +90,10 @@ export function PaymentMethodSelect({ ))}
-

{t('balance', { balance: pdBalance })}

+

+ {t('balance', { balance: pdBalance })} + {vpBalance !== undefined ? ` · ${t('balanceVp', { balance: vpBalance })}` : ''} +

) } diff --git a/web-next/components/SendGiftForm.tsx b/web-next/components/SendGiftForm.tsx index 25bea02..cf66954 100644 --- a/web-next/components/SendGiftForm.tsx +++ b/web-next/components/SendGiftForm.tsx @@ -51,11 +51,13 @@ export function SendGiftForm({ catalog, currency = '€', pdBalance = 0, + vpBalance = 0, }: { characters: CharOption[] catalog: GiftCategory[] currency?: string pdBalance?: number + vpBalance?: number }) { const t = useTranslations('CharService') const tpay = useTranslations('Pay') @@ -260,7 +262,7 @@ export function SendGiftForm({

{t.rich('sendGift.total', { total, currency, s: (c) => {c} })}

- {total > 0 && } + {total > 0 && }