Biz Telegram Payment Notify — Alerte en temps réel quand Sepay reçoit un paiement
Ce skill intègre la notification bot Telegram dans le webhook Sepay déjà existant de votre projet Next.js. À chaque fois qu'un client effectue un virement réussi sur un compte bancaire lié à Sepay, le bot Telegram envoie un message en temps réel dans le chat du propriétaire (ou du groupe de vente) — avec le nom du client, son téléphone, le montant, le produit et l'heure.
Philosophie : L'auto-répondeur email pour le client (Resend) et l'alerte Telegram pour le propriétaire servent deux acteurs différents. L'email est formel pour le client. Telegram est un "ping" pour le propriétaire — vous êtes en déplacement, vous ouvrez Telegram et vous voyez immédiatement la nouvelle commande, sans avoir à ouvrir Gmail et chercher parmi 100 autres emails. Latence Telegram < 2 secondes contre 30-60 secondes pour l'email.
Pourquoi Telegram et pas Slack/Discord/Zalo : La configuration d'un bot Telegram prend 5 minutes (1 token + 1 chat_id), nécessite ni espace de travail ni administrateur serveur, est gratuite avec messages illimités, a des notifications push plus puissantes que l'email, et dispose d'applications natives iOS/Android/desktop. Slack nécessite un espace de travail + une invitation. Discord nécessite un serveur + une configuration de permissions. Zalo OA nécessite un compte professionnel vérifié (approbation de 3 à 7 jours) + des frais mensuels pour utilisateur actif. Telegram est le chemin de moindre résistance pour les alertes côté propriétaire.
Sortie : correctif de code + guide de configuration du bot étape par étape + liste des variables d'environnement + plan de test en 3 niveaux. Le skill NE teste PAS l'envoi Telegram lui-même — il demande à l'utilisateur de coller le token et le chat_id du bot au préalable, car ces identifiants sont la propriété de l'utilisateur.
Quand utiliser ce skill
- L'utilisateur vient de configurer le webhook Sepay et souhaite être notifié en temps réel sur Telegram lorsqu'un client transfère de l'argent.
- L'utilisateur a déjà un
/api/payment-success(ou équivalent) qui gère l'email Resend et souhaite ajouter une alerte Telegram en PARALLÈLE — sans créer une nouvelle route, sans dupliquer la logique. - L'utilisateur dit "prévenez-moi quand il y a une commande" et le contexte est une page de vente avec intégration Sepay.
N'utilisez PAS ce skill quand :
- L'utilisateur veut une notification Slack/Discord → cas d'utilisation différent.
- L'utilisateur veut un bot Zalo OA → nécessite un compte professionnel vérifié + une structure de coûts différente.
- L'utilisateur n'a pas encore configuré le webhook Sepay (pas de compte bancaire lié à Sepay, pas de point de terminaison pour recevoir le webhook) → ce skill ne configure pas Sepay à partir de zéro, il ne fait que s'intégrer dans un webhook existant.
- L'utilisateur veut un email uniquement pour le propriétaire →
biz-email-setupa déjà un Email B (notification propriétaire).
Workflow général (6 phases)
Phase 0: DÉTECTER la route du webhook Sepay existant (chemin + framework + forme du gestionnaire)
↓
Phase 0.5: DÉTECTER le magasin de leads (lib/leads-kv.ts depuis /biz-setup-sepay-payment)
→ présent : utiliser la recherche KV réelle
→ absent : proposer 2 options (mode brut OU exécuter /biz-setup-sepay-payment d'abord)
↓
Phase 1: GUIDER l'utilisateur pour créer le bot Telegram (BotFather → token → chat_id)
↓ [BARRIÈRE — attendre que l'utilisateur colle le token + chat_id]
↓
Phase 2: PROPOSER le format du message → montrer un aperçu à l'utilisateur → confirmer
↓
Phase 3: INTÉGRER le code (assistant lib/telegram.ts + intégration dans le gestionnaire webhook + variables d'environnement)
↓
Phase 4: PLAN DE TEST (curl bot direct → payload Sepay simulé → vérification en production)
Les phases 1 et 4 ont une barrière d'attente car l'utilisateur doit effectuer des opérations manuelles (récupérer le token, vérifier en production). Les phases 0/0.5/2/3 sont prises en charge par le skill.
Phase 0 — Détecter le webhook Sepay existant
Ce skill s'intègre dans un webhook existant, il NE crée PAS de nouvelle route. Commencez par localiser le gestionnaire.
Trouver la route du webhook — rechercher avec les mots-clés courants :
# Dans la racine du projet
grep -r "sepay" --include="*.ts" --include="*.js" --include="*.tsx" -l
grep -r "transferAmount\|transferType\|payment-success\|payment-webhook" --include="*.ts" --include="*.js" -l
3 scénarios courants :
| Scénario | Chemin existant | Action |
|---|---|---|
L'utilisateur utilise le pipeline biz-email-setup | app/api/payment-success/route.ts (ou pages/api/payment-success.ts) contient déjà Resend appelant sendCustomerEmail + sendOwnerEmail | Intégrer sendTelegramNotification dans le même Promise.allSettled |
| L'utilisateur a une route spécifique à Sepay | app/api/sepay-webhook/route.ts ou app/api/webhook/sepay/route.ts | Intégrer après l'analyse et la validation du payload Sepay (branche transferType === 'in') |
| Aucune route trouvée | — | ARRÊTER. Informer l'utilisateur : "Je n'ai pas trouvé de route webhook recevant Sepay. Veuillez d'abord configurer le webhook Sepay (lier un compte bancaire à Sepay → créer une URL de webhook pointant vers votre domaine), puis revenez ici pour que j'intègre Telegram. Ou si le webhook se trouve à un point de terminaison avec un nom différent, veuillez m'indiquer le chemin." |
Lire le gestionnaire actuel pour comprendre sa forme :
- Y a-t-il une authentification de la requête Sepay ? (en-tête Authorization
Apikeyou HMAC) - Y a-t-il une déduplication basée sur
idoureferenceCode? (Sepay relance si non-200 → nécessite une déduplication pour éviter les notifications en double) - Y a-t-il une recherche de lead basée sur
content(le message de virement contenant l'ID de commande) ? - Format de la réponse à Sepay :
{ success: true }ou200 OKsimple ?
Si le gestionnaire actuel n'a pas de déduplication — avertir l'utilisateur mais NE PAS bloquer. Le skill ne fait qu'intégrer Telegram, la déduplication est une préoccupation de la logique de paiement. Suggérer à l'utilisateur d'ajouter une déduplication séparée si nécessaire.
Phase 0.5 — Détecter le magasin de leads (CRITIQUE)
Le webhook Sepay n'envoie que content (le texte que le client a collé lors du virement) + transferAmount + referenceCode. Sepay ne sait pas qui est le client — il faut rechercher dans un stockage local en fonction du content (qui contient l'ID de commande).
Le skill vérifie 3 choses :
# 1. Vérifier si l'assistant du magasin de leads existe
test -f lib/leads-kv.ts || test -f lib/leads-kv.js && echo "magasin de leads : ✓"
# 2. Vérifier les variables d'environnement Vercel KV
grep -E "KV_REST_API_URL|KV_REST_API_TOKEN" .env.local && echo "variables d'environnement KV : ✓"
# 3. Vérifier si @vercel/kv est installé
grep '"@vercel/kv"' package.json && echo "paquet KV : ✓"
3 scénarios :
| Scénario | Action |
|---|---|
| Les 3 ✓ — magasin de leads prêt | Passer à la Phase 1, utiliser getLeadByOrderId() depuis lib/leads-kv.ts dans le gestionnaire webhook |
| 1+ manquant — pas encore configuré | ARRÊTER, demander à l'utilisateur 2 options (voir ci-dessous) |
Si manquant, demander à l'utilisateur :
J'ai détecté que vous n'avez pas encore de magasin de leads (Vercel KV). Le webhook Sepay ne connaît pas l'identité du client — il n'a que le contenu du virement + le montant. Que souhaitez-vous faire ?
Option 1 — Configurer un magasin de leads complet via
/biz-setup-sepay-paymentd'abord (recommandée) :
- Ce skill configurera Vercel KV, créera le squelette de lib/leads-kv.ts et intégrera la soumission de formulaire pour sauvegarder le lead lors de l'inscription de l'utilisateur
- Revenez ensuite ici pour que j'intègre Telegram avec une recherche complète du nom/téléphone/email du client
- L'alerte Telegram affichera : "Nguyễn Văn A — 0901234567 — [email protected] — 499K — Cours AI Agent"
Option 2 — Mode brut : envoyer le payload Sepay directement sur Telegram, sans recherche de lead :
- Telegram affichera seulement : "Vietcombank — 499.000đ — contenu : DH001 0901234567 — 14:32"
- Le propriétaire devra deviner le client à partir du contenu et recouper avec les logs email Resend
- Simple, fonctionne immédiatement, mais mauvaise expérience utilisateur
Choisissez l'option 1 ou 2 ?
Si l'utilisateur choisit 1 → ARRÊTER ce skill, suggérer /biz-setup-sepay-payment. Une fois que l'utilisateur a terminé la configuration → revenir à ce skill.
Si l'utilisateur choisit 2 → passer à la Phase 1, utiliser le modèle brut (voir templates/lib-telegram-raw.ts).
Phase 1 — Guider l'utilisateur pour créer le bot Telegram
Lire references/telegram-bot-setup.md pour des instructions détaillées avec des captures d'écran textuelles. Résumé en 3 étapes pour l'utilisateur :
Étape 1 — Créer un nouveau bot avec @BotFather
1. Ouvrir Telegram (mobile/desktop)
2. Rechercher : @BotFather → cliquer sur Démarrer
3. Taper la commande : /newbot
4. BotFather demande le nom du bot — saisir un nom d'affichage (ex : "Commandes Marque X")
5. BotFather demande un nom d'utilisateur — saisir un nom d'utilisateur se terminant par "_bot" (ex : marquex_commandes_bot) — doit être unique globalement
6. BotFather renvoie un message contenant la ligne :
"Use this token to access the HTTP API: 1234567890:AAxxxxxxxxxxxxxxxxxxxxxx"
7. Copier ce token, le coller ici.
ATTENTION SÉCURITÉ : Ce token équivaut à un droit de contrôle du bot. Ne le committez pas sur Git, ne le partagez pas publiquement. En cas de fuite → retourner chez @BotFather → /token → /revoke pour créer un nouveau token.
Étape 2 — Récupérer le chat_id
Chat 1-1 (seul le propriétaire reçoit) — recommandé pour un solopreneur :
1. Dans Telegram, rechercher : @userinfobot → cliquer sur Démarrer
2. Le bot renvoie un message contenant "Id : 123456789" — copier ce numéro.
3. C'est le chat_id.
Chat de groupe (l'équipe de vente reçoit ensemble) — recommandé pour une équipe :
1. Créer un nouveau groupe Telegram (ou utiliser un groupe existant)
2. Cliquer sur le nom du groupe → Ajouter un membre → rechercher le bot que vous venez de créer (par son nom d'utilisateur @marquex_commandes_bot) → Ajouter
3. Dans le groupe, taper : /start@marquex_commandes_bot (ou n'importe quel message)
4. Ouvrir un navigateur, accéder à :
https://api.telegram.org/bot<TOKEN>/getUpdates
(remplacer <TOKEN> par le token de l'étape 1)
5. Dans la réponse JSON, chercher "chat":{"id":-1001234567890,...} — copier ce numéro (ne pas oublier le signe moins s'il est présent)
6. C'est le chat_id du groupe.
Channel (diffusion à sens unique, pas de discussion) — le chat_id commence par -100. La méthode pour le récupérer est la même que pour un groupe, mais il faut ajouter le bot en tant qu'administrateur du channel.
Étape 3 — Coller les informations pour le skill
Attendre que l'utilisateur colle :
TELEGRAM_BOT_TOKEN=1234567890:AAxxxxxxxxxxxxxxxxxxxxxx
TELEGRAM_CHAT_ID=123456789
BARRIÈRE IMPORTANTE : NE PAS passer à la Phase 2 tant que l'utilisateur n'a pas collé les deux valeurs. Si l'utilisateur dit "je les ai déjà, ailleurs" → attendre quand même le collage, car elles doivent être injectées dans .env.local à la Phase 3.
Phase 2 — Proposer le format du message
Format par défaut en français, adapté aux mobiles (Telegram rend le HTML) :
🎉 <b>NOUVELLE COMMANDE</b>
👤 <b>{{nom}}</b>
📞 {{téléphone}}
📧 {{email}}
💰 <b>{{montant}}</b> — {{nom_produit}}
🕐 {{heure_vn}}
🔗 Code transaction : <code>{{code_référence}}</code>
Après injection des variables :
🎉 NOUVELLE COMMANDE
👤 Jean Dupont
📞 06 12 34 56 78
📧 [email protected]
💰 499.000đ — Cours AI Agent
🕐 14:32 14/05/2026
🔗 Code transaction : FT26134567890
Montrer un aperçu à l'utilisateur, demander :
- Ce format vous convient-il ? Souhaitez-vous ajouter/supprimer des champs ? (ex : ajouter la source UTM, le nombre de commandes du jour, le lien Zalo du client)
- Y a-t-il trop d'emojis ? (certains utilisateurs préfèrent le texte brut)
- Souhaitez-vous ajouter un bouton en ligne (CTA) menant vers un CRM ou Google Sheets ? (avancé — le skill peut ajouter un clavier en ligne)
Règles de format :
- Mode HTML (
parse_mode: "HTML") — Telegram supporte<b>,<i>,<u>,<code>,<pre>,<a href>. NE supporte PAS<br>— utiliser\n. - Format du montant VN :
499.000đ(point comme séparateur de milliers, avecđà la fin) OU version simplifiée499K/1.99Mpour plus d'élégance. Lire depuis le paramètrevoicedansoffer.jsonsi présent. - Format du téléphone :
06 12 34 56 78(par paires) — plus facile à scanner sur mobile. - Heure VN :
HH:mm dd/mm/yyyy— régler sur l'heure du Vietnam (UTC+7), pas UTC. - Éviter les spams d'emojis (>5 emojis/message → diminue le sentiment professionnel).
Phase 3 — Intégrer le code
3 modifications :
3.1 Créer la fonction d'assistance lib/telegram.ts
Lire templates/lib-telegram.ts (TypeScript) ou templates/lib-telegram.js (JavaScript vanille). Contenu :
// lib/telegram.ts
type TelegramPayload = {
name: string;
phone: string;
email: string;
amount: number; // VND, nombre brut (ex : 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] TELEGRAM_BOT_TOKEN ou TELEGRAM_CHAT_ID manquant');
return; // Ne pas lever d'exception — un échec Telegram ne doit pas bloquer la réponse de paiement
}
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] Échec de l'envoi : ${res.status} ${errBody}`);
}
} catch (err) {
console.error('[telegram] Erreur réseau :', err);
// Ignorer — ne pas laisser un échec Telegram provoquer une relance de Sepay
}
}
function formatMessage(p: TelegramPayload): string {
const amountStr = p.amount.toLocaleString('vi-VN') + 'đ';
const phoneStr = p.phone.replace(/(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/, '$1 $2 $3 $4 $5');
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>NOUVELLE COMMANDE</b>',
'',
`👤 <b>${escapeHtml(p.name)}</b>`,
`📞 ${phoneStr}`,
`📧 ${escapeHtml(p.email)}`,
`💰 <b>${amountStr}</b> — ${escapeHtml(p.productName)}`,
`🕐 ${now}`,
p.referenceCode ? `\n🔗 Code transaction : <code>${escapeHtml(p.referenceCode)}</code>` : '',
].filter(Boolean).join('\n');
}
function escapeHtml(s: string): string {
return s.replace(/[<>&]/g, c => ({ '<': '<', '>': '>', '&': '&' }[c]!));
}
Pourquoi ignorer l'erreur : Sepay relance le webhook s'il reçoit un code non-200. Si un timeout/panne de Telegram fait planter le webhook → Sepay relance → chaque relance renvoie l'email et enregistre à nouveau le paiement → l'utilisateur reçoit 5 emails + 5 alertes Telegram pour une seule commande. Telegram est un canal secondaire best-effort, pas un chemin critique.
3.2 Intégrer dans le gestionnaire webhook Sepay
Lire le gestionnaire actuel, localiser l'endroit où le paiement réussi est traité (après l'analyse du payload Sepay + validation de transferType === 'in'). Ajouter :
// app/api/payment-success/route.ts (ou équivalent)
import { sendTelegramNotification } from '@/lib/telegram';
import { sendCustomerEmail, sendOwnerEmail } from '@/lib/resend'; // si existant
export async function POST(req: Request) {
const payload = await req.json();
// [Existant] Valider l'authentification Sepay + analyser le payload
if (payload.transferType !== 'in') {
return Response.json({ success: true }); // ignorer les transferts sortants
}
// [Existant] Rechercher le lead via le contenu (contenant l'ID de commande) ou le code de référence
const lead = await lookupLeadByTransferContent(payload.content);
// [NOUVEAU] Lancer 3 effets secondaires en parallèle — un échec Telegram ne bloque pas l'email
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 });
}
Raison d'utiliser Promise.allSettled au lieu de Promise.all :
Promise.allse rejette si N'IMPORTE QUELLE promesse échoue → le gestionnaire lève une exception → Sepay reçoit un 500 → relance → email en double.Promise.allSettledattend que toutes les promesses soient tenues (résolues ou rejetées), sans lancer d'exception.