نمونه کد بکاند
این صفحه گامبهگام نشان میدهد چطور یک سرویس کامل پرداخت بسازید.
نمای کلی
سرویس پرداخت سه عمل اصلی را انجام میدهد:
initiatePayment— تراکنش را در DB میسازد و از درگاهpay_urlمیگیرد.handleCallback— بعد از بازگشت کاربر از درگاه، وضعیت را تشخیص میدهد.verifyTransaction— پرداخت را در درگاه نهایی میکند و اشتراک را فعال میکند.
سه HTTP endpoint هم بالای سرویس قرار میگیرد: POST /payment/initiate، GET /payment/callback، و POST /payment/verify.
ساختار پروژه
project/
├── services/
│ └── payment.{ext} # PaymentService
├── routes/ # یا controllers/
│ └── payment.{ext}
├── middleware/
│ └── auth.{ext}
└── .envمرحله ۱ — نصب وابستگیها
Node.js
npm install axios uuid dotenv express
npm install -D typescript @types/uuid @types/expressمرحله ۲ — تعریف Config
Node.js
// services/payment.config.ts
export interface PaymentConfig {
apiUrl: string
secret: string
callbackUrl: string
}
export const paymentConfig: PaymentConfig = {
apiUrl: process.env.PAYMENT_GATEWAY_API!,
secret: process.env.PAYMENT_GATEWAY_SECRET!,
callbackUrl: process.env.PAYMENT_CALLBACK_URL!,
}مرحله ۳ — ساخت Payment Service
این کلاس قلب سرویس است. همهی منطق تراکنش، Verify و فعالسازی اشتراک اینجاست.
Node.js
// services/payment.service.ts
import { v4 as uuidv4 } from 'uuid'
import axios from 'axios'
import { PaymentConfig } from './payment.config'
export class PaymentService {
constructor(private config: PaymentConfig, private db: any) {}
static tomanToRial(toman: number) { return toman * 10 }
static rialToToman(rial: number) { return Math.round(rial / 10) }
private generateReferenceId(userId: number, plan: string) {
const random = uuidv4().replace(/-/g, '').substring(0, 12)
return `${plan.toUpperCase()}-${userId}-${Date.now()}-${random}`
}
async initiatePayment(userId: number, plan: string, amountRial: number) {
const referenceId = this.generateReferenceId(userId, plan)
await this.db.query(
`INSERT INTO payment_transactions (user_id, reference_id, plan, amount, status)
VALUES ($1, $2, $3, $4, 'pending')`,
[userId, referenceId, plan, amountRial]
)
try {
const { data } = await axios.post(
`${this.config.apiUrl}/pay/transactions`,
{
reference_id: referenceId,
amount: amountRial,
description: this.describePlan(plan),
callback_url: this.config.callbackUrl,
},
{ headers: { 'X-Gateway-Secret': this.config.secret }, timeout: 30_000 }
)
await this.db.query(
`UPDATE payment_transactions
SET meta_data = $1, fee = $2, updated_at = NOW()
WHERE reference_id = $3`,
[JSON.stringify({ hash_id: data.hash_id }), data.order?.fee ?? null, referenceId]
)
return { url: data.pay_url, hashId: data.hash_id, referenceId }
} catch (err: any) {
await this.markFailed(referenceId)
throw new Error(err.response?.data?.message || 'خطا در اتصال به درگاه پرداخت')
}
}
async handleCallback(params: {
hashId: string
referenceId: string
status: string
refId?: string
}) {
const { rows } = await this.db.query(
`SELECT * FROM payment_transactions WHERE reference_id = $1`,
[params.referenceId]
)
if (rows.length === 0) throw new Error('TRANSACTION_NOT_FOUND')
const tx = rows[0]
if (params.refId) {
await this.db.query(
`UPDATE payment_transactions SET ref_id = $1, updated_at = NOW() WHERE reference_id = $2`,
[params.refId, params.referenceId]
)
}
if (params.status === 'failed') {
await this.markFailed(params.referenceId)
return { userId: tx.user_id, plan: tx.plan, success: false }
}
// در صورت ابهام، با inquiry وضعیت را روشن کن
let status = params.status
if (!['success', 'unverified'].includes(status)) {
const inquiry = await this.inquiry(params.hashId)
if (inquiry.status?.id === 3) status = 'success'
else if (inquiry.status?.id === 5) status = 'unverified'
}
if (status === 'success' || status === 'unverified') {
const result = await this.verifyTransaction(params.hashId, params.referenceId, tx.user_id)
return { userId: tx.user_id, plan: tx.plan, success: result.success }
}
return { userId: tx.user_id, plan: tx.plan, success: false }
}
async verifyTransaction(hashId: string, referenceId: string, userId: number) {
const { rows } = await this.db.query(
`SELECT * FROM payment_transactions WHERE reference_id = $1`,
[referenceId]
)
const tx = rows[0]
// Idempotency: اگر قبلاً Verify شده، دوباره نزن
if (tx.verified_at) return { success: true, plan: tx.plan }
try {
const { data } = await axios.post(
`${this.config.apiUrl}/pay/transactions/${hashId}/verify`,
{},
{ headers: { 'X-Gateway-Secret': this.config.secret }, timeout: 30_000 }
)
if (data.status?.id !== 3) throw new Error('PAYMENT_NOT_CONFIRMED')
await this.db.query(
`UPDATE payment_transactions
SET status = 'verified', verified_at = NOW(),
transaction_id = $1, ref_id = COALESCE($2, ref_id), updated_at = NOW()
WHERE reference_id = $3`,
[hashId, data.ref_id, referenceId]
)
await this.activateSubscription(userId, tx.plan, hashId)
return { success: true, plan: tx.plan }
} catch (err: any) {
// اگر در سمت درگاه قبلاً Verify شده بود، آن را موفق در نظر بگیر
const status = err.response?.status
const msg = err.response?.data?.message ?? ''
if (status === 410 || (status === 422 && msg.includes('قبلاً'))) {
await this.db.query(
`UPDATE payment_transactions
SET status = 'verified', verified_at = NOW(), updated_at = NOW()
WHERE reference_id = $1`,
[referenceId]
)
await this.activateSubscription(userId, tx.plan, hashId)
return { success: true, plan: tx.plan }
}
await this.markFailed(referenceId)
throw err
}
}
private async inquiry(hashId: string) {
const { data } = await axios.get(
`${this.config.apiUrl}/pay/transactions/${hashId}/inquiry`,
{ headers: { 'X-Gateway-Secret': this.config.secret }, timeout: 30_000 }
)
return data
}
private async activateSubscription(userId: number, plan: string, paymentRef: string) {
const days = plan === 'premium_monthly' ? 30 : 365
const startsAt = new Date()
const endsAt = new Date(Date.now() + days * 86400 * 1000)
await this.db.query(
`INSERT INTO subscriptions (user_id, plan, status, starts_at, ends_at, payment_ref)
VALUES ($1, $2, 'active', $3, $4, $5)`,
[userId, plan, startsAt, endsAt, paymentRef]
)
await this.db.query(`UPDATE users SET plan = $1 WHERE id = $2`, [plan, userId])
}
private async markFailed(referenceId: string) {
await this.db.query(
`UPDATE payment_transactions SET status = 'failed', updated_at = NOW() WHERE reference_id = $1`,
[referenceId]
)
}
private describePlan(plan: string) {
return plan === 'premium_monthly' ? 'اشتراک پریمیوم ماهانه' : 'اشتراک پریمیوم سالانه'
}
}دو نکتهی مهم در کد بالا:
- Idempotency: قبل از Verify، چک میکنیم که
verified_atپر نشده باشد. اگر Callback دوبار صدا زده شود (که اتفاق میافتد)، تراکنش دوبار Verify نمیشود. - پیام «قبلاً»: اگر در Verify، درگاه با کد ۴۱۰ یا ۴۲۲ پاسخ داد و پیام شامل «قبلاً» بود، یعنی پرداخت قبلاً Verify شده — آن را موفق در نظر بگیرید.
مرحله ۴ — HTTP Routes / Controllers
این لایه HTTP request ها را به Service تبدیل میکند. سه endpoint داریم:
POST /api/payment/initiate— احراز هویت لازم استGET /api/payment/callback— عمومی (چون از خود درگاه فراخوانی میشود)POST /api/payment/verify— احراز هویت لازم است
Node.js
// routes/payment.routes.ts
import express from 'express'
import { PaymentService } from '../services/payment.service'
import { paymentConfig } from '../services/payment.config'
import { authMiddleware } from '../middleware/auth'
import { db } from '../db'
const router = express.Router()
const paymentService = new PaymentService(paymentConfig, db)
const PRICES_TOMAN: Record<string, number> = {
premium_monthly: 50_000,
premium_yearly: 500_000,
}
router.post('/initiate', authMiddleware, async (req, res) => {
const { plan } = req.body
if (!PRICES_TOMAN[plan]) {
return res.status(400).json({ error: 'INVALID_PLAN' })
}
try {
const amountRial = PaymentService.tomanToRial(PRICES_TOMAN[plan])
const result = await paymentService.initiatePayment(req.user.id, plan, amountRial)
res.status(201).json(result)
} catch (err: any) {
res.status(500).json({ error: err.message })
}
})
router.get('/callback', async (req, res) => {
try {
const result = await paymentService.handleCallback({
hashId: String(req.query.hash_id),
referenceId: String(req.query.reference_id),
status: String(req.query.status),
refId: req.query.ref_id ? String(req.query.ref_id) : undefined,
})
res.redirect(
result.success
? `/payment/result?status=success&plan=${result.plan}`
: `/payment/result?status=failed`
)
} catch {
res.redirect('/payment/result?status=failed')
}
})
router.post('/verify', authMiddleware, async (req, res) => {
try {
const { hash_id, reference_id } = req.body
const result = await paymentService.verifyTransaction(hash_id, reference_id, req.user.id)
res.json(result)
} catch (err: any) {
res.status(422).json({ error: err.message })
}
})
export default routerمرحله ۵ — راهاندازی برنامه
Node.js
// app.ts
import express from 'express'
import paymentRoutes from './routes/payment.routes'
const app = express()
app.use(express.json())
app.use('/api/payment', paymentRoutes)
app.listen(3000, () => console.log('Server on :3000'))قدم بعدی
- صفحات کاربر: فرانتاند (Checkout و Callback در React/Vue/Angular)
Last updated on