Biz Telegram Payment Notify — Sepay 入金時にリアルタイムアラート
このスキルは、Next.js プロジェクトの既存の Sepay Webhook に Telegram ボット通知 を組み込みます。顧客が Sepay にリンクされた銀行口座に振り込みに成功するたびに、このスキルは Telegram ボットを使用して、オーナー(または営業チームグループ)のチャットにリアルタイムでメッセージをプッシュします。メッセージには、顧客名、電話番号、金額、商品名、時刻が含まれます。
理念: 顧客向けのメール自動応答(Resend)とオーナー向けの Telegram アラートは、異なる2つのアクターにサービスを提供します。メールは顧客向けのフォーマルな手段です。Telegram はオーナー向けの「ping」です。外出先でも Telegram を開けばすぐに新しい注文を確認でき、100通のメールの中から探すために Gmail を開く必要はありません。Telegram のレイテンシは 2 秒未満ですが、メールは 30~60 秒です。
なぜ Slack/Discord/Zalo ではなく Telegram なのか: Telegram ボットは 5 分でセットアップでき(トークン1つ + chat_id 1つ)、ワークスペースやサーバー管理者は不要、無制限の無料メッセージ、メールより強力なプッシュ通知、iOS/Android/デスクトップのネイティブアプリを備えています。Slack はワークスペースと招待が必要です。Discord はサーバーと権限設定が必要です。Zalo OA は確認済みビジネスアカウント(承認に3〜7日)と月間アクティブユーザー料金が必要です。Telegram はオーナー側のアラートにとって最も抵抗の少ない経路です。
出力: コードパッチ + ステップバイステップのボットセットアップガイド + 環境変数リスト + 3段階のテスト計画。このスキルは Telegram 送信テストを自動では行いません — ユーザーにあらかじめボットトークンと chat_id を貼り付けてもらう必要があります。これらの認証情報はユーザーの資産だからです。
このスキルを使用するタイミング
- ユーザーが Sepay Webhook を設定したばかりで、顧客からの振込があったときに Telegram でリアルタイムに通知を受け取りたい場合。
- ユーザーが
/api/payment-success(または同等のエンドポイント)で Resend メールを既に処理しており、並列で Telegram アラートを追加したい場合 — 新しいルートを作成したり、ロジックを複製したりしません。 - ユーザーが「注文があったら教えて」と言っていて、コンテキストが Sepay 統合のあるランディングページ販売の場合。
以下の場合はこのスキルを使用しないでください:
- ユーザーが Slack/Discord 通知を希望する場合 → 別のユースケース。
- ユーザーが Zalo OA ボットを希望する場合 → OA 確認済みビジネスアカウントと異なるコスト構造が必要。
- ユーザーがまだ Sepay Webhook をセットアップしていない場合(銀行口座が Sepay にリンクされておらず、Webhook を受信するエンドポイントがない)→ このスキルは Sepay をゼロからセットアップするのではなく、既存の Webhook に配線するだけです。
- ユーザーがオーナー向けにメールのみを希望する場合 →
biz-email-setupに既に Email B(オーナー通知)があります。
ワークフロー概要(6フェーズ)
フェーズ 0: 既存の Sepay Webhook ルートを検出(パス + フレームワーク + ハンドラーの形状)
↓
フェーズ 0.5: リードストアを検出(lib/leads-kv.ts(/biz-setup-sepay-payment から))
→ あり: 実際の KV ルックアップを使用
→ なし: 2つのオプションを提示(raw モード または /biz-setup-sepay-payment を先に実行)
↓
フェーズ 1: ユーザーに Telegram ボットの作成をガイド(BotFather → トークン → chat_id)
↓ [ゲート — ユーザーがトークンと chat_id を貼り付けるのを待つ]
↓
フェーズ 2: メッセージフォーマットをドラフト → ユーザーにプレビュー表示 → 確認
↓
フェーズ 3: コードを配線(lib/telegram.ts ヘルパー + Webhook ハンドラーへの統合 + 環境変数)
↓
フェーズ 4: テスト計画(curl ボット直接 → シミュレートされた Sepay ペイロード → 本番確認)
フェーズ 1 とフェーズ 4 には ユーザーを待つゲート があります。ユーザーによる手動操作(トークン取得、本番確認)が必要だからです。フェーズ 0/0.5/2/3 はスキルが能動的に行います。
フェーズ 0 — 既存の Sepay Webhook を検出
このスキルは既存の Webhook に配線し、新しいルートは作成しません。最初にハンドラーを見つけます。
Webhook ルートを検索 — 一般的なキーワードで grep します:
# プロジェクトルートで
grep -r "sepay" --include="*.ts" --include="*.js" --include="*.tsx" -l
grep -r "transferAmount\|transferType\|payment-success\|payment-webhook" --include="*.ts" --include="*.js" -l
3つの一般的なシナリオ:
| シナリオ | 既存のパス | アクション |
|---|---|---|
ユーザーがパイプライン biz-email-setup を使用 | app/api/payment-success/route.ts(または pages/api/payment-success.ts)に sendCustomerEmail + sendOwnerEmail を呼び出す Resend が既にある | sendTelegramNotification を同じ Promise.allSettled に配線 |
| ユーザーが Sepay 固有のルートを持っている | app/api/sepay-webhook/route.ts または app/api/webhook/sepay/route.ts | Sepay ペイロードを解析して検証した後(transferType === 'in' ブランチ)に配線 |
| どのルートも見つからない | — | 停止。ユーザーに通知: "Sepay を受信する Webhook ルートが見つかりませんでした。先に Sepay Webhook をセットアップして(銀行口座を Sepay にリンク → ドメインを指す Webhook URL を作成)、その後戻ってきて Telegram を配線してください。または、Webhook が別の名前のエンドポイントにある場合は、パスを教えてください。" |
現在のハンドラーを読み取り、形状を理解します:
- Sepay リクエストを認証しているか?(Authorization ヘッダー
Apikeyまたは HMAC) idまたはreferenceCodeに基づいて重複排除しているか?(Sepay は non-200 の場合にリトライするため、重複通知を避けるには重複排除が必要)content(注文 ID を含む振込メッセージ)に基づいてリードをルックアップしているか?- Sepay に返すレスポンス形式:
{ success: true }またはプレーンな200 OK?
現在のハンドラーに 重複排除がない 場合 — ユーザーに警告しますが、ブロックはしません。このスキルは Telegram を配線するだけで、重複排除は支払いロジックの関心事です。必要に応じて、ユーザーに別途重複排除を追加することを提案します。
フェーズ 0.5 — リードストアを検出(重要)
Sepay Webhook は content(顧客が振込時に貼り付けたテキスト)+ transferAmount + referenceCode のみを送信します。Sepay は顧客が誰かを知りません — content(注文 ID を含む)に基づいてローカルストアをルックアップする必要があります。
スキルは3つの項目をチェックします:
# 1. リードストアヘルパーが存在するか確認
test -f lib/leads-kv.ts || test -f lib/leads-kv.js && echo "lead store: ✓"
# 2. Vercel KV 環境変数を確認
grep -E "KV_REST_API_URL|KV_REST_API_TOKEN" .env.local && echo "KV env: ✓"
# 3. @vercel/kv がインストールされているか確認
grep '"@vercel/kv"' package.json && echo "KV package: ✓"
3つのシナリオ:
| シナリオ | アクション |
|---|---|
| 3つすべて ✓ — リードストア準備完了 | フェーズ 1 に進み、Webhook ハンドラーで lib/leads-kv.ts の getLeadByOrderId() を使用 |
| 1つ以上不足 — セットアップ未完了 | 停止、ユーザーに2つのオプションを尋ねる(下記参照) |
不足している場合、ユーザーに尋ねます:
リードストア(Vercel KV)が見つかりませんでした。Sepay Webhook は顧客が誰かを知りません — 振込内容と金額しかありません。以下のオプションがあります:
オプション 1 — 先に
/biz-setup-sepay-paymentでリードストアを完全にセットアップ(推奨):
- そのスキルは Vercel KV のセットアップ、lib/leads-kv.ts のスキャフォールディング、ユーザー登録時にリードを保存するフォーム送信の配線を行います
- その後、こちらに戻って Telegram を配線し、顧客の名前/電話番号/メールアドレスを完全にルックアップします
- Telegram アラートには "Nguyễn Văn A — 0901234567 — [email protected] — 499K — Khoá AI Agent" と表示されます
オプション 2 — Raw モード: リードをルックアップせずに、Sepay ペイロードをそのまま Telegram に送信します:
- Telegram には "Vietcombank — 499.000đ — content: DH001 0901234567 — 14:32" のみ表示されます
- オーナーは content から顧客を推測し、Resend メールログと照合します
- シンプルで今すぐ実行できますが、UX は劣ります
1 か 2 を選んでください。
ユーザーが 1 を選んだ場合 → このスキルを停止し、/biz-setup-sepay-payment を提案します。ユーザーがセットアップを完了したら、このスキルに戻ってきます。
ユーザーが 2 を選んだ場合 → フェーズ 1 に進み、raw テンプレートを使用します(templates/lib-telegram-raw.ts を参照)。
フェーズ 1 — ユーザーに Telegram ボットの作成をガイド
詳細な手順とテキストスクリーンショットについては references/telegram-bot-setup.md を参照してください。ユーザー向けの3つの手順を要約します:
ステップ 1 — @BotFather で新しいボットを作成
1. Telegram を開く(モバイル/デスクトップ)
2. 検索: @BotFather → スタートを押す
3. コマンドを入力: /newbot
4. BotFather がボット名を尋ねる — 表示名を入力(例: "Đơn Hàng Brand X")
5. BotFather がユーザー名を尋ねる — "_bot" で終わるユーザー名を入力(例: brandx_orders_bot)— グローバルに一意である必要があります
6. BotFather が次の行を含むメッセージを返します:
"Use this token to access the HTTP API: 1234567890:AAxxxxxxxxxxxxxxxxxxxxxx"
7. そのトークンをコピーして、私に貼り付けてください。
セキュリティに関する注意: このトークンはボットを制御する権限を与えます。Git にコミットしたり、公開したりしないでください。もし漏洩した場合は、@BotFather に戻り → /token → /revoke と入力して新しいトークンを作成してください。
ステップ 2 — chat_id を取得
1対1チャット(オーナーのみが受信) — ソロプレナーに推奨:
1. Telegram で @userinfobot を検索 → スタートを押す
2. ボットが "Id: 123456789" を含むメッセージを返します — その番号をコピーします。
3. それがあなたの chat_id です。
グループチャット(営業チームで共同受信) — チームに推奨:
1. 新しい Telegram グループを作成する(または既存のグループを使用する)
2. グループ名をタップ → メンバーを追加 → 作成したボットを検索(ユーザー名 @brandx_orders_bot)→ 追加
3. グループ内で、/start@brandx_orders_bot と入力する(または任意のメッセージ)
4. ブラウザを開き、以下にアクセス:
https://api.telegram.org/bot<TOKEN>/getUpdates
(<TOKEN> をステップ1のトークンに置き換えてください)
5. JSON レスポンス内で "chat":{"id":-1001234567890,...} を探し、その番号をコピーします(マイナス記号があれば含めてください)
6. それがグループの chat_id です。
チャンネル(一方向ブロードキャスト、ディスカッションなし)— chat_id は -100 で始まります。取得方法はグループと同じですが、ボットをチャンネルの管理者として追加します。
ステップ 3 — スキルに貼り付ける
ユーザーが以下を貼り付けるのを待ちます:
TELEGRAM_BOT_TOKEN=1234567890:AAxxxxxxxxxxxxxxxxxxxxxx
TELEGRAM_CHAT_ID=123456789
重要なゲート: ユーザーが両方の値を貼り付けるまで、フェーズ 2 に進まないでください。ユーザーが "別の場所に既に持っています" と言った場合でも、貼り付けを待ちます。フェーズ 3 で .env.local に注入する必要があるからです。
フェーズ 2 — メッセージフォーマットをドラフト
デフォルトのベトナム語、モバイルフレンドリーなフォーマット(Telegram は HTML をレンダリング):
🎉 <b>ĐƠN HÀNG MỚI</b>
👤 <b>{{name}}</b>
📞 {{phone}}
📧 {{email}}
💰 <b>{{amount}}</b> — {{product_name}}
🕐 {{time_vn}}
🔗 Mã GD: <code>{{reference_code}}</code>
変数が注入された後:
🎉 ĐƠN HÀNG MỚI
👤 Nguyễn Văn A
📞 0901 234 567
📧 [email protected]
💰 499.000đ — Khoá học AI Agent
🕐 14:32 14/05/2026
🔗 Mã GD: FT26134567890
ユーザーにプレビューを表示し、尋ねます:
- このフォーマットでよろしいですか? 追加/削除したいフィールドはありますか?(例: UTM ソース、本日の注文件数、顧客の Zalo リンクなど)
- 絵文字が多すぎますか?(一部のユーザーはプレーンテキストを好みます)
- CRM/Sheets へのインライン CTA ボタン(リンク)を追加しますか?(高度 — スキルはインラインキーボードを追加できます)
フォーマットルール:
- HTML モード(
parse_mode: "HTML")— Telegram は<b>、<i>、<u>、<code>、<pre>、<a href>をサポートします。<br>はサポートされていません —\nを使用してください。 - ベトナムの金額フォーマット:
499.000đ(カンマは千単位の区切り、末尾にđ)または見やすい形式499K/1.99M。offer.jsonのvoice設定があればそれに従ってください。 - 電話番号フォーマット:
0901 234 567(3-3-3 区切り)— モバイルでの視認性を向上。 - ベトナム時間:
HH:mm dd/mm/yyyy— ベトナム時間(UTC+7)を設定し、UTC は使用しないでください。 - 絵文字の多用を避ける(メッセージあたり5つ以上の絵文字はプロフェッショナル感を損なう可能性があります)。
フェーズ 3 — コードを配線
3つの変更:
3.1 ヘルパー関数 lib/telegram.ts を作成
templates/lib-telegram.ts(TypeScript)または templates/lib-telegram.js(バニラ JS)を参照してください。内容:
// lib/telegram.ts
type TelegramPayload = {
name: string;
phone: string;
email: string;
amount: number; // VND, raw number (例: 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] Missing TELEGRAM_BOT_TOKEN or TELEGRAM_CHAT_ID');
return; // スローしない — Telegram の失敗が支払いレスポンスをブロックしてはいけない
}
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] Send failed: ${res.status} ${errBody}`);
}
} catch (err) {
console.error('[telegram] Network error:', err);
// エラーを飲み込む — Telegram の失敗で Sepay にリトライさせない
}
}
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>ĐƠN HÀNG MỚI</b>',
'',
`👤 <b>${escapeHtml(p.name)}</b>`,
`📞 ${phoneStr}`,
`📧 ${escapeHtml(p.email)}`,
`💰 <b>${amountStr}</b> — ${escapeHtml(p.productName)}`,
`🕐 ${now}`,
p.referenceCode ? `\n🔗 Mã GD: <code>${escapeHtml(p.referenceCode)}</code>` : '',
].filter(Boolean).join('\n');
}
function escapeHtml(s: string): string {
return s.replace(/[<>&]/g, c => ({ '<': '<', '>': '>', '&': '&' }[c]!));
}
なぜエラーを飲み込むのか: Sepay は non-200 を受信すると Webhook をリトライします。Telegram のタイムアウト/ダウンが Webhook をクラッシュさせると、Sepay がリトライし、リトライのたびにメールが再送信され、支払いが再ログされ、ユーザーは1つの注文に対して5通のメールと5つの Telegram アラートを受け取ることになります。Telegram は ベストエフォートのサイドチャネル であり、クリティカルパスではありません。
3.2 Sepay Webhook ハンドラーに配線
現在のハンドラーを読み取り、支払い成功処理箇所(Sepay ペイロードを解析し transferType === 'in' を検証した後)を特定します。以下を追加:
// app/api/payment-success/route.ts(または同等)
import { sendTelegramNotification } from '@/lib/telegram';
import { sendCustomerEmail, sendOwnerEmail } from '@/lib/resend'; // もし既にある場合
export async function POST(req: Request) {
const payload = await req.json();
// [既存] Sepay 認証を検証しペイロードを解析
if (payload.transferType !== 'in') {
return Response.json({ success: true }); // 送金を無視
}
// [既存] content(注文 ID を含む)または referenceCode に基づいてリードをルックアップ
const lead = await lookupLeadByTransferContent(payload.content);
// [新規] 3つの副作用を並列でファンアウト — Telegram の失敗はメールをブロックしない
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 });
}
Promise.all ではなく Promise.allSettled を使用する理由:
Promise.allはいずれかの promise が失敗すると reject します → ハンドラーが throw し → Sepay が 500 を受信 → リトライ → 重複メール。Promise.allSettledはすべての promise が settle(resolve または reject)するのを待ち、throw しません。
3.3 環境変数と .gitignore
# .env.local に追加
echo """
# Telegram Bot Notification
TELEGRAM_BOT_TOKEN=""
TELEGRAM_CHAT_ID=""
""" >> .env.local
.env.local が既に .gitignore にあることを確認:
grep -q '.env.local' .gitignore || echo '.env.local' >> .gitignore
最後に: ユーザーに、フェーズ 1 で取得したトークンと chat_id を .env.local に貼り付けるように指示します:
⚠️ 貼り付けが必要です:
1. .env.local を開く
2. TELEGRAM_BOT_TOKEN= の後にボットトークンを貼り付け
3. TELEGRAM_CHAT_ID= の後に chat_id を貼り付け
4. 保存 → npm run dev で再起動
フェーズ 4 — テスト計画
3段階:
レベル 1 — ボット直接テスト(ネットワーク確認)
# 設定が正しいか確認
curl -X POST "https://api.telegram.org/bot<TELEGRAM_BOT_TOKEN>/sendMessage" \
-H "Content-Type: application/json" \
-d '{
"chat_id": "<TELEGRAM_CHAT_ID>",
"text": "🧪 Telegram 通知システムのテスト",
"parse_mode": "HTML"
}'
# 期待される結果: Telegram で "🧪 Telegram 通知システムのテスト" が表示される
# 表示されない場合: トークン/chat_id を確認。トークンが無効な場合、403 エラーが返る
レベル 2 — シミュレートされた Sepay Webhook
フェーズ 3 のコードをデプロイせずにテスト。ローカルでシミュレート:
# ローカルサーバーを起動 (npm run dev)
# 新しいターミナルで:
curl -X POST "http://localhost:3000/api/sepay-webhook" \
-H "Content-Type: application/json" \
-d '{
"id": "test_telegram_001",
"transferType": "in",
"transferAmount": 499000,
"content": "TEST_ORDER_123",
"referenceCode": "TEST_REF001",
"accountNumber": "1234567890",
"gateway": "Vietcombank"
}'
# Terminal のログで確認:
# [telegram] Send success: 200 OK
# Telegram で受信したメッセージを確認
Raw モードの場合(リードストアなし)— フォーマットは異なりますが、テスト手順は同じです。Promise.allSettled により、リードルックアップが失敗しても Telegram が引き続き動作することを確認します。
レベル 3 — 本番確認
# 1 本番にデプロイ
# 2 実際の bank transfer を行う(数万 VND で十分)
# 3 Telegram で受信したメッセージを確認:
# - 名前、電話番号、メールが正しいか?
# - 金額が正しいか?(499.000đ vs 499K vs ...)
# - 時間がベトナム時間か?
# 4 Resend メールログでメールが送信されたか確認(Telegram の失敗はメールに影響しない)
ロールバック計画:
- 問題が発生した場合:
.env.localからTELEGRAM_BOT_TOKENとTELEGRAM_CHAT_IDを削除 → サーバーを再起動 → コードは残るが、トークンがないためsendTelegramNotificationは早期リターンしてエラーログを出力する。Sepay Webhook とメールは引き続き正常に動作する。 - それでも問題が解決しない場合:
lib/telegram.tsを削除し、Webhook ハンドラーからのインポートと呼び出しを削除する。
まとめ
| アーティファクト | 場所 |
|---|---|
| ヘルパー関数 | lib/telegram.ts |
| 環境変数 | TELEGRAM_BOT_TOKEN + TELEGRAM_CHAT_ID in .env.local |
| Webhook 統合 | app/api/payment-success/route.ts(または同等)に Promise.allSettled として配線 |
| メッセージテンプレート | lib/telegram.ts の formatMessage() 関数 |
| テストスクリプト | レベル 1 (curl)、レベル 2 (simulated webhook)、レベル 3 (production test) |