This commit is contained in:
payacom
2026-08-31 20:45:41 +03:30
parent fa9f23c61d
commit 3f416a7a60
367 changed files with 621895 additions and 6443 deletions

View File

@@ -0,0 +1,385 @@
/* eslint-disable camelcase */
const axios = require('axios')
const jMoment = require('moment-jalaali')
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 MERCHANT_ID = process.env.ZARINPAL_MERCHANT_ID || process.env.MERCHENT_CODE
const GIFT_MIN_AMOUNT = 2000
const GIFT_MAX_AMOUNT = 100000000
const GIFT_COMMISSION_RATE = 0.1
function toRial (tomanAmount) {
return Number(tomanAmount) * 10
}
async function validateGiftTarget (senderId, receiverId, amount) {
if (!receiverId) {
return { error: { status: 400, message: 'کاربر گیرنده مشخص نشده است' } }
}
if (String(receiverId) === String(senderId)) {
return { error: { status: 400, message: 'نمی‌توانید به خودتان هدیه بدهید' } }
}
const giftAmount = Number(amount)
if (!Number.isFinite(giftAmount) || giftAmount < GIFT_MIN_AMOUNT || giftAmount > GIFT_MAX_AMOUNT) {
return { error: { status: 422, message: 'مبلغ هدیه نامعتبر است' } }
}
const receiver = await UserModel.findById(receiverId)
if (!receiver) {
return { error: { status: 404, message: 'کاربر گیرنده یافت نشد' } }
}
if (receiver.gift_enabled === false) {
return { error: { status: 403, message: 'این کاربر دریافت هدیه را غیرفعال کرده است' } }
}
return { receiver, giftAmount }
}
const GIFT_USER_SELECT = 'profile_image user_level first_name last_name user_name is_verified verify_badge'
const initiateGiftPayment = async (req, res, next) => {
try {
const senderId = req.user._id
const { receiverId, amount, message } = req.body
if (!receiverId) {
return res.status(400).json({ error: true, message: 'کاربر گیرنده مشخص نشده است' })
}
if (String(receiverId) === String(senderId)) {
return res.status(400).json({ error: true, message: 'نمی‌توانید به خودتان هدیه بدهید' })
}
const giftAmount = Number(amount)
if (!Number.isFinite(giftAmount) || giftAmount < GIFT_MIN_AMOUNT || giftAmount > GIFT_MAX_AMOUNT) {
return res.status(422).json({ error: true, message: 'مبلغ هدیه نامعتبر است' })
}
const receiver = await UserModel.findById(receiverId)
if (!receiver) {
return res.status(404).json({ error: true, message: 'کاربر گیرنده یافت نشد' })
}
if (receiver.gift_enabled === false) {
return res.status(403).json({ error: true, 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 response = await axios.post(
'https://api.zarinpal.com/pg/v4/payment/request.json',
{
merchant_id: MERCHANT_ID,
amount: toRial(giftAmount),
description: `هدیه ویژه به ${receiver.first_name || receiver.user_name}`,
callback_url: callbackUrl
},
{ headers: { 'Content-Type': 'application/json' } }
)
const result = response.data?.data
if (result?.code === 100 && result?.authority) {
return res.json({
authority: result.authority,
paymentUrl: `https://www.zarinpal.com/pg/StartPay/${result.authority}`
})
}
console.error('ZarinPal gift initiate error:', response.data)
return res.status(502).json({ error: true, message: 'درگاه پرداخت پاسخ نامعتبر داد. لطفاً دوباره تلاش کنید.' })
} catch (error) {
next(error)
}
}
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}` : ''}`
try {
if (Status !== 'OK') return res.redirect(failRedirect)
const giftAmount = Number(amount)
const verify = await axios.post(
'https://api.zarinpal.com/pg/v4/payment/verify.json',
{
merchant_id: MERCHANT_ID,
authority: Authority,
amount: toRial(giftAmount)
},
{ headers: { 'Content-Type': 'application/json' } }
)
const data = verify.data?.data
if (data?.code !== 100 && data?.code !== 101) {
return res.redirect(failRedirect)
}
const payment = await PaymentModel.create({
amount: giftAmount,
status: 'successful',
authority: Authority,
user_id: senderId,
type: 'gift'
})
const gift = await GiftModel.create({
sender: senderId,
receiver: receiverId,
amount: giftAmount,
message: message ? decodeURIComponent(message) : null,
transaction_id: payment._id
})
await WalletTransactionModel.create({
user: receiverId,
bucket: 'gift',
type: 'credit',
amount: Math.round(giftAmount * (1 - GIFT_COMMISSION_RATE)),
referenceType: 'payment',
referenceId: gift._id,
description: 'هدیه دریافتی از کاربر دیگر (پس از کسر ۱۰٪ کارمزد)'
})
const receiverUser = await UserModel.findById(receiverId).select('user_name').lean()
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)
return res.redirect(failRedirect)
}
}
const getMyGifts = async (req, res, next) => {
try {
const userId = req.user._id
const { page = 1, limit = 10, filter } = req.query
const query = filter === 'received' ? { receiver: userId } : { sender: userId }
const options = {
page: parseInt(page, 10),
limit: parseInt(limit, 10),
sort: { createdAt: -1 },
populate: [
{ path: 'sender', select: GIFT_USER_SELECT },
{ path: 'receiver', select: GIFT_USER_SELECT }
]
}
const result = await GiftModel.paginate(query, options)
const gifts = result.docs.map((gift) => ({
...gift._doc,
createdAt: jMoment(gift.createdAt).format('jYYYY-jMM-jDD HH:mm')
}))
return res.json({
gifts,
totalPages: result.totalPages,
totalItems: result.totalDocs
})
} catch (error) {
next(error)
}
}
const replyToGift = async (req, res, next) => {
try {
const userId = req.user._id
const { giftId, reply } = req.body
if (!giftId || !reply || !reply.trim()) {
return res.status(400).json({ error: true, message: 'متن پاسخ الزامی است' })
}
const gift = await GiftModel.findById(giftId)
if (!gift || String(gift.receiver) !== String(userId)) {
return res.status(404).json({ error: true, message: 'هدیه یافت نشد' })
}
gift.reply = reply.trim()
gift.repliedAt = new Date()
await gift.save()
return res.json({ message: 'پاسخ شما ثبت شد', gift })
} catch (error) {
next(error)
}
}
const initiateChatGift = async (req, res, next) => {
try {
const senderId = req.user._id
const { receiverId, amount, message, return_path: returnPath } = 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,
referenceType: 'payment',
description: 'پرداخت هدیه از کیف پول'
})
const gift = await GiftModel.create({
sender: senderId,
receiver: receiverId,
amount: giftAmount,
message: message || null,
paymentMethod: 'wallet',
viaChatMessage: true,
opened: false,
openedAt: null
})
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 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 response = await axios.post(
'https://api.zarinpal.com/pg/v4/payment/request.json',
{
merchant_id: MERCHANT_ID,
amount: toRial(giftAmount),
description: `هدیه در چت به ${receiver.first_name || receiver.user_name}`,
callback_url: callbackUrl
},
{ headers: { 'Content-Type': 'application/json' } }
)
const result = response.data?.data
if (result?.code === 100 && result?.authority) {
return res.json({
paidFromWallet: false,
authority: result.authority,
paymentUrl: `https://www.zarinpal.com/pg/StartPay/${result.authority}`
})
}
console.error('ZarinPal chat gift initiate error:', response.data)
return res.status(502).json({ error: true, message: 'درگاه پرداخت پاسخ نامعتبر داد. لطفاً دوباره تلاش کنید.' })
} catch (error) {
next(error)
}
}
const handleChatGiftPaymentCallback = async (req, res) => {
const { Authority, Status, senderId, receiverId, amount, message, returnPath } = req.query
const safeReturnPath = typeof returnPath === 'string' && returnPath.startsWith('/') ? decodeURIComponent(returnPath) : '/'
const failRedirect = `${process.env.APP_SITE}${safeReturnPath}?giftPaymentFailed=1`
try {
if (Status !== 'OK') return res.redirect(failRedirect)
const giftAmount = Number(amount)
const verify = await axios.post(
'https://api.zarinpal.com/pg/v4/payment/verify.json',
{
merchant_id: MERCHANT_ID,
authority: Authority,
amount: toRial(giftAmount)
},
{ headers: { 'Content-Type': 'application/json' } }
)
const data = verify.data?.data
if (data?.code !== 100 && data?.code !== 101) {
return res.redirect(failRedirect)
}
const payment = await PaymentModel.create({
amount: giftAmount,
status: 'successful',
authority: Authority,
user_id: senderId,
type: 'gift'
})
const decodedMessage = message ? decodeURIComponent(message) : null
const gift = await GiftModel.create({
sender: senderId,
receiver: receiverId,
amount: giftAmount,
message: decodedMessage,
transaction_id: payment._id,
paymentMethod: 'online',
viaChatMessage: true,
opened: false,
openedAt: null
})
return res.redirect(
`${process.env.APP_SITE}${safeReturnPath}?giftId=${gift._id}&giftAmount=${giftAmount}&giftMessage=${encodeURIComponent(decodedMessage || '')}`
)
} catch (error) {
console.error('handleChatGiftPaymentCallback error:', error?.response?.data || error.message)
return res.redirect(failRedirect)
}
}
const openGift = async (req, res, next) => {
try {
const userId = req.user._id
const { id } = req.params
const gift = await GiftModel.findById(id)
if (!gift || String(gift.receiver) !== String(userId)) {
return res.status(404).json({ error: true, message: 'هدیه یافت نشد' })
}
if (gift.opened) {
return res.json({ gift, alreadyOpened: true })
}
await WalletTransactionModel.create({
user: gift.receiver,
bucket: 'gift',
type: 'credit',
amount: Math.round(gift.amount * (1 - GIFT_COMMISSION_RATE)),
referenceType: 'payment',
referenceId: gift._id,
description: 'هدیه دریافتی از کاربر دیگر (پس از کسر ۱۰٪ کارمزد)'
})
gift.opened = true
gift.openedAt = new Date()
await gift.save()
return res.json({ gift, alreadyOpened: false })
} catch (error) {
next(error)
}
}
module.exports = {
initiateGiftPayment,
handleGiftPaymentCallback,
getMyGifts,
replyToGift,
initiateChatGift,
handleChatGiftPaymentCallback,
openGift
}