Biz Telegram Payment Notify — Alerta en tiempo real cuando Sepay recibe el pago
Esta habilidad conecta la notificación del bot de Telegram al webhook de Sepay ya existente en el proyecto Next.js. Cada vez que un cliente realiza una transferencia exitosa a la cuenta bancaria vinculada con Sepay, el skill hace que el bot de Telegram envíe un mensaje en tiempo real al chat del propietario (o al grupo del equipo de ventas) — incluyendo el nombre del cliente, teléfono, monto, producto y hora.
Filosofía: El auto-respondedor por correo electrónico para el cliente (Resend) y la alerta de Telegram para el propietario atienden a dos actores diferentes. El correo es formal para el cliente. Telegram es un "ping" para el propietario — usted está en la calle, abre Telegram y ve el nuevo pedido de inmediato, sin tener que abrir Gmail y buscar entre 100 correos. Latencia de Telegram < 2 segundos vs correo electrónico 30-60 segundos.
¿Por qué Telegram y no Slack/Discord/Zalo?: La configuración del bot de Telegram toma 5 minutos (1 token + 1 chat_id), no necesita workspace/admin de servidor, mensajes ilimitados gratuitos, notificaciones push más potentes que el correo, aplicación nativa iOS/Android/escritorio. Slack necesita workspace + invitación. Discord necesita servidor + configuración de permisos. Zalo OA necesita una cuenta de negocio verificada (3-7 días de aprobación) + tarifa mensual por usuario activo. Telegram es el camino de menor resistencia para las alertas del lado del propietario.
Salida del skill: parche de código + guía de configuración del bot paso a paso + lista de variables de entorno + plan de pruebas de 3 niveles. El skill NO prueba automáticamente el envío de Telegram — requiere que el usuario pegue el token del bot y el chat_id primero, ya que estas credenciales son propiedad del usuario.
Cuándo usar esta habilidad
- El usuario acaba de conectar el webhook de Sepay y quiere recibir notificaciones en tiempo real en Telegram cuando un cliente transfiere dinero.
- El usuario ya tiene
/api/payment-success(o equivalente) manejando el correo electrónico con Resend y quiere agregar la alerta de Telegram en PARALELO — sin crear una nueva ruta, sin duplicar lógica. - El usuario dice "avísame cuando tenga un pedido" y el contexto es una landing page de ventas con integración de Sepay.
NO use esta habilidad cuando:
- El usuario quiere notificaciones de Slack/Discord → caso de uso diferente.
- El usuario quiere un bot de Zalo OA → necesita una cuenta de negocio verificada + estructura de costos diferente.
- El usuario aún no ha configurado el webhook de Sepay (aún no tiene una cuenta bancaria vinculada a Sepay, aún no tiene un endpoint para recibir el webhook) → esta habilidad no configura Sepay desde cero, solo se conecta al webhook existente.
- El usuario solo quiere correo electrónico para el propietario →
biz-email-setupya tiene el Correo B (notificación al propietario).
Flujo de trabajo general (6 fases)
Fase 0: DETECTAR la ruta del webhook de Sepay existente (ruta + framework + forma del manejador)
↓
Fase 0.5: DETECTAR el almacén de leads (lib/leads-kv.ts de /biz-setup-sepay-payment)
→ existe: usar búsqueda KV real
→ no existe: ofrecer 2 opciones (modo raw O ejecutar /biz-setup-sepay-payment primero)
↓
Fase 1: GUIAR al usuario para crear el bot de Telegram (BotFather → token → chat_id)
↓ [BARRERA — esperar a que el usuario pegue token + chat_id]
↓
Fase 2: REDACTAR el formato del mensaje → mostrar vista previa al usuario → confirmar
↓
Fase 3: CONECTAR el código (helper lib/telegram.ts + integrar en el manejador del webhook + variables de entorno)
↓
Fase 4: PLAN DE PRUEBAS (curl al bot directo → payload simulado de Sepay → verificación en producción)
Las fases 1 y 4 tienen una barrera de espera para el usuario porque requieren acciones manuales del usuario (obtener token, verificar en producción). Las fases 0/0.5/2/3 el skill las maneja activamente.
Fase 0 — Detectar el webhook de Sepay existente
Esta habilidad se conecta al webhook existente, NO crea una nueva ruta. Primero, localice el manejador.
Buscar la ruta del webhook — busque con palabras clave comunes:
# En la raíz del proyecto
grep -r "sepay" --include="*.ts" --include="*.js" --include="*.tsx" -l
grep -r "transferAmount\|transferType\|payment-success\|payment-webhook" --include="*.ts" --include="*.js" -l
3 escenarios comunes:
| Escenario | Ruta existente | Acción |
|---|---|---|
El usuario usó el pipeline biz-email-setup | app/api/payment-success/route.ts (o pages/api/payment-success.ts) ya tiene Resend llamando sendCustomerEmail + sendOwnerEmail | Conectar sendTelegramNotification en el mismo Promise.allSettled |
| El usuario tiene una ruta específica de Sepay | app/api/sepay-webhook/route.ts o app/api/webhook/sepay/route.ts | Conectar después de analizar + validar el payload de Sepay (rama transferType === 'in') |
| No se encuentra ninguna ruta | — | DETENER. Informar al usuario: "No encontré una ruta de webhook que reciba Sepay. Configure el webhook de Sepay primero (vincule el banco a Sepay → cree una URL de webhook que apunte a su dominio) y luego vuelva para que conecte Telegram. O, si el webhook está en un endpoint con otro nombre, indíqueme la ruta." |
Leer el manejador actual para entender la forma:
- ¿Autentica la solicitud de Sepay? (Cabecera de Authorization
Apikeyo HMAC) - ¿Deduce duplicados por
idoreferenceCode? (Sepay reintenta si non-200 → necesita deduplicación para evitar notificaciones dobles) - ¿Busca el lead por
content(mensaje de transferencia que contiene el ID del pedido)? - Formato de respuesta a Sepay:
{ success: true }o200 OKsimple?
Si el manejador actual no tiene deduplicación — advierta al usuario pero NO lo bloquee. El skill solo conecta Telegram, la deduplicación es un problema de la lógica de pago. Sugiera al usuario agregar deduplicación por separado si es necesario.
Fase 0.5 — Detectar el almacén de leads (CRÍTICO)
El webhook de Sepay solo envía content (texto que el cliente pegó al transferir) + transferAmount + referenceCode. Sepay NO sabe quién es el cliente — debe buscar en el almacén local según content (que contiene el ID del pedido).
El skill verifica 3 cosas:
# 1. Verificar si existe el helper del almacén de leads
test -f lib/leads-kv.ts || test -f lib/leads-kv.js && echo "lead store: ✓"
# 2. Verificar las variables de entorno de Vercel KV
grep -E "KV_REST_API_URL|KV_REST_API_TOKEN" .env.local && echo "KV env: ✓"
# 3. Verificar si @vercel/kv está instalado
grep '"@vercel/kv"' package.json && echo "KV package: ✓"
3 escenarios:
| Escenario | Acción |
|---|---|
| Las 3 ✓ — almacén de leads listo | Continuar a la Fase 1, usar getLeadByOrderId() de lib/leads-kv.ts en el manejador del webhook |
| Falta 1+ — no configurado | DETENER, preguntar al usuario 2 opciones (ver abajo) |
Si falta, pregunte al usuario:
Detecté que aún no tiene un almacén de leads (Vercel KV). El webhook de Sepay no sabe quién es el cliente — solo tiene el contenido de la transferencia + el monto. ¿Qué prefiere?
Opción 1 — Configurar el almacén de leads completo con
/biz-setup-sepay-paymentprimero (recomendado):
- Ese skill configurará Vercel KV + creará el esqueleto de lib/leads-kv.ts + conectará el envío del formulario para guardar el lead cuando el usuario se registre
- Luego vuelva aquí y conectaré Telegram para buscar el nombre/teléfono/correo del cliente completo
- La alerta de Telegram mostrará: "Nguyễn Văn A — 0901234567 — [email protected] — 499K — Curso de AI Agent"
Opción 2 — Modo raw: enviar a Telegram el payload de Sepay directamente, sin buscar el lead:
- Telegram solo mostrará: "Vietcombank — 499.000đ — content: DH001 0901234567 — 14:32"
- El propietario deberá adivinar el cliente por el contenido y cotejar con el registro de correos de Resend
- Simple, funciona de inmediato, pero con mala experiencia de usuario
¿Elige la opción 1 o la 2?
Si el usuario elige la 1 → DETENER este skill, sugerir /biz-setup-sepay-payment. Después de que el usuario termine la configuración → volver a este skill.
Si el usuario elige la 2 → continuar a la Fase 1, usar la plantilla raw (ver templates/lib-telegram-raw.ts).
Fase 1 — Guiar al usuario para crear el bot de Telegram
Lea references/telegram-bot-setup.md para obtener instrucciones detalladas con capturas de pantalla en texto. Resuma los 3 pasos para el usuario:
Paso 1 — Crear un nuevo bot con @BotFather
1. Abra Telegram (móvil/escritorio)
2. Busque: @BotFather → presione Iniciar
3. Escriba el comando: /newbot
4. BotFather pregunta el nombre del bot — ingrese el nombre para mostrar (ejemplo: "Pedidos Marca X")
5. BotFather pregunta el nombre de usuario — ingrese un nombre de usuario que termine en "_bot" (ejemplo: pedidos_marca_x_bot) — debe ser único globalmente
6. BotFather devuelve un mensaje con la línea:
"Use this token to access the HTTP API: 1234567890:AAxxxxxxxxxxxxxxxxxxxxxx"
7. Copie ese token, péguelo para mí.
NOTA DE SEGURIDAD: Este token equivale al control total del bot. No lo suba a Git, no lo comparta públicamente. Si se filtra → vuelva a @BotFather → /token → /revoke para crear un token nuevo.
Paso 2 — Obtener el chat_id
Chat 1 a 1 (solo el propietario recibe) — recomendado para emprendedores individuales:
1. En Telegram, busque: @userinfobot → presione Iniciar
2. El bot devuelve un mensaje con "Id: 123456789" — copie ese número.
3. Ese es su chat_id.
Chat grupal (el equipo de ventas también recibe) — recomendado para equipos:
1. Cree un nuevo grupo de Telegram (o use uno existente)
2. Presione el nombre del grupo → Agregar miembro → busque el bot que acaba de crear (por el nombre de usuario @pedidos_marca_x_bot) → Agregar
3. En el grupo, escriba: /start@pedidos_marca_x_bot (o cualquier mensaje)
4. Abra el navegador, acceda:
https://api.telegram.org/bot<TOKEN>/getUpdates
(reemplace <TOKEN> por el token del paso 1)
5. En la respuesta JSON, busque "chat":{"id":-1001234567890,...} — copie ese número (recuerde mantener el signo menos si lo tiene)
6. Ese es el chat_id del grupo.
Canal (transmisión unidireccional, sin discusión) — chat_id comienza con -100. La forma de obtenerlo es similar a la del grupo, pero agregue el bot como administrador del canal.
Paso 3 — Pegar para el skill
Espere a que el usuario pegue:
TELEGRAM_BOT_TOKEN=1234567890:AAxxxxxxxxxxxxxxxxxxxxxx
TELEGRAM_CHAT_ID=123456789
BARRERA IMPORTANTE: NO continúe a la Fase 2 hasta que el usuario pegue ambos valores. Si el usuario dice "ya lo tengo, en otro lugar" → aún espere a que lo pegue, porque es necesario inyectarlo en .env.local en la Fase 3.
Fase 2 — Redactar el formato del mensaje
Formato predeterminado en vietnamita, compatible con dispositivos móviles (Telegram renderiza HTML):
🎉 <b>PEDIDO NUEVO</b>
👤 <b>{{name}}</b>
📞 {{phone}}
📧 {{email}}
💰 <b>{{amount}}</b> — {{product_name}}
🕐 {{time_vn}}
🔗 Código de transacción: <code>{{reference_code}}</code>
Después de inyectar las variables:
🎉 PEDIDO NUEVO
👤 Nguyễn Văn A
📞 0901 234 567
📧 [email protected]
💰 499.000đ — Curso de AI Agent
🕐 14:32 14/05/2026
🔗 Código de transacción: FT26134567890
Mostrar vista previa al usuario, preguntar:
- ¿Este formato está bien? ¿Quiere agregar o quitar algún campo? (ejemplo: agregar fuente UTM, número de pedidos del día, enlace de Zalo del cliente)
- ¿Hay demasiados emojis? (Algunos usuarios prefieren texto sin formato)
- ¿Quiere agregar un botón en línea (CTA) con enlace a CRM/Hojas de cálculo? (avanzado — el skill puede agregar un teclado en línea)
Reglas de formato:
- Modo HTML (
parse_mode: "HTML") — Telegram admite<b>,<i>,<u>,<code>,<pre>,<a href>. NO admite<br>— use\n. - Formato de monto VN:
499.000đ(punto como separador de miles, conđal final) O estilo charm499K/1.99Mpara mayor claridad. Lea la configuración devoiceenoffer.jsonsi existe. - Formato de teléfono:
0901 234 567(separación 3-3-3) — más fácil de escanear en móvil. - Hora VN:
HH:mm dd/mm/yyyy— configure la hora de Vietnam (UTC+7), no UTC. - Evite el spam de emojis (>5 emojis/mensaje → reduce la sensación profesional).
Fase 3 — Conectar el código
3 cambios:
3.1 Crear la función auxiliar lib/telegram.ts
Lea templates/lib-telegram.ts (TypeScript) o templates/lib-telegram.js (vanilla JS). Contenido:
// lib/telegram.ts
type TelegramPayload = {
name: string;
phone: string;
email: string;
amount: number; // VND, número sin formato (ej: 499000)
productName: string;
referenceCode?: string;
};
export async function sendTelegramNotification(payload: TelegramPayload): Promise<void> {
const token = process.env.TELEGRAM_BOT_TOKEN;
const chatId = process.env.TELEGRAM_CHAT_ID;
if (!token || !chatId) {
console.error('[telegram] Falta TELEGRAM_BOT_TOKEN o TELEGRAM_CHAT_ID');
return; // No lanzar error — una falla de Telegram no debe bloquear la respuesta de pago
}
const text = formatMessage(payload);
try {
const res = await fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
chat_id: chatId,
text,
parse_mode: 'HTML',
disable_web_page_preview: true,
}),
});
if (!res.ok) {
const errBody = await res.text();
console.error(`[telegram] Error al enviar: ${res.status} ${errBody}`);
}
} catch (err) {
console.error('[telegram] Error de red:', err);
// Ignorar — evitar que una falla de Telegram haga que Sepay reintente
}
}
function formatMessage(p: TelegramPayload): string {
const amountStr = p.amount.toLocaleString('vi-VN') + 'đ';
const phoneStr = p.phone.replace(/(\d{4})(\d{3})(\d{3})/, '$1 $2 $3');
const now = new Date().toLocaleString('vi-VN', {
timeZone: 'Asia/Ho_Chi_Minh',
day: '2-digit', month: '2-digit', year: 'numeric',
hour: '2-digit', minute: '2-digit',
});
return [
'🎉 <b>PEDIDO NUEVO</b>',
'',
`👤 <b>${escapeHtml(p.name)}</b>`,
`📞 ${phoneStr}`,
`📧 ${escapeHtml(p.email)}`,
`💰 <b>${amountStr}</b> — ${escapeHtml(p.productName)}`,
`🕐 ${now}`,
p.referenceCode ? `\n🔗 Código de transacción: <code>${escapeHtml(p.referenceCode)}</code>` : '',
].filter(Boolean).join('\n');
}
function escapeHtml(s: string): string {
return s.replace(/[<>&]/g, c => ({ '<': '<', '>': '>', '&': '&' }[c]!));
}
Por qué ignorar el error: Sepay reintenta el webhook si recibe un non-200. Si Telegram se agota/cae y hace que el webhook se bloquee → Sepay reintenta → cada reintento envía el correo nuevamente + registra el pago de nuevo → el usuario recibe 5 correos electrónicos + 5 alertas de Telegram para un solo pedido. Telegram es un canal secundario de mejor esfuerzo, no una ruta crítica.
3.2 Conectar en el manejador del webhook de Sepay
Lea el manejador actual, localice el lugar donde se procesa el pago exitoso (después de analizar el payload de Sepay + validar transferType === 'in'). Agregue:
// app/api/payment-success/route.ts (o equivalente)
import { sendTelegramNotification } from '@/lib/telegram';
import { sendCustomerEmail, sendOwnerEmail } from '@/lib/resend'; // si ya existe
export async function POST(req: Request) {
const payload = await req.json();
// [Existente] Validar autenticación de Sepay + analizar payload
if (payload.transferType !== 'in') {
return Response.json({ success: true }); // ignorar transferencia saliente
}
// [Existente] Buscar lead según content (que contiene ID del pedido) o referenceCode
const lead = await lookupLeadByTransferContent(payload.content);
// [NUEVO] Distribuir 3 efectos secundarios en paralelo — una falla de Telegram no bloquea el correo
await Promise.allSettled([
sendCustomerEmail(lead),
sendOwnerEmail(lead),
sendTelegramNotification({
name: lead.name,
phone: lead.phone,
email: lead.email,
amount: payload.transferAmount,
productName: lead.productName,
referenceCode: payload.referenceCode,
}),
]);
return Response.json({ success: true });
}
Razón para usar Promise.allSettled en lugar de Promise.all:
Promise.allrechaza si CUALQUIER promesa falla → el manejador lanza un error → Sepay recibe 500 → reintenta → correo duplicado.Promise.allSettledespera a que todas se resuelvan (ya sea resuelta o rechazada), no lanza un error.
...
[truncated for translation]