Bài này show code tích hợp BeePay vào Next.js 14 (App Router) + Postgres + Prisma — production-grade. Copy paste là chạy được sau khi điền .env.

1. Schema Prisma

model Order {
  id            String   @id @default(cuid())
  publicId      String   @unique  // SHOP-ABC123 — match với BeePay
  amount        Int
  status        OrderStatus @default(PENDING)
  customerEmail String?
  bankRef       String?
  paidAt        DateTime?
  createdAt     DateTime @default(now())
  expiresAt     DateTime
}

enum OrderStatus { PENDING PAID EXPIRED }

2. API tạo order

// app/api/orders/route.ts
import { NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'

export async function POST(req: Request) {
  const { amount, customerEmail } = await req.json()
  const publicId = 'SHOP' + Date.now().toString(36).toUpperCase()
  const order = await prisma.order.create({
    data: {
      publicId, amount, customerEmail,
      expiresAt: new Date(Date.now() + 15 * 60_000),
    },
  })
  const qrUrl = `https://qr.beepay.vn/img/MB-${BANK_ACCOUNT}.png?amount=${amount}&addInfo=${publicId}`
  return NextResponse.json({
    orderId: order.publicId,
    qrUrl,
    bank: { code: 'MB', account: BANK_ACCOUNT, holder: HOLDER },
    content: publicId,
    expiresIn: 900,
  })
}

3. Webhook receiver

// app/api/beepay-webhook/route.ts
import crypto from 'crypto'
import { prisma } from '@/lib/prisma'

export async function POST(req: Request) {
  const raw = await req.text()
  const sig = req.headers.get('x-webhook-signature') || ''
  const expected = 'sha256=' + crypto
    .createHmac('sha256', process.env.BEEPAY_WEBHOOK_SECRET!)
    .update(raw)
    .digest('hex')
  if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig))) {
    return new Response('forbidden', { status: 401 })
  }

  const body = JSON.parse(raw)
  if (body.event !== 'bank_transaction') {
    return Response.json({ ok: true, ignored: true })
  }

  const order = await prisma.order.findUnique({
    where: { publicId: body.order_id },
  })
  if (!order) return Response.json({ ok: true, skipped: 'not our order' })
  if (order.status === 'PAID') return Response.json({ ok: true, already_paid: true })

  const amountReceived = parseFloat(body.amount)
  if (amountReceived < order.amount) {
    return Response.json({ ok: true, skipped: 'amount mismatch' })
  }

  await prisma.order.update({
    where: { id: order.id },
    data: {
      status: 'PAID',
      bankRef: body.transaction_ref,
      paidAt: new Date(),
    },
  })

  // Trigger downstream: send email, kích hoạt license, ...
  await sendConfirmationEmail(order)

  return Response.json({ ok: true })
}

4. Polling status endpoint

// app/api/orders/[id]/route.ts
export async function GET(_req: Request, { params }: { params: { id: string } }) {
  const order = await prisma.order.findUnique({ where: { publicId: params.id } })
  if (!order) return Response.json({ ok: false }, { status: 404 })
  return Response.json({
    status: order.status,
    paidAt: order.paidAt,
    amount: order.amount,
  })
}

5. Client component — modal checkout

'use client'
import { useEffect, useRef, useState } from 'react'

export default function PayModal({ orderId, qrUrl }: Props) {
  const [status, setStatus] = useState<'waiting' | 'paid' | 'expired'>('waiting')
  const ref = useRef<ReturnType<typeof setInterval> | null>(null)

  useEffect(() => {
    ref.current = setInterval(async () => {
      const r = await fetch(`/api/orders/${orderId}`).then(r => r.json())
      if (r.status === 'PAID') {
        clearInterval(ref.current!)
        setStatus('paid')
      }
    }, 3000)
    return () => { if (ref.current) clearInterval(ref.current) }
  }, [orderId])

  return status === 'paid'
    ? <div>✓ Thành công</div>
    : <img src={qrUrl} alt="QR" />
}

Setup script tự động

Boilerplate đầy đủ — gồm setup script tự gọi BeePay API tạo system + register webhook + lưu secret vào .env — có sẵn tại /tai-nguyen/nextjs-saas-boilerplate. git clonenpm install → điền .env → npm run setup → done.