This commit is contained in:
payacom
2026-09-24 21:00:07 +03:30
parent 1954b2661d
commit 7f0ddfde70
113 changed files with 570 additions and 5049 deletions

View File

@@ -5,7 +5,7 @@ const GiftModel = require('../../../models/GiftModel')
const UserModel = require('../../../models/UserModel')
const PaymentModel = require('../../../models/PaymentModel')
const WalletTransactionModel = require('../../../models/WalletTransactionModel')
const { getBucketBalance } = require('../wallet/walletController')
const { resolveWalletSplit, debitWalletForPurchase } = require('../../../utils/walletSplit')
const MERCHANT_ID = process.env.ZARINPAL_MERCHANT_ID || process.env.MERCHENT_CODE
const GIFT_MIN_AMOUNT = 2000
@@ -39,10 +39,22 @@ async function validateGiftTarget (senderId, receiverId, amount) {
const GIFT_USER_SELECT = 'profile_image user_level first_name last_name user_name is_verified verify_badge'
async function creditGiftReceiver (receiverId, giftId, giftAmount) {
await WalletTransactionModel.create({
user: receiverId,
bucket: 'gift',
type: 'credit',
amount: Math.round(giftAmount * (1 - GIFT_COMMISSION_RATE)),
referenceType: 'payment',
referenceId: giftId,
description: 'هدیه دریافتی از کاربر دیگر (پس از کسر ۱۰٪ کارمزد)'
})
}
const initiateGiftPayment = async (req, res, next) => {
try {
const senderId = req.user._id
const { receiverId, amount, message } = req.body
const { receiverId, amount, message, platform, useWallet } = req.body
if (!receiverId) {
return res.status(400).json({ error: true, message: 'کاربر گیرنده مشخص نشده است' })
@@ -64,18 +76,43 @@ const initiateGiftPayment = async (req, res, next) => {
return res.status(403).json({ error: true, message: 'این کاربر دریافت هدیه را غیرفعال کرده است' })
}
const { walletAmount, gatewayAmount, fullyCovered } = await resolveWalletSplit(senderId, giftAmount, useWallet)
if (fullyCovered) {
await debitWalletForPurchase(senderId, walletAmount, {
referenceType: 'payment',
description: 'پرداخت هدیه از کیف پول'
})
const gift = await GiftModel.create({
sender: senderId,
receiver: receiverId,
amount: giftAmount,
message: message || null,
paymentMethod: 'wallet'
})
await creditGiftReceiver(receiverId, gift._id, giftAmount)
return res.json({
paidFromWallet: true,
gift: { _id: gift._id, amount: gift.amount, message: gift.message }
})
}
const callbackBase = process.env.APP_SITE_CallBack || process.env.APP_URL
if (!callbackBase) {
return res.status(500).json({ error: true, message: 'آدرس بازگشت پرداخت تنظیم نشده است' })
}
const callbackUrl = `${callbackBase}/gifts/payment-callback?senderId=${senderId}&receiverId=${receiverId}&amount=${giftAmount}&message=${encodeURIComponent(message || '')}`
const platformSuffix = platform ? `&platform=${encodeURIComponent(platform)}` : ''
const callbackUrl = `${callbackBase}/gifts/payment-callback?senderId=${senderId}&receiverId=${receiverId}&amount=${giftAmount}&walletAmount=${walletAmount}&message=${encodeURIComponent(message || '')}${platformSuffix}`
const response = await axios.post(
'https://api.zarinpal.com/pg/v4/payment/request.json',
{
merchant_id: MERCHANT_ID,
amount: toRial(giftAmount),
amount: toRial(gatewayAmount),
description: `هدیه ویژه به ${receiver.first_name || receiver.user_name}`,
callback_url: callbackUrl
},
@@ -98,19 +135,25 @@ const initiateGiftPayment = async (req, res, next) => {
}
const handleGiftPaymentCallback = async (req, res) => {
const { Authority, Status, senderId, receiverId, amount, message } = req.query
const failRedirect = `${process.env.APP_SITE}/gift/payment/failed${receiverId ? `?receiverId=${receiverId}` : ''}`
const { Authority, Status, senderId, receiverId, amount, walletAmount, message, platform } = req.query
const isAndroid = platform === 'android'
const failRedirect = isAndroid
? `modstagram://FailedPay?type=gift${receiverId ? `&receiverId=${receiverId}` : ''}`
: `${process.env.APP_SITE}/gift/payment/failed${receiverId ? `?receiverId=${receiverId}` : ''}`
try {
if (Status !== 'OK') return res.redirect(failRedirect)
const giftAmount = Number(amount)
const usedWalletAmount = Number(walletAmount) || 0
const gatewayAmount = giftAmount - usedWalletAmount
const verify = await axios.post(
'https://api.zarinpal.com/pg/v4/payment/verify.json',
{
merchant_id: MERCHANT_ID,
authority: Authority,
amount: toRial(giftAmount)
amount: toRial(gatewayAmount)
},
{ headers: { 'Content-Type': 'application/json' } }
)
@@ -120,8 +163,15 @@ const handleGiftPaymentCallback = async (req, res) => {
return res.redirect(failRedirect)
}
// پرداخت درگاه قطعاً موفق شد — همین حالا سهمِ کیف پول هم کسر می‌شه (نگاه کن به
// utils/walletSplit.js)
await debitWalletForPurchase(senderId, usedWalletAmount, {
referenceType: 'payment',
description: 'پرداخت هدیه از کیف پول'
})
const payment = await PaymentModel.create({
amount: giftAmount,
amount: gatewayAmount,
status: 'successful',
authority: Authority,
user_id: senderId,
@@ -133,22 +183,20 @@ const handleGiftPaymentCallback = async (req, res) => {
receiver: receiverId,
amount: giftAmount,
message: message ? decodeURIComponent(message) : null,
transaction_id: payment._id
transaction_id: payment._id,
paymentMethod: usedWalletAmount > 0 ? 'mixed' : 'online'
})
await WalletTransactionModel.create({
user: receiverId,
bucket: 'gift',
type: 'credit',
amount: Math.round(giftAmount * (1 - GIFT_COMMISSION_RATE)),
referenceType: 'payment',
referenceId: gift._id,
description: 'هدیه دریافتی از کاربر دیگر (پس از کسر ۱۰٪ کارمزد)'
})
await creditGiftReceiver(receiverId, gift._id, giftAmount)
const receiverUser = await UserModel.findById(receiverId).select('user_name').lean()
const usernameParam = receiverUser?.user_name ? `&receiverUsername=${encodeURIComponent(receiverUser.user_name)}` : ''
if (isAndroid) {
const usernameParam = receiverUser?.user_name ? `&username=${encodeURIComponent(receiverUser.user_name)}` : ''
return res.redirect(`modstagram://SuccessPay?type=gift&giftId=${gift._id}&receiverId=${receiverId}${usernameParam}`)
}
const usernameParam = receiverUser?.user_name ? `&receiverUsername=${encodeURIComponent(receiverUser.user_name)}` : ''
return res.redirect(`${process.env.APP_SITE}/gift/payment/success?giftId=${gift._id}&receiverId=${receiverId}${usernameParam}`)
} catch (error) {
console.error('handleGiftPaymentCallback error:', error?.response?.data || error.message)
@@ -217,18 +265,15 @@ const replyToGift = async (req, res, next) => {
const initiateChatGift = async (req, res, next) => {
try {
const senderId = req.user._id
const { receiverId, amount, message, return_path: returnPath } = req.body
const { receiverId, amount, message, return_path: returnPath, platform, useWallet } = req.body
const { error, receiver, giftAmount } = await validateGiftTarget(senderId, receiverId, amount)
if (error) return res.status(error.status).json({ error: true, message: error.message })
const chargedBalance = await getBucketBalance(senderId, 'charged')
if (chargedBalance >= giftAmount) {
await WalletTransactionModel.create({
user: senderId,
bucket: 'charged',
type: 'debit',
amount: giftAmount,
const { walletAmount, gatewayAmount, fullyCovered } = await resolveWalletSplit(senderId, giftAmount, useWallet)
if (fullyCovered) {
await debitWalletForPurchase(senderId, walletAmount, {
referenceType: 'payment',
description: 'پرداخت هدیه از کیف پول'
})
@@ -256,13 +301,14 @@ const initiateChatGift = async (req, res, next) => {
}
const safeReturnPath = typeof returnPath === 'string' && returnPath.startsWith('/') ? returnPath : '/'
const callbackUrl = `${callbackBase}/gifts/chat/payment-callback?senderId=${senderId}&receiverId=${receiverId}&amount=${giftAmount}&message=${encodeURIComponent(message || '')}&returnPath=${encodeURIComponent(safeReturnPath)}`
const platformSuffix = platform ? `&platform=${encodeURIComponent(platform)}` : ''
const callbackUrl = `${callbackBase}/gifts/chat/payment-callback?senderId=${senderId}&receiverId=${receiverId}&amount=${giftAmount}&walletAmount=${walletAmount}&message=${encodeURIComponent(message || '')}&returnPath=${encodeURIComponent(safeReturnPath)}${platformSuffix}`
const response = await axios.post(
'https://api.zarinpal.com/pg/v4/payment/request.json',
{
merchant_id: MERCHANT_ID,
amount: toRial(giftAmount),
amount: toRial(gatewayAmount),
description: `هدیه در چت به ${receiver.first_name || receiver.user_name}`,
callback_url: callbackUrl
},
@@ -286,20 +332,26 @@ const initiateChatGift = async (req, res, next) => {
}
const handleChatGiftPaymentCallback = async (req, res) => {
const { Authority, Status, senderId, receiverId, amount, message, returnPath } = req.query
const { Authority, Status, senderId, receiverId, amount, walletAmount, message, returnPath, platform } = req.query
const isAndroid = platform === 'android'
const safeReturnPath = typeof returnPath === 'string' && returnPath.startsWith('/') ? decodeURIComponent(returnPath) : '/'
const failRedirect = `${process.env.APP_SITE}${safeReturnPath}?giftPaymentFailed=1`
const failRedirect = isAndroid
? `modstagram://FailedPay?type=gift${receiverId ? `&receiverId=${receiverId}` : ''}`
: `${process.env.APP_SITE}${safeReturnPath}?giftPaymentFailed=1`
try {
if (Status !== 'OK') return res.redirect(failRedirect)
const giftAmount = Number(amount)
const usedWalletAmount = Number(walletAmount) || 0
const gatewayAmount = giftAmount - usedWalletAmount
const verify = await axios.post(
'https://api.zarinpal.com/pg/v4/payment/verify.json',
{
merchant_id: MERCHANT_ID,
authority: Authority,
amount: toRial(giftAmount)
amount: toRial(gatewayAmount)
},
{ headers: { 'Content-Type': 'application/json' } }
)
@@ -309,8 +361,13 @@ const handleChatGiftPaymentCallback = async (req, res) => {
return res.redirect(failRedirect)
}
await debitWalletForPurchase(senderId, usedWalletAmount, {
referenceType: 'payment',
description: 'پرداخت هدیه از کیف پول'
})
const payment = await PaymentModel.create({
amount: giftAmount,
amount: gatewayAmount,
status: 'successful',
authority: Authority,
user_id: senderId,
@@ -325,12 +382,20 @@ const handleChatGiftPaymentCallback = async (req, res) => {
amount: giftAmount,
message: decodedMessage,
transaction_id: payment._id,
paymentMethod: 'online',
paymentMethod: usedWalletAmount > 0 ? 'mixed' : 'online',
viaChatMessage: true,
opened: false,
openedAt: null
})
if (isAndroid) {
const receiverUser = await UserModel.findById(receiverId).select('user_name').lean()
const usernameParam = receiverUser?.user_name ? `&username=${encodeURIComponent(receiverUser.user_name)}` : ''
return res.redirect(
`modstagram://SuccessPay?type=gift&giftId=${gift._id}&receiverId=${receiverId}${usernameParam}`
)
}
return res.redirect(
`${process.env.APP_SITE}${safeReturnPath}?giftId=${gift._id}&giftAmount=${giftAmount}&giftMessage=${encodeURIComponent(decodedMessage || '')}`
)