Biz Setup Sepay Payment — Infraestructura de pago VietQR integral para Next.js
Esta skill construye toda la infraestructura de pago para una landing page Next.js usando Sepay (la pasarela de pago VietQR de Vietnam). Al finalizar: el cliente llena un formulario → se crea una orden → escanea un código QR → realiza la transferencia → un webhook de Sepay llega → se verifica el pago, se busca el lead y se activa el envío de notificaciones por email/telegram. Todo automatizado, sin necesidad de confirmación manual por parte de un administrador.
Filosofía: Sepay es el camino de menor resistencia para Vietnam — VietQR + API bancaria nativa, sin necesidad de que el cliente se registre, solo escanea el QR con su app bancaria. Comisiones del 1.5-2%, más barato que Stripe (4-5%), liquidación T+1, y un plan gratuito razonable para landing pages con menos de 100 pedidos al mes.
Por qué un almacén de leads: El webhook de Sepay solo envía
content(el texto que el cliente escribe al transferir) + el monto. Sepay NO sabe quién es el cliente. Necesitamos un almacén de leads local para mapear elcontental lead{nombre, teléfono, email, producto, monto}.
2 opciones para el almacén de leads — el usuario elige una en la Fase 0:
- Vercel KV (Upstash Redis) — configuración en 2 minutos, TTL nativo, gratis 30K comandos/mes → < 500 pedidos/mes
- Supabase Free (Postgres) — configuración en 5 minutos, consultas SQL + interfaz Studio, plan gratuito ~50 veces más amplio → < 2000 pedidos/mes. RECOMENDADA.
Las rutas API (/api/checkout, /api/sepay-webhook, /api/admin/leads) son independientes del proveedor a través de lib/leads-store.ts, que es un re-export delgado. Cambiar de proveedor después = modificar una línea.
La skill NO registra automáticamente una cuenta Sepay para el usuario (requiere verificación KYC de negocio + banco). La skill guía al usuario paso a paso y luego enlaza todo el código.
Cuándo usar esta skill
- El usuario está lanzando un producto digital / curso / coaching y necesita recibir pagos en línea.
- El usuario ya tiene una landing page Next.js (a través de
ui-ux-pro-maxo similar) y quiere añadir un flujo de pago. - El usuario está a punto de conectar
biz-email-setupobiz-telegram-payment-notifypero aún no tiene la infraestructura de pago → esta skill es un PRERREQUISITO.
NO usar esta skill cuando:
- El usuario usa Stripe/PayPal (pago con tarjeta internacional) → caso de uso diferente.
- El usuario acepta transferencias bancarias manuales y las verifica él mismo (baja escala, no recomendado).
- El usuario ya tiene infraestructura de pago y solo le falta la notificación → ir directamente a
/biz-email-setupo/biz-telegram-payment-notify.
Flujo de trabajo general (7 fases)
Fase 0: DETECTAR stack Next.js + PREGUNTAR proveedor (KV / Supabase) — BARRERA
↓
Fase 1: GUIAR al usuario para configurar cuenta Sepay — BARRERA manual, esperar al usuario
↓
Fase 2A (si KV): CONFIGURAR Vercel KV (instalar paquete + variables de entorno + namespace KV)
Fase 2B (si Supabase): CONFIGURAR Supabase (crear proyecto + migración SQL + service_role)
↓
Fase 3: CREAR andamio lib/leads-store.ts (envoltorio de re-export) + lib/leads-{kv|supabase}.ts (implementación)
↓
Fase 4: CREAR andamio ruta /api/checkout (formulario → orden → almacenamiento)
↓
Fase 5: CREAR andamio ruta /api/sepay-webhook (verificar + buscar + marcador de efectos secundarios)
↓
Fase 6: INSERTAR VietQR en la landing page (elegir 1 de 3 patrones UX)
↓
Fase 7: Plan de pruebas de 5 niveles (almacén → checkout → webhook → producción → (solo Supabase) ping cron)
Las fases 0 y 1 tienen barrera de espera para el usuario. Las fases 2/3/4/5/6 la skill las realiza activamente.
Fase 0 — Detectar stack Next.js + elegir proveedor
0.1 Detectar stack
test -f next.config.js -o -f next.config.mjs -o -f next.config.ts && echo "Next.js: ✓"
test -d app && echo "Router: App"
test -d pages && echo "Router: Pages"
test -f tsconfig.json && echo "TS: ✓"
test -f pnpm-lock.yaml && echo "pkg: pnpm" || test -f yarn.lock && echo "pkg: yarn" || echo "pkg: npm"
Si no es Next.js: DETENER. Informar: "Esta skill solo soporta Next.js actualmente. Tu stack actual es [X]. ¿Exporto la lógica a Vite/estático con función de Vercel?" — esperar al usuario.
Si existen tanto app/ como pages/: preguntar al usuario cuál usar.
0.2 Verificar infraestructura existente
ls app/api/ pages/api/ lib/ 2>/dev/null
grep -E '"@vercel/kv"|"@supabase/supabase-js"|"@upstash/redis"' package.json
grep -E "SEPAY|KV_REST|SUPABASE_" .env.local 2>/dev/null
0.3 PREGUNTAR al usuario qué proveedor elegir
He detectado que usas Next.js con enrutador [App/Pages] + TypeScript + [pnpm/yarn/npm].
¿Dónde quieres almacenar los leads?
1. Vercel KV (Upstash Redis)
✓ Configuración rápida en 2 minutos (1 clic en el panel de Vercel)
✓ TTL nativo — sin preocupación por pausa de proyecto
✗ Gratis solo 30K comandos/mes → ~500 pedidos/mes
✗ Sin SQL — filtros administrativos complejos y difíciles
2. Supabase Free (Postgres) — RECOMENDADA
✓ Plan gratuito más amplio (500 MB de BD + 5 GB de ancho de banda, consultas ilimitadas)
✓ Postgres = consultas SQL potentes, interfaz Studio de Supabase para ver datos
✓ TypeScript con tipos generados automáticamente
✓ Configuración en 5 minutos (Consola + pegar 2 variables de entorno)
⚠ El proyecto se pausa automáticamente después de 7 días de inactividad → configuro un ping Vercel Cron a `/api/health` cada 6 días para solucionarlo
⚠ TTL manual a través de pg_cron (ya configurado en la migración SQL)
→ Adecuado para < 2000 pedidos/mes, fácil de escalar con funciones administrativas
¿Eliges 1 o 2?
BARRERA: NO pasar a la Fase 1 hasta que el usuario elija. Sugerir Supabase por defecto, pero forzar una elección explícita.
Después de que el usuario elija, resumir el plan:
Has elegido [Vercel KV / Supabase]. Voy a:
- Guiarte en la configuración de una cuenta Sepay (5-10 minutos)
- Configurar el almacén de leads según el proveedor elegido
- Conectar el código: lib/leads-store.ts + /api/checkout + /api/sepay-webhook + insertar VietQR
- Probar
¿Listo para pasar a la Fase 1?
Fase 1 — Guiar al usuario en la configuración de la cuenta Sepay
Leer references/sepay-account-setup.md para la guía completa. Resumir 5 pasos para el usuario:
Paso 1 — Registrarse en my.sepay.vn
- Visitar https://my.sepay.vn → Registrarse
- Ingresar número de teléfono, correo electrónico, contraseña → verificar OTP
- Completar perfil: Nombre completo + DNI/CCCD (personal) o Código de impuestos (empresa)
Paso 2 — Vincular cuenta bancaria
Panel de control → Cuentas bancarias → Agregar cuenta:
- Seleccionar banco (Vietcombank, Techcombank, MB, ACB, VPBank, BIDV, VietinBank, TPBank, ...)
- Ingresar número de cuenta + nombre del titular (debe coincidir con la app bancaria)
- Verificación automática (a través de API bancaria) o manual (transferir 1.000-10.000 VND con el código de verificación que Sepay proporciona)
Paso 3 — Obtener clave API
Panel de control → Configuración → API y Webhook → Crear nueva clave API:
- Nombre:
landing-page-prod - Permisos:
webhook:read - Copiar la clave (solo se muestra una vez)
Paso 4 — Configurar URL del webhook
En la misma página → Webhook → Agregar webhook:
- URL:
https://tudominio.vn/api/sepay-webhook(estará activa después del despliegue) - Autenticación: API Key → pegar la clave recién creada
- Eventos:
transaction:incoming - Guardar
⚠️ Desarrollo local: usar ngrok para exponer
localhost:3000y probar con Sepay.
Paso 5 — Pegar para la skill
SEPAY_WEBHOOK_API_KEY=sk_xxxxxxxxxxxxxxxxxxxx
SEPAY_BANK_ACCOUNT_NUMBER=1023456789
SEPAY_BANK_NAME=Vietcombank
SEPAY_ACCOUNT_NAME=NGUYEN VAN A
BARRERA: NO pasar a la Fase 2 hasta que el usuario pegue todos los datos.
Fase 2A — Configurar Vercel KV (solo si el usuario eligió KV en la Fase 0)
Vercel KV = Upstash Redis gestionado. Gratis 30K comandos/mes.
2A.1 Crear namespace KV
Panel de Vercel → Proyecto → Storage → Create Database → KV (Redis) → nombre sepay-leads → Crear.
Genera automáticamente 4 variables de entorno:
KV_URL=...
KV_REST_API_URL=...
KV_REST_API_TOKEN=...
KV_REST_API_READ_ONLY_TOKEN=...
Hacer clic en Connect to Project → Vercel inyecta las variables de entorno (Production + Preview + Development).
2A.2 Obtener variables de entorno localmente
npm i -g vercel # si aún no está instalado
vercel link
vercel env pull .env.local
2A.3 Instalar paquete
pnpm add @vercel/kv # o yarn / npm install
2A.4 Verificar
node -e "
const { kv } = require('@vercel/kv');
kv.set('test', 'hello').then(() => kv.get('test')).then(v => {
console.log('KV OK:', v);
return kv.del('test');
});
"
Esperar KV OK: hello. Ir a Fase 3.
Fase 2B — Configurar Supabase Free (solo si el usuario eligió Supabase en la Fase 0)
Leer references/supabase-setup.md para la guía completa de 6 pasos. Resumen:
2B.1 Crear cuenta y proyecto en Supabase
- https://supabase.com → Iniciar proyecto → Iniciar sesión con GitHub
- New project:
- Nombre:
sepay-leads-<marca> - Contraseña de la base de datos: hacer clic en Generate → guardar
- Región: ⚠️ Southeast Asia (Singapore) (por defecto us-east tiene latencia de 250ms+)
- Precio: Free
- Nombre:
- Esperar ~2 minutos para el aprovisionamiento
2B.2 Ejecutar migración SQL
Barra lateral → SQL Editor → New query → pegar el contenido de templates/supabase-migration.sql → Run.
Crea 4 tablas + índices + función next_order_id() + habilita RLS + trabajo diario de limpieza pg_cron.
Verificar la salida: 4 tablas (leads/phone_index/order_counter/webhook_dedup) + 1 trabajo pg_cron + next_order_id() devuelve "DH000001".
Restablecer el contador (porque la verificación ya lo incrementó):
UPDATE order_counter SET current_value = 0 WHERE id = 1;
DELETE FROM leads WHERE order_id = 'DH000001';
2B.3 Obtener credenciales API
Project Settings → API:
- Copiar Project URL (
https://xxxxx.supabase.co) - Copiar el secreto
service_role(NO la claveanon— service_role omite RLS, se usa para el servidor)
2B.4 Pegar variables de entorno
SUPABASE_URL=https://xxxxx.supabase.co
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJI...
La skill verifica automáticamente:
- La URL coincide con
^https://[a-z0-9]+\.supabase\.co$ - La clave decodifica JWT, verifica
role: "service_role"(NOanon)
Si es incorrecto → mostrar error, pedir que lo pegue de nuevo.
2B.5 Instalar paquete
pnpm add @supabase/supabase-js
2B.6 Configurar Vercel Cron ping (para evitar pausa)
Supabase Free se pausa automáticamente después de 7 días de inactividad. Solución: hacer ping a /api/health cada 6 días.
La skill crea app/api/health/route.ts (a partir de templates/api-health-route.ts) y agrega vercel.json con:
{
"crons": [
{ "path": "/api/health", "schedule": "0 3 */6 * *" }
]
}
⚠️ El plan Vercel Cron Hobby tiene un límite de 2 crons/mes. El patrón */6 days = ~5 veces/mes → supera la cuota. Opciones:
- Vercel Pro ($20/mes) — crons ilimitados
- GitHub Actions flujo de trabajo
.github/workflows/keep-alive.yml(GRATIS) — la skill lo crea si detecta un repositorio de GitHub - cron-job.org (GRATIS) — configuración manual, pegar URL
/api/health
La skill elige por defecto la opción 2 (GitHub Actions) si el repositorio tiene .git/config con un remoto de GitHub. De lo contrario, muestra las instrucciones de la opción 3.
2B.7 Verificar
node -e "
const { createClient } = require('@supabase/supabase-js');
const sb = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_SERVICE_ROLE_KEY);
sb.from('order_counter').select('current_value').eq('id', 1).single().then(r => {
if (r.error) { console.error('FAIL:', r.error.message); process.exit(1); }
console.log('Supabase OK. current_value =', r.data.current_value);
});
"
Esperar Supabase OK. current_value = 0. Ir a Fase 3.
Fase 3 — Crear andamio lib/leads-store.ts + implementación
Abstracción del proveedor: las rutas API importan desde lib/leads-store.ts, sin saber si el almacenamiento subyacente es KV o Postgres.
Si eligió KV (Fase 0)
Crear 2 archivos:
lib/leads-store.ts← copiartemplates/lib-leads-store-kv.ts(1 líneaexport * from './leads-kv')lib/leads-kv.ts← copiartemplates/lib-leads-kv.ts(implementación KV, ~160 líneas)
Si eligió Supabase
Crear 3 archivos:
lib/leads-store.ts← copiartemplates/lib-leads-store-supabase.ts(re-exportar Supabase)lib/leads-supabase.ts← copiartemplates/lib-leads-supabase.ts(implementación Postgres + listLeads para administradores)lib/supabase-admin.ts← copiartemplates/lib-supabase-admin.ts(cliente administrador en caché singleton con service_role)
SUPERFICIE de API COMÚN (ambas implementaciones):
createLead(input) → { orderId, lead }— ID atómico + inserción + índice secundariogetLeadByOrderId(orderId) → Lead | nullgetLeadByPhone(phone) → Lead | null— búsqueda de respaldofindPendingLeadByAmountAndTime(amount, start, end) → Lead[]— Estrategia 3 de coincidencia de webhookmarkLeadPaid(orderId, payment) → Lead | nullisTransactionProcessed(sepayId) → boolean— deduplicaciónmarkTransactionProcessed(sepayId) → void
TTL: 7 días pendiente / 90 días pagado. KV a través del parámetro ex:. Supabase a través del campo expire_at + trabajo diario de eliminación pg_cron.
Cambiar de proveedor después = modificar 1 línea de leads-store.ts (NO tocar el código de la API).
Leer references/leads-kv-schema.md (KV) o references/supabase-leads-schema.md (Supabase) para conocer la justificación detallada.
Fase 4 — Crear andamio ruta /api/checkout
Leer templates/api-checkout-app-router.ts (enrutador App) o templates/api-checkout-pages-router.ts (enrutador Pages). Copiar en la ruta correspondiente.
Ambas plantillas import { createLead } from '@/lib/leads-store' — independientes del proveedor.
Flujo:
Cliente POST /api/checkout { name, phone, email, productName, amount }
↓
Servidor:
1. Validar (formato de teléfono VN, regex de email, amount > 0)
2. createLead() → { orderId: "DH000123" }
3. Generar URL de QR de Sepay: https://qr.sepay.vn/img?acc=X&bank=Y&amount=Z&des=DH000123&template=compact
4. Devolver { orderId, amount, bankInfo, content, qrUrl }
↓
Cliente: mostrar modal con QR O redirigir a /checkout/DH000123
Fase 5 — Crear andamio ruta /api/sepay-webhook
Leer templates/api-sepay-webhook-app-router.ts. Copiar en app/api/sepay-webhook/route.ts.
La plantilla import ... from '@/lib/leads-store' — independiente del proveedor.
Flujo:
Sepay POST /api/sepay-webhook
Authorization: Apikey {SEPAY_WEBHOOK_API_KEY}
Cuerpo: SepayWebhookPayload
↓
Servidor:
1. Verificar autenticación con timingSafeEqual — proteger contra ataques de temporización
2. Deduplicación temprana: isTransactionProcessed(payload.id) → devolver 200 pronto
3. Registrar deduplicación PRIMERO — idempotente incluso si falla el procesamiento
4. Filtrar transferType === 'in' (ignorar transferencias salientes)
5. Coincidencia multi-estrategia:
a. content → order_id (regex /DH\d+/)
b. content → teléfono (regex /0\d{9}/) → getLeadByPhone
c. amount + timestamp ±30min → findPendingLeadByAmountAndTime (solo aceptar si length === 1)
6. Rechazar pagos insuficientes, ACEPTAR pagos excesivos
7. markLeadPaid(orderId, payment) → actualizar almacén
8. Propagar efectos secundarios a través de un bucle try/catch (NO Promise.all)
9. SIEMPRE devolver 200 (incluso en caso de error interno) — Sepay reintenta con Fibonacci 7 veces si no es 200
Marcadores de efectos secundarios para que biz-email-setup + biz-telegram-payment-notify se conecten después.
Leer references/sepay-webhook-flow.md para conocer la justificación de la deduplicación y el comportamiento de reintentos.
Fase 6 — Insertar VietQR en la landing page
3 patrones de UX, preguntar al usuario que elija 1:
| Patrón | Cuándo | UX |
|---|---|---|
| A. Ventana emergente modal | Producto único / compra impulsiva | Enviar formulario → ventana emergente modal con QR en la misma página |
| B. Página /checkout/[orderId] | Múltiples productos / necesidad de compartir URL |
...
[truncado para la traducción]