d-points: reconciliación SumUp (sin webhooks) para asegurar la entrega de PD

SumUp eliminó los webhooks, así que la acreditación no puede depender de que
el navegador vuelva al return_url. Se añade reconciliación consultando el
estado en la API de SumUp de los checkouts pendientes (fulfilled=0):

- reconcileSumUpCheckouts() en lib/sumup.ts: recorre los pendientes recientes
  y acredita los que estén PAID (reclamo atómico idempotente).
- /d-points y /account reconcilian los pendientes de la cuenta al cargar, para
  reflejar el saldo aunque el usuario no pasara por la página de éxito.
- /api/dpoints/reconcile: backstop global protegido con CRON_SECRET, pensado
  para un systemd timer que lo llame periódicamente.

Verificado E2E con tarjeta de test: pago SumUp sin volver a la web -> el
backstop acredita los PD (idempotente). Credenciales y CRON_SECRET viven en
.env.local (fuera de git).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-13 20:22:44 +00:00
parent 6222a46062
commit 42fc15de59
4 changed files with 79 additions and 0 deletions
+5
View File
@@ -2,6 +2,7 @@ import { getTranslations, setRequestLocale } from 'next-intl/server'
import { redirect, Link } from '@/i18n/navigation'
import { getSession } from '@/lib/session'
import { getAccountDashboard, getAccountRank } from '@/lib/account'
import { reconcileSumUpCheckouts } from '@/lib/sumup'
import { getGameAccounts } from '@/lib/auth'
import { isAdmin } from '@/lib/admin'
import { PageShell } from '@/components/PageShell'
@@ -38,6 +39,10 @@ export default async function AccountPage({ params }: { params: Promise<{ locale
if (!session.bnetId) redirect({ href: '/login', locale })
if (!session.username) redirect({ href: '/select-account', locale })
// Acredita cualquier pago SumUp pendiente de esta cuenta (SumUp no tiene
// webhooks) antes de leer el saldo de PD, para que se muestre actualizado.
if (session.accountId) await reconcileSumUpCheckouts({ accountId: session.accountId }).catch(() => {})
const data = await getAccountDashboard(session)
const admin = await isAdmin(session)
const rank = getAccountRank(data.dp)