This commit is contained in:
payacom
2026-10-06 17:08:44 +03:30
parent fade8543e2
commit 367416d419
14 changed files with 6087 additions and 5937 deletions

View File

@@ -6,6 +6,8 @@ const OrderModel = require('../../../models/OrderModel')
const UserModel = require('../../../models/UserModel')
const WalletTransactionModel = require('../../../models/WalletTransactionModel')
const WalletWithdrawalRequestModel = require('../../../models/WalletWithdrawalRequestModel')
const PaymentModel = require('../../../models/PaymentModel')
const { buildPaymentPageUrl } = require('../../../utils/zarinpalPaymentPage')
const MERCHANT_ID = process.env.ZARINPAL_MERCHANT_ID || process.env.MERCHENT_CODE
const GIFT_WITHDRAWAL_MIN = 1000000
@@ -199,49 +201,89 @@ const initiateWalletCharge = async (req, res, next) => {
return walletError(res, 422, 'مبلغ نامعتبر است')
}
const callbackBase = process.env.APP_SITE_CallBack || process.env.APP_URL
const callbackBase = (process.env.APP_SITE_CallBack || process.env.APP_URL || '').replace(/\/$/, '')
if (!callbackBase) {
return walletError(res, 500, 'آدرس بازگشت پرداخت تنظیم نشده است')
}
const platformSuffix = platform ? `&platform=${encodeURIComponent(platform)}` : ''
// کال‌بک باید یک آدرس ثابت و بدون query string باشد تا با دامنه/Callback
// ثبت‌شده در زرین‌پال دقیقاً یکی باشد. اطلاعات پرداخت را در دیتابیس نگه می‌داریم.
const callbackUrl = `${callbackBase}/wallet/charge/callback`
const paymentType = platform === 'android' ? 'wallet_charge_android' : 'wallet_charge_web'
const response = await axios.post(
'https://api.zarinpal.com/pg/v4/payment/request.json',
{
merchant_id: MERCHANT_ID,
amount: toRial(chargeAmount),
description: 'شارژ کیف پول مدستاگرام',
callback_url: `${callbackBase}/wallet/charge/callback?userId=${req.user._id}&amount=${chargeAmount}${platformSuffix}`
callback_url: callbackUrl
},
{ headers: { 'Content-Type': 'application/json' } }
{ headers: { 'Content-Type': 'application/json' }, timeout: 20000 }
)
const result = response.data?.data
if (result?.code === 100 && result?.authority) {
return res.json({ authority: result.authority })
await PaymentModel.create({
amount: chargeAmount,
status: 'pending',
authority: result.authority,
user_id: req.user._id,
type: paymentType
})
return res.json({
authority: result.authority,
paymentUrl: `${buildPaymentPageUrl(result.authority)}`
})
}
console.error('ZarinPal wallet charge initiate error:', response.data)
return walletError(res, 502, 'درگاه پرداخت پاسخ نامعتبر داد. لطفاً دوباره تلاش کنید.')
} catch (error) {
console.error('initiateWalletCharge error:', error?.response?.data || error?.message || error)
next(error)
}
}
const handleWalletChargeCallback = async (req, res) => {
const { Authority, Status, userId, amount, platform } = req.query
const isAndroid = platform === 'android'
const failRedirect = isAndroid
? 'modstagram://FailedPay?type=wallet'
: `${process.env.APP_SITE}/settings/wallet?charge=failed`
const successRedirect = isAndroid
? 'modstagram://SuccessPay?type=wallet'
: `${process.env.APP_SITE}/settings/wallet?charge=success`
const { Authority, Status } = req.query
let payment = null
let failRedirect = `${process.env.APP_SITE}/settings/wallet/payment/failed`
let successRedirect = `${process.env.APP_SITE}/settings/wallet/payment/success`
try {
if (Status !== 'OK') return res.redirect(failRedirect)
if (!Authority) return res.redirect(failRedirect)
payment = await PaymentModel.findOne({
authority: Authority,
type: { $in: ['wallet_charge_android', 'wallet_charge_web'] }
})
if (!payment) return res.redirect(failRedirect)
const chargeAmount = Number(payment.amount)
const isAndroid = payment.type === 'wallet_charge_android'
failRedirect = isAndroid
? `modstagram://FailedPay?type=wallet&amount=${encodeURIComponent(chargeAmount)}`
: `${process.env.APP_SITE}/settings/wallet/payment/failed?amount=${encodeURIComponent(chargeAmount)}`
successRedirect = isAndroid
? `modstagram://SuccessPay?type=wallet&amount=${encodeURIComponent(chargeAmount)}`
: `${process.env.APP_SITE}/settings/wallet/payment/success?amount=${encodeURIComponent(chargeAmount)}`
if (Status !== 'OK') {
payment.status = 'failed'
await payment.save()
return res.redirect(failRedirect)
}
// اگر callback دوباره ارسال شد، شارژ دوباره به کیف پول اضافه نشود.
if (payment.status === 'successful') {
return res.redirect(successRedirect)
}
const chargeAmount = Number(amount)
const verify = await axios.post(
'https://api.zarinpal.com/pg/v4/payment/verify.json',
{
@@ -249,41 +291,60 @@ const handleWalletChargeCallback = async (req, res) => {
authority: Authority,
amount: toRial(chargeAmount)
},
{ headers: { 'Content-Type': 'application/json' } }
{ headers: { 'Content-Type': 'application/json' }, timeout: 20000 }
)
const data = verify.data?.data
if (data?.code === 100 || data?.code === 101) {
const user = await UserModel.findById(userId)
if (!user) return res.redirect(failRedirect)
const user = await UserModel.findById(payment.user_id)
if (!user) {
payment.status = 'failed'
await payment.save()
return res.redirect(failRedirect)
}
await WalletTransactionModel.create({
user: userId,
user: payment.user_id,
bucket: 'charged',
type: 'credit',
amount: chargeAmount,
referenceType: 'payment',
referenceId: payment._id,
description: 'شارژ کیف پول'
})
const bonus = Math.round(chargeAmount * CHARGE_BONUS_RATE)
if (bonus > 0) {
await WalletTransactionModel.create({
user: userId,
user: payment.user_id,
bucket: 'gift',
type: 'credit',
amount: bonus,
referenceType: 'payment',
referenceId: payment._id,
description: 'پاداش شارژ کیف پول (۵٪)'
})
}
payment.status = 'successful'
await payment.save()
return res.redirect(successRedirect)
}
payment.status = 'failed'
await payment.save()
return res.redirect(failRedirect)
} catch (error) {
console.error('handleWalletChargeCallback error:', error?.response?.data || error.message)
console.error('handleWalletChargeCallback error:', error?.response?.data || error?.message || error)
if (payment) {
try {
payment.status = 'failed'
await payment.save()
} catch (saveError) {
console.error('Failed to update wallet payment status:', saveError?.message || saveError)
}
}
return res.redirect(failRedirect)
}
}