Skip to Content
APIهای درگاه پرداخت اپ‌استور باسلام منتشر شد — مشاهده APIها
درگاه پرداخت اپ‌استورنمونه کد بک‌اند

نمونه کد بک‌اند

این صفحه گام‌به‌گام نشان می‌دهد چطور یک سرویس کامل پرداخت بسازید.

نمای کلی

سرویس پرداخت سه عمل اصلی را انجام می‌دهد:

  1. initiatePayment — تراکنش را در DB می‌سازد و از درگاه pay_url می‌گیرد.
  2. handleCallback — بعد از بازگشت کاربر از درگاه، وضعیت را تشخیص می‌دهد.
  3. 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

مرحله ۱ — نصب وابستگی‌ها

npm install axios uuid dotenv express npm install -D typescript @types/uuid @types/express

مرحله ۲ — تعریف Config

// 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 و فعال‌سازی اشتراک اینجاست.

// 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 — احراز هویت لازم است
// 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

مرحله ۵ — راه‌اندازی برنامه

// 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'))

قدم بعدی

Last updated on