full
This commit is contained in:
385
controllers/application/gifts/giftController.js
Normal file
385
controllers/application/gifts/giftController.js
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user