store: pagar también con Stripe y SumUp (tarjeta), además del saldo PD/PV
Selector de forma de pago en el carrito (Saldo PD/PV · Stripe · SumUp), como el resto de servicios. Con tarjeta, el carrito se convierte a euros (100 PD = 1 €, 200 PV = 1 €), se guarda como pedido y se entrega tras confirmarse el pago. - BD: tabla home_store_order (ref, cuenta, personaje, items, importe) para no depender del límite de metadata de la pasarela (sql/home_store_order.sql). - lib/store: storeEuroTotal, createStoreOrder, fulfillStoreOrder. - paid-services: servicio 'store' (fulfill = enviar el pedido por order_ref); la entrega la dispara service-success/reconciliación/webhook como los demás. - /api/store/send: rama provider stripe/sumup (crea pedido + checkout, mínimo 1 €/0,50 €) además del pago con saldo. - StoreBrowser: selector Saldo/Stripe/SumUp; con tarjeta redirige a la pasarela. - i18n Store (payMethod/payBalance/payStripe/paySumUp/confirmCard/amountTooLow) y Paid.store (title/success). Verificado: euros 200PD+28PV=2,14 €, pedido y fulfill por order_ref (SOAP real pendiente, worldserver caído). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { useTranslations, useLocale } from 'next-intl'
|
||||
import { CharacterSelect, type CharOption } from '@/components/CharacterSelect'
|
||||
import { WowheadLink } from '@/components/WowheadLink'
|
||||
import { wowheadIcon } from '@/lib/wowhead'
|
||||
@@ -78,12 +78,16 @@ function CategoryNode({
|
||||
)
|
||||
}
|
||||
|
||||
type PayMethod = 'balance' | 'stripe' | 'sumup'
|
||||
|
||||
export function StoreBrowser({ characters, dp, vp }: { characters: CharOption[]; dp: number; vp: number }) {
|
||||
const t = useTranslations('Store')
|
||||
const locale = useLocale()
|
||||
const [character, setCharacter] = useState('')
|
||||
const [catalog, setCatalog] = useState<StoreCategory[] | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [cart, setCart] = useState<Map<number, StoreItem>>(new Map())
|
||||
const [method, setMethod] = useState<PayMethod>('balance')
|
||||
const [sending, setSending] = useState(false)
|
||||
const [modal, setModal] = useState<{ ok: boolean; text: string } | null>(null)
|
||||
|
||||
@@ -121,22 +125,34 @@ export function StoreBrowser({ characters, dp, vp }: { characters: CharOption[];
|
||||
const lines = [...cart.values()]
|
||||
const pdTotal = lines.filter((l) => l.currency === 'pd').reduce((s, l) => s + l.price, 0)
|
||||
const vpTotal = lines.filter((l) => l.currency === 'pv').reduce((s, l) => s + l.price, 0)
|
||||
// 100 PD = 1 €, 200 PV = 1 € (igual que en lib/store storeEuroTotal).
|
||||
const eurTotal = Math.round((pdTotal / 100 + vpTotal / 200) * 100) / 100
|
||||
|
||||
async function send() {
|
||||
if (sending || cart.size === 0) return
|
||||
if (!window.confirm(t('confirm', { character, pd: pdTotal, vp: vpTotal }))) return
|
||||
const card = method === 'stripe' || method === 'sumup'
|
||||
const ok = card
|
||||
? window.confirm(t('confirmCard', { eur: eurTotal, character }))
|
||||
: window.confirm(t('confirm', { character, pd: pdTotal, vp: vpTotal }))
|
||||
if (!ok) return
|
||||
setSending(true)
|
||||
try {
|
||||
const res = await fetch('/api/store/send', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({ character, items: [...cart.keys()] }),
|
||||
body: JSON.stringify({ character, items: [...cart.keys()], provider: method, locale }),
|
||||
})
|
||||
const data: { success?: boolean; error?: string } = await res.json()
|
||||
const data: { success?: boolean; error?: string; url?: string; min?: number } = await res.json()
|
||||
if (data.success && data.url) {
|
||||
window.location.href = data.url // pasarela (Stripe/SumUp)
|
||||
return
|
||||
}
|
||||
if (data.success) {
|
||||
setModal({ ok: true, text: t('successMsg', { character }) })
|
||||
setCart(new Map())
|
||||
} else if (data.error === 'amountTooLow') {
|
||||
setModal({ ok: false, text: t('errors.amountTooLow', { min: data.min ?? 1 }) })
|
||||
} else {
|
||||
setModal({ ok: false, text: t.has(`errors.${data.error}`) ? t(`errors.${data.error}`) : t('errors.generic') })
|
||||
}
|
||||
@@ -171,6 +187,24 @@ export function StoreBrowser({ characters, dp, vp }: { characters: CharOption[];
|
||||
|
||||
{/* Carrito */}
|
||||
<div id="cart-list">
|
||||
{lines.length > 0 && (
|
||||
<div className="pay-method-select">
|
||||
<p className="second-brown">{t('payMethod')}</p>
|
||||
<div className="pay-method-options">
|
||||
{([
|
||||
{ id: 'balance' as PayMethod, label: t('payBalance'), sub: `${pdTotal} PD · ${vpTotal} PV` },
|
||||
{ id: 'stripe' as PayMethod, label: t('payStripe'), sub: `${eurTotal} €` },
|
||||
{ id: 'sumup' as PayMethod, label: t('paySumUp'), sub: `${eurTotal} €` },
|
||||
]).map((o) => (
|
||||
<label key={o.id} className={`pay-method-option${method === o.id ? ' selected' : ''}`}>
|
||||
<input type="radio" name="store-pay" value={o.id} checked={method === o.id} onChange={() => setMethod(o.id)} />
|
||||
<span className="pay-method-label">{o.label}</span>
|
||||
<span className="pay-method-sub yellow-info">{o.sub}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<table className="max-left-table2">
|
||||
<tbody>
|
||||
<tr>
|
||||
|
||||
Reference in New Issue
Block a user