Thiết lập Thanh toán Sepay Biz — Hạ tầng thanh toán VietQR End-to-end cho Next.js
Kỹ năng này xây dựng toàn bộ hạ tầng thanh toán cho landing page Next.js sử dụng Sepay (cổng thanh toán VietQR Việt Nam). Sau khi hoàn tất: khách điền form → tạo order → quét QR → chuyển khoản → Sepay webhook về → xác minh + tra cứu lead + kích hoạt email/telegram. Tự động hết, không cần admin xác nhận thủ công.
Triết lý: Sepay là con đường ít kháng cự nhất cho VN — VietQR + bank API thuần, không cần khách đăng ký gì, scan QR bằng app ngân hàng có sẵn. Phí 1.5-2% rẻ hơn Stripe (4-5%), thanh toán T+1, free tier hợp lý cho landing page <100 đơn/tháng.
Tại sao cần lead store: Sepay webhook chỉ gửi
content(text khách dán khi chuyển khoản) + amount. Sepay KHÔNG biết khách là ai. Phải có lead store local để mapcontent→ lead{name, phone, email, product, amount}.
2 tùy chọn lead store — người dùng chọn 1 ở Giai đoạn 0:
- Vercel KV (Upstash Redis) — thiết lập 2 phút, TTL gốc, free 30K commands/tháng → < 500 đơn/tháng
- Supabase Free (Postgres) — thiết lập 5 phút, SQL queries + Studio UI, free tier rộng hơn ~50× → < 2000 đơn/tháng. KHUYẾN NGHỊ.
Các route API (/api/checkout, /api/sepay-webhook, /api/admin/leads) không phụ thuộc vào provider thông qua lib/leads-store.ts re-export mỏng. Đổi provider sau = sửa 1 dòng.
Kỹ năng KHÔNG tự đăng ký Sepay account cho người dùng (cần KYC doanh nghiệp + ngân hàng). Kỹ năng hướng dẫn người dùng từng bước, sau đó tích hợp mã.
Khi nào dùng kỹ năng này
- Người dùng đang ra mắt sản phẩm số / khoá học / coaching và cần nhận thanh toán trực tuyến.
- Người dùng đã có landing page Next.js (qua
ui-ux-pro-maxhoặc tương đương) và muốn thêm luồng thanh toán. - Người dùng chuẩn bị tích hợp
biz-email-setuphoặcbiz-telegram-payment-notifymà chưa có hạ tầng thanh toán → kỹ năng này LÀM TIỀN ĐỀ.
KHÔNG dùng kỹ năng này khi:
- Người dùng dùng Stripe/PayPal (thanh toán thẻ quốc tế) → use case khác.
- Người dùng chấp nhận chuyển khoản ngân hàng thủ công + tự xác minh (quy mô nhỏ, không khuyến khích).
- Người dùng đã có hạ tầng thanh toán và chỉ thiếu thông báo → đi thẳng
/biz-email-setuphoặc/biz-telegram-payment-notify.
Workflow tổng quan (7 giai đoạn)
Giai đoạn 0: DETECT next.js stack + HỎI provider (KV / Supabase) — GATE
↓
Giai đoạn 1: HƯỚNG DẪN người dùng thiết lập Sepay account — GATE thủ công đợi người dùng xong
↓
Giai đoạn 2A (nếu KV): THIẾT LẬP Vercel KV (cài package + env vars + KV namespace)
Giai đoạn 2B (nếu Supabase): THIẾT LẬP Supabase (tạo project + migration SQL + service_role)
↓
Giai đoạn 3: SCAFFOLD lib/leads-store.ts (re-export wrapper) + lib/leads-{kv|supabase}.ts (impl)
↓
Giai đoạn 4: SCAFFOLD /api/checkout route (form → order → store)
↓
Giai đoạn 5: SCAFFOLD /api/sepay-webhook route (xác minh + tra cứu + side effects placeholder)
↓
Giai đoạn 6: NHÚNG VietQR vào landing page (chọn 1 trong 3 pattern UX)
↓
Giai đoạn 7: KẾ HOẠCH test 5 cấp (store → checkout → webhook → production → (Supabase only) cron ping)
Giai đoạn 0 + 1 có gate đợi người dùng. Giai đoạn 2/3/4/5/6 kỹ năng chủ động.
Giai đoạn 0 — Detect Next.js stack + chọn provider
0.1 Detect 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"
Nếu không phải Next.js: DỪNG. Báo: "Kỹ năng này hiện chỉ hỗ trợ Next.js. Stack hiện tại của anh/chị là [X]. Em port logic sang Vite/static với Vercel function không?" — đợi người dùng.
Nếu cả app/ và pages/: hỏi người dùng dùng cái nào.
0.2 Kiểm tra hạ tầng hiện có
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 HỎI người dùng chọn provider
Em phát hiện anh/chị dùng Next.js [App/Pages] Router + TypeScript + [pnpm/yarn/npm].
Anh/chị muốn lưu lead store ở đâu?
1. Vercel KV (Upstash Redis)
✓ Thiết lập nhanh 2 phút (1-click trong Vercel dashboard)
✓ Native TTL — không lo project pause
✗ Free chỉ 30K commands/tháng → ~500 đơn/tháng
✗ Không có SQL — admin filter phức tạp khó
2. Supabase Free (Postgres) — KHUYẾN NGHỊ
✓ Free tier rộng nhất (500MB DB + 5GB bandwidth, unlimited query)
✓ Postgres = SQL query mạnh, Supabase Studio UI đẹp để view data
✓ Type-safe TypeScript auto-generated
✓ Thiết lập 5 phút (Console + paste 2 env vars)
⚠ Project auto-pause sau 7 ngày silence → em wire Vercel Cron ping `/api/health` mỗi 6 ngày để fix
⚠ TTL manual qua pg_cron (đã wire trong migration SQL)
→ Phù hợp < 2000 đơn/tháng, dễ scale admin features
Anh/chị chọn 1 hay 2?
GATE: KHÔNG đi Giai đoạn 1 cho đến khi người dùng chọn. Default suggest Supabase nhưng force chọn explicit.
Sau khi người dùng chọn, tóm tắt plan:
Anh/chị chọn [Vercel KV / Supabase]. Em sẽ:
- Hướng dẫn anh/chị thiết lập Sepay account (5-10 phút)
- Thiết lập lead store theo provider đã chọn
- Wire code: lib/leads-store.ts + /api/checkout + /api/sepay-webhook + nhúng VietQR
- Test
Sẵn sàng đi Giai đoạn 1 chưa anh/chị?
Giai đoạn 1 — Hướng dẫn người dùng thiết lập Sepay account
Đọc references/sepay-account-setup.md cho full guide. Tóm tắt 5 bước cho người dùng:
Bước 1 — Đăng ký my.sepay.vn
- Truy cập https://my.sepay.vn → Đăng ký
- Điền SĐT, email, mật khẩu → verify OTP
- Hoàn tất profile: Họ tên + CCCD/CMND (cá nhân) hoặc Mã số thuế (công ty)
Bước 2 — Liên kết tài khoản ngân hàng
Dashboard → Tài khoản ngân hàng → Thêm tài khoản:
- Chọn ngân hàng (Vietcombank, Techcombank, MB, ACB, VPBank, BIDV, VietinBank, TPBank, ...)
- Nhập số TK + tên chủ TK (khớp với app banking)
- Verify tự động (qua bank API) hoặc thủ công (chuyển 1.000-10.000đ với mã xác minh Sepay đưa)
Bước 3 — Lấy API key
Dashboard → Cài đặt → API & Webhook → Tạo API Key mới:
- Name:
landing-page-prod - Permissions:
webhook:read - Copy key (chỉ hiện 1 lần)
Bước 4 — Thiết lập webhook URL
Cùng trang → Webhook → Thêm webhook:
- URL:
https://yourdomain.vn/api/sepay-webhook(sẽ live sau deploy) - Auth: API Key → paste key vừa tạo
- Events:
transaction:incoming - Save
⚠️ Local dev: dùng ngrok expose
localhost:3000cho Sepay test.
Bước 5 — Paste cho kỹ năng
SEPAY_WEBHOOK_API_KEY=sk_xxxxxxxxxxxxxxxxxxxx
SEPAY_BANK_ACCOUNT_NUMBER=1023456789
SEPAY_BANK_NAME=Vietcombank
SEPAY_ACCOUNT_NAME=NGUYEN VAN A
GATE: KHÔNG đi Giai đoạn 2 cho đến khi người dùng paste đủ.
Giai đoạn 2A — Thiết lập Vercel KV (chỉ nếu người dùng chọn KV ở Giai đoạn 0)
Vercel KV = Upstash Redis managed. Free 30K commands/tháng.
2A.1 Tạo KV namespace
Vercel dashboard → Project → Storage → Create Database → KV (Redis) → name sepay-leads → Create.
Auto-generate 4 env vars:
KV_URL=...
KV_REST_API_URL=...
KV_REST_API_TOKEN=...
KV_REST_API_READ_ONLY_TOKEN=...
Bấm Connect to Project → Vercel inject env (Production + Preview + Development).
2A.2 Kéo env xuống local
npm i -g vercel # nếu chưa có
vercel link
vercel env pull .env.local
2A.3 Cài package
pnpm add @vercel/kv # hoặc yarn / npm install
2A.4 Verify
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');
});
"
Expect KV OK: hello. Đi Giai đoạn 3.
Giai đoạn 2B — Thiết lập Supabase Free (chỉ nếu người dùng chọn Supabase ở Giai đoạn 0)
Đọc references/supabase-setup.md cho full guide chi tiết 6 bước. Tóm tắt:
2B.1 Tạo Supabase account + project
- https://supabase.com → Start your project → Login GitHub
- New project:
- Name:
sepay-leads-<brand> - Database Password: bấm Generate → save
- Region: ⚠️ Southeast Asia (Singapore) (default us-east latency 250ms+)
- Pricing: Free
- Name:
- Đợi ~2 phút provision
2B.2 Chạy migration SQL
Sidebar → SQL Editor → New query → paste nội dung templates/supabase-migration.sql → Run.
Tạo 4 bảng + indexes + next_order_id() function + RLS enable + pg_cron daily cleanup job.
Verify output: 4 bảng (leads/phone_index/order_counter/webhook_dedup) + 1 pg_cron job + next_order_id() trả "DH000001".
Reset counter (vì verify đã tăng counter):
UPDATE order_counter SET current_value = 0 WHERE id = 1;
DELETE FROM leads WHERE order_id = 'DH000001';
2B.3 Lấy API credentials
Project Settings → API:
- Copy Project URL (
https://xxxxx.supabase.co) - Copy
service_rolesecret (KHÔNGanonkey — service_role bypass RLS, dùng cho server)
2B.4 Paste env vars
SUPABASE_URL=https://xxxxx.supabase.co
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJI...
Kỹ năng tự động verify:
- URL match
^https://[a-z0-9]+\.supabase\.co$ - Key decode JWT, check
role: "service_role"(KHÔNGanon)
Nếu sai → báo lỗi, xin paste lại.
2B.5 Cài package
pnpm add @supabase/supabase-js
2B.6 Wire Vercel Cron ping (chống pause)
Supabase Free tự động pause sau 7 ngày silence. Workaround: ping /api/health mỗi 6 ngày.
Kỹ năng scaffold app/api/health/route.ts (từ templates/api-health-route.ts) + append vercel.json với:
{
"crons": [
{ "path": "/api/health", "schedule": "0 3 */6 * *" }
]
}
⚠️ Vercel Cron Hobby giới hạn 2 cron/tháng. Pattern */6 days = ~5 lần/tháng → vượt quota. Các lựa chọn:
- Vercel Pro ($20/tháng) — unlimited cron
- GitHub Actions workflow
.github/workflows/keep-alive.yml(FREE) — kỹ năng scaffold nếu detect repo GitHub - cron-job.org (FREE) — thiết lập thủ công, paste URL
/api/health
Kỹ năng mặc định chọn option 2 (GitHub Actions) nếu repo có .git/config với GitHub remote. Otherwise printout option 3 instruction.
2B.7 Verify
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);
});
"
Expect Supabase OK. current_value = 0. Đi Giai đoạn 3.
Giai đoạn 3 — Scaffold lib/leads-store.ts + impl
Provider abstraction: Các route API import từ lib/leads-store.ts, không biết underlying là KV hay Postgres.
Nếu chọn KV (Giai đoạn 0)
Scaffold 2 file:
lib/leads-store.ts← copytemplates/lib-leads-store-kv.ts(1 dòngexport * from './leads-kv')lib/leads-kv.ts← copytemplates/lib-leads-kv.ts(KV impl, ~160 dòng)
Nếu chọn Supabase
Scaffold 3 file:
lib/leads-store.ts← copytemplates/lib-leads-store-supabase.ts(re-export Supabase)lib/leads-supabase.ts← copytemplates/lib-leads-supabase.ts(Postgres impl + listLeads cho admin)lib/supabase-admin.ts← copytemplates/lib-supabase-admin.ts(admin client cached singleton với service_role)
API surface CHUNG (cả 2 impl):
createLead(input) → { orderId, lead }— atomic ID + insert + secondary indexgetLeadByOrderId(orderId) → Lead | nullgetLeadByPhone(phone) → Lead | null— fallback lookupfindPendingLeadByAmountAndTime(amount, start, end) → Lead[]— Strategy 3 webhook matchingmarkLeadPaid(orderId, payment) → Lead | nullisTransactionProcessed(sepayId) → boolean— dedupmarkTransactionProcessed(sepayId) → void
TTL: 7 ngày pending / 90 ngày paid. KV qua ex: param. Supabase qua expire_at field + pg_cron daily delete.
Đổi provider sau = sửa 1 dòng leads-store.ts (KHÔNG đụng API code).
Đọc references/leads-kv-schema.md (KV) hoặc references/supabase-leads-schema.md (Supabase) cho rationale chi tiết.
Giai đoạn 4 — Scaffold /api/checkout route
Đọc templates/api-checkout-app-router.ts (App Router) hoặc templates/api-checkout-pages-router.ts (Pages Router). Copy vào path tương ứng.
Cả 2 template import { createLead } from '@/lib/leads-store' — provider-agnostic.
Flow:
Client POST /api/checkout { name, phone, email, productName, amount }
↓
Server:
1. Validate (phone VN format, email regex, amount > 0)
2. createLead() → { orderId: "DH000123" }
3. Generate Sepay QR URL: https://qr.sepay.vn/img?acc=X&bank=Y&amount=Z&des=DH000123&template=compact
4. Return { orderId, amount, bankInfo, content, qrUrl }
↓
Client: show QR modal HOẶC redirect /checkout/DH000123
Giai đoạn 5 — Scaffold /api/sepay-webhook route
Đọc templates/api-sepay-webhook-app-router.ts. Copy vào app/api/sepay-webhook/route.ts.
Template import ... from '@/lib/leads-store' — provider-agnostic.
Flow:
Sepay POST /api/sepay-webhook
Authorization: Apikey {SEPAY_WEBHOOK_API_KEY}
Body: SepayWebhookPayload
↓
Server:
1. Xác minh auth qua timingSafeEqual — chống timing attack
2. Early dedup: isTransactionProcessed(payload.id) → return 200 sớm
3. Record dedup FIRST — idempotent ngay cả khi processing fail
4. Filter transferType === 'in' (bỏ outgoing)
5. Multi-strategy matching:
a. content → order_id (regex /DH\d+/)
b. content → phone (regex /0\d{9}/) → getLeadByPhone
c. amount + timestamp ±30min → findPendingLeadByAmountAndTime (chỉ accept length === 1)
6. Reject underpayment, ACCEPT overpayment
7. markLeadPaid(orderId, payment) → update store
8. Fan-out side effects qua try/catch loop (KHÔNG Promise.all)
9. ALWAYS return 200 (kể cả internal error) — Sepay retry Fibonacci 7 lần nếu non-200
Side effects placeholder cho biz-email-setup + biz-telegram-payment-notify wire vào sau.
Đọc references/sepay-webhook-flow.md cho dedup rationale + retry behavior.
Giai đoạn 6 — Nhúng VietQR vào landing page
3 pattern UX, hỏi người dùng chọn 1:
| Pattern | Khi nào | UX |
|---|---|---|
| A. Modal popup | Sản phẩm đơn / mua impulse | Submit form → modal popup QR ngay tại trang gốc |
| B. /checkout/[orderId] page | Đa sản phẩm / cần share URL … |
…
[đã cắt ngắn để dịch]