Stripe Checkout: thu tiền trong 30 phút
// File: stripe.ts
import Stripe from 'stripe'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY)
// Tạo checkout session — redirect user đến trang thanh toán Stripe
async function createCheckout(req: Request, res: Response) {
const session = await stripe.checkout.sessions.create({
mode: 'subscription', // hoặc 'payment' cho one-time
line_items: [{
price: 'price_pro_monthly', // price ID từ Stripe Dashboard
quantity: 1,
}],
customer_email: req.user.email,
metadata: { userId: req.user.id }, // gắn user ID để track
success_url: `${BASE_URL}/payment/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${BASE_URL}/pricing`,
})
res.json({ url: session.url })
}
Webhook: khi nào Stripe nói “tiền đã vào”
// File: webhook.ts
// Stripe gửi webhook khi payment thành công
// QUAN TRỌNG: verify signature để chống fake webhook
async function handleWebhook(req: Request, res: Response) {
const sig = req.headers["stripe-signature"]
let event: Stripe.Event
try {
event = stripe.webhooks.constructEvent(
req.body, sig, process.env.STRIPE_WEBHOOK_SECRET
)
} catch (err) {
return res.status(400).json({ error: "Invalid signature" })
}
// Xử lý theo event type
switch (event.type) {
case "checkout.session.completed": {
const session = event.data.object
const userId = session.metadata.userId
// Idempotency: check đã xử lý chưa
const exists = await db.payments.findByStripeId(session.id)
if (exists) break // đã xử lý rồi → skip
await db.users.update(userId, { plan: "pro" })
await db.payments.create({
stripeSessionId: session.id,
userId, amount: session.amount_total,
})
break
}
case "customer.subscription.deleted": {
// User huỷ subscription → downgrade
const sub = event.data.object
await db.users.update(sub.metadata.userId, { plan: "free" })
break
}
}
res.json({ received: true })
}
⚠ Idempotency là BẮT BUỘC
Stripe có thể gửi cùng webhook NHIỀU LẦN (retry). Nếu bạn không check idempotency → charge user 2 lần, tạo 2 account, v.v. Luôn check “đã xử lý event này chưa” trước khi thực hiện.
VNPay / MoMo cho thị trường Việt Nam
Stripe chưa hỗ trợ đầy đủ tại VN. Dùng VNPay (phổ biến nhất) hoặc MoMo cho thanh toán nội địa. Flow tương tự: tạo URL → redirect user → nhận IPN (webhook).
❓ Vì sao cần idempotency khi xử lý webhook thanh toán?
- Tạo Stripe Checkout session
- Xử lý webhook với signature verification
- Implement idempotency check
- Xử lý subscription cancellation / downgrade
- Biết flow VNPay/MoMo cho thị trường VN