full app
This commit is contained in:
@@ -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 || '')}`
|
||||
)
|
||||
|
||||
@@ -14,6 +14,7 @@ const {
|
||||
consumeFreeCreation,
|
||||
freeCreationLimitResponse
|
||||
} = require('../../../utils/freeCreationLimit')
|
||||
const { canUseFreeBillboard, consumeFreeBillboard } = require('../../../utils/billboardFreeLimit')
|
||||
const {
|
||||
PROJECT_ACCEPT_RATE,
|
||||
PROJECT_COMPLETION_COMMISSION_RATE,
|
||||
@@ -646,24 +647,20 @@ const initiateAdvertisingPayment = async (req, res) => {
|
||||
const user = await UserModel.findById(userId)
|
||||
|
||||
if (item.name === 'free' && item.price === 0) {
|
||||
if (user.daily_free_request > 0 && canUseFreeCreation(user)) {
|
||||
const consumed = await consumeFreeCreation(user)
|
||||
if (!consumed.ok) {
|
||||
return res.status(403).json(freeCreationLimitResponse())
|
||||
}
|
||||
user.daily_free_request -= 1
|
||||
if (canUseFreeBillboard(user)) {
|
||||
await consumeFreeBillboard(user)
|
||||
await user.save()
|
||||
const project = await ProjectModel.findById(advertisingId)
|
||||
const project = await AdvertisingModel.findById(advertisingId)
|
||||
if (!project) {
|
||||
res.status(404).send('Project not found')
|
||||
res.status(404).send('Advertising not found')
|
||||
return
|
||||
}
|
||||
project.payment_status = 'done' // Set project status to 'done'
|
||||
project.status = await resolveApprovalStatus(userId, 'temp_accept') // Set project status to 'done'
|
||||
await project.save()
|
||||
res.status(200).json({ message: 'پروژه رایگان با موفقیت ایجاد شد', advertisingId, type: 'free' })
|
||||
res.status(200).json({ message: 'بیلبورد رایگان با موفقیت ایجاد شد', advertisingId, type: 'free' })
|
||||
} else {
|
||||
res.status(403).json({ message: 'شما قبلاً از درخواست رایگان امروز استفاده کردهاید.' })
|
||||
res.status(403).json({ message: 'سقف ۲۰۰ بیلبورد رایگان شما به پایان رسیده است.' })
|
||||
}
|
||||
} else {
|
||||
const response = await axios.post(
|
||||
@@ -802,24 +799,20 @@ const republishAdvertisingPayment = async (req, res) => {
|
||||
const user = await UserModel.findById(userId)
|
||||
|
||||
if (item.name === 'free' && item.price === 0) {
|
||||
if (user.daily_free_request > 0 && canUseFreeCreation(user)) {
|
||||
const consumed = await consumeFreeCreation(user)
|
||||
if (!consumed.ok) {
|
||||
return res.status(403).json(freeCreationLimitResponse())
|
||||
}
|
||||
user.daily_free_request -= 1
|
||||
if (canUseFreeBillboard(user)) {
|
||||
await consumeFreeBillboard(user)
|
||||
await user.save()
|
||||
const project = await ProjectModel.findById(advertisingId)
|
||||
const project = await AdvertisingModel.findById(advertisingId)
|
||||
if (!project) {
|
||||
res.status(404).send('Project not found')
|
||||
res.status(404).send('Advertising not found')
|
||||
return
|
||||
}
|
||||
project.payment_status = 'done' // Set project status to 'done'
|
||||
project.status = await resolveApprovalStatus(userId, 'temp_accept') // Set project status to 'done'
|
||||
await project.save()
|
||||
res.status(200).json({ message: 'پروژه رایگان با موفقیت ایجاد شد', advertisingId, type: 'free' })
|
||||
res.status(200).json({ message: 'بیلبورد رایگان با موفقیت ایجاد شد', advertisingId, type: 'free' })
|
||||
} else {
|
||||
res.status(403).json({ message: 'شما قبلاً از درخواست رایگان امروز استفاده کردهاید.' })
|
||||
res.status(403).json({ message: 'سقف ۲۰۰ بیلبورد رایگان شما به پایان رسیده است.' })
|
||||
}
|
||||
} else {
|
||||
const response = await axios.post(
|
||||
|
||||
@@ -9,14 +9,16 @@ const NotificationModel = require('../../../models/NotificationModel')
|
||||
const UserModel = require('../../../models/UserModel')
|
||||
const AdvertisingTypeModel = require('../../../models/AdvertisingTypeModel')
|
||||
const AdvertisingModel = require('../../../models/AdvertisingModel')
|
||||
const { canUseFreeBillboard, consumeFreeBillboard } = require('../../../utils/billboardFreeLimit')
|
||||
const OfferTypeModel = require('../../../models/OfferTypeModel')
|
||||
const OfferModel = require('../../../models/OfferModel')
|
||||
const { default: axios } = require('axios')
|
||||
const WalletTransactionModel = require('../../../models/WalletTransactionModel')
|
||||
const { PROJECT_ACCEPT_RATE, PROJECT_COMPLETION_COMMISSION_RATE, creditProjectShare } = require('../../../utils/projectCommission')
|
||||
const { isVerifiedBadge } = require('../../../utils/verifyBadge')
|
||||
const { resolveWalletSplit, debitWalletForPurchase } = require('../../../utils/walletSplit')
|
||||
|
||||
const OFFER_COMMISSION_RATE = 0.3
|
||||
const OFFER_COMMISSION_RATE = 0.5
|
||||
|
||||
const apiKey =
|
||||
process.env.ZARINPAL_MERCHANT_ID ||
|
||||
@@ -285,6 +287,7 @@ const paymentRequestAcceptWeb = async (req, res) => {
|
||||
}
|
||||
|
||||
const requestId = req.body.requestId
|
||||
const platform = req.body.platform
|
||||
const request = await RequestModel.findById(requestId)
|
||||
if (!request) {
|
||||
return res.status(404).json({ error: 'Request not found' })
|
||||
@@ -300,13 +303,14 @@ const paymentRequestAcceptWeb = async (req, res) => {
|
||||
return res.status(500).json({ error: true, message: 'آدرس بازگشت پرداخت تنظیم نشده است' })
|
||||
}
|
||||
|
||||
const platformSuffix = platform ? `&platform=${encodeURIComponent(platform)}` : ''
|
||||
const response = await axios.post(
|
||||
'https://api.zarinpal.com/pg/v4/payment/request.json',
|
||||
{
|
||||
merchant_id: apiKey,
|
||||
amount: toRial(acceptAmount),
|
||||
description: `پرداخت پذیرش همکاری برای پروژه ${project.title || projectId}`,
|
||||
callback_url: `${callbackBase}/projects/payment-request-step-one-callback/web?projectId=${projectId}&requestId=${requestId}&userId=${userId}`
|
||||
callback_url: `${callbackBase}/projects/payment-request-step-one-callback/web?projectId=${projectId}&requestId=${requestId}&userId=${userId}${platformSuffix}`
|
||||
},
|
||||
{ headers: { 'Content-Type': 'application/json' }, timeout: 20000 }
|
||||
)
|
||||
@@ -328,8 +332,14 @@ const paymentRequestAcceptWeb = async (req, res) => {
|
||||
}
|
||||
|
||||
const paymentRequestAcceptWebCallback = async (req, res) => {
|
||||
const { Authority, Status, projectId, requestId, userId } = req.query
|
||||
const failRedirect = `${process.env.APP_SITE}/projects/payment/failed?projectId=${projectId || ''}`
|
||||
const { Authority, Status, projectId, requestId, userId, platform } = req.query
|
||||
const isAndroid = platform === 'android'
|
||||
const failRedirect = isAndroid
|
||||
? `modstagram://FailedPay?type=steps&projectId=${projectId || ''}`
|
||||
: `${process.env.APP_SITE}/projects/payment/failed?projectId=${projectId || ''}`
|
||||
const successRedirect = isAndroid
|
||||
? `modstagram://SuccessPay?type=steps&projectId=${projectId || ''}&requestId=${requestId || ''}`
|
||||
: `${process.env.APP_SITE}/projects/payment/success?projectId=${projectId}&type=accept`
|
||||
|
||||
try {
|
||||
if (Status !== 'OK') return res.redirect(failRedirect)
|
||||
@@ -339,7 +349,7 @@ const paymentRequestAcceptWebCallback = async (req, res) => {
|
||||
if (!request || !project) return res.redirect(failRedirect)
|
||||
|
||||
if (project.status === 'ongoing') {
|
||||
return res.redirect(`${process.env.APP_SITE}/projects/payment/success?projectId=${projectId}&type=accept`)
|
||||
return res.redirect(successRedirect)
|
||||
}
|
||||
|
||||
const acceptAmount = Math.round(Number(request.price) * PROJECT_ACCEPT_RATE)
|
||||
@@ -402,7 +412,11 @@ const paymentRequestAcceptWebCallback = async (req, res) => {
|
||||
})
|
||||
await notification.save()
|
||||
|
||||
return res.redirect(`${process.env.APP_SITE}/projects/payment/success?projectId=${projectId}&type=accept`)
|
||||
return res.redirect(
|
||||
isAndroid
|
||||
? `modstagram://SuccessPay?type=steps&projectId=${projectId}&requestId=${requestId}&price=${acceptAmount}`
|
||||
: successRedirect
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('paymentRequestAcceptWebCallback error:', error?.response?.data || error?.message || error)
|
||||
return res.redirect(failRedirect)
|
||||
@@ -557,8 +571,8 @@ const initiateAdvertisingPaymentWeb = async (req, res) => {
|
||||
const user = await UserModel.findById(userId)
|
||||
|
||||
if (item.name === 'free' && item.price === 0) {
|
||||
if (user.daily_free_request > 0) {
|
||||
user.daily_free_request -= 1
|
||||
if (canUseFreeBillboard(user)) {
|
||||
await consumeFreeBillboard(user)
|
||||
await user.save()
|
||||
const project = await AdvertisingModel.findById(advertisingId)
|
||||
if (!project) {
|
||||
@@ -570,7 +584,7 @@ const initiateAdvertisingPaymentWeb = async (req, res) => {
|
||||
await project.save()
|
||||
res.status(200).json({ message: 'بیلبورد رایگان با موفقیت ایجاد شد', advertisingId, type: 'free' })
|
||||
} else {
|
||||
res.status(403).json({ message: 'شما قبلاً از بیلبورد رایگان این ماه استفاده کردهاید.' })
|
||||
res.status(403).json({ message: 'سقف ۲۰۰ بیلبورد رایگان شما به پایان رسیده است.' })
|
||||
}
|
||||
} else {
|
||||
const response = await axios.post(
|
||||
@@ -711,20 +725,20 @@ const republishAdvertisingPaymentWeb = async (req, res) => {
|
||||
const user = await UserModel.findById(userId)
|
||||
|
||||
if (item.name === 'free' && item.price === 0) {
|
||||
if (user.daily_free_request > 0) {
|
||||
user.daily_free_request -= 1
|
||||
if (canUseFreeBillboard(user)) {
|
||||
await consumeFreeBillboard(user)
|
||||
await user.save()
|
||||
const project = await AdvertisingModel.findById(advertisingId)
|
||||
if (!project) {
|
||||
res.status(404).send('Project not found')
|
||||
res.status(404).send('Advertising not found')
|
||||
return
|
||||
}
|
||||
project.payment_status = 'done' // Set project status to 'done'
|
||||
project.status = await resolveApprovalStatus(userId, 'temp_accept') // Set project status to 'done'
|
||||
await project.save()
|
||||
res.status(200).json({ message: 'پروژه رایگان با موفقیت ایجاد شد', advertisingId, type: 'free' })
|
||||
res.status(200).json({ message: 'بیلبورد رایگان با موفقیت ایجاد شد', advertisingId, type: 'free' })
|
||||
} else {
|
||||
res.status(403).json({ message: 'شما قبلاً از درخواست رایگان امروز استفاده کردهاید.' })
|
||||
res.status(403).json({ message: 'سقف ۲۰۰ بیلبورد رایگان شما به پایان رسیده است.' })
|
||||
}
|
||||
} else {
|
||||
const response = await axios.post(
|
||||
@@ -853,14 +867,14 @@ const initiateOfferPaymentWeb = async (req, res) => {
|
||||
const decoded = jwt.verify(token, process.env.APP_SECRET)
|
||||
const userId = decoded.id
|
||||
|
||||
const { receiverId, offerType, platform } = req.body
|
||||
const { receiverId, offerType, platform, useWallet } = req.body
|
||||
if (!receiverId || !offerType) { return res.status(400).json({ error: 'Invalid data' }) }
|
||||
|
||||
const user = await UserModel.findById(userId)
|
||||
const receiver = await UserModel.findById(receiverId)
|
||||
if (!user || !receiver) { return res.status(404).json({ error: 'User not found' }) }
|
||||
|
||||
const price = await getOfferPriceFromDatabase(offerType) // ✅ ریال
|
||||
const price = await getOfferPriceFromDatabase(offerType) // تومان
|
||||
|
||||
// 🎁 free offer
|
||||
if (offerType === 'free' && user.monthly_free_offer > 0) {
|
||||
@@ -894,9 +908,39 @@ const initiateOfferPaymentWeb = async (req, res) => {
|
||||
})
|
||||
}
|
||||
|
||||
const amount = Number(price) * 10 // ✅ ریال – بدون ×10
|
||||
const { walletAmount, gatewayAmount, fullyCovered } = await resolveWalletSplit(userId, price, useWallet)
|
||||
|
||||
// کاملاً از کیف پول پوشش داده شد — اصلاً نیازی به درگاه نیست
|
||||
if (fullyCovered) {
|
||||
const payment = await PaymentModel.create({
|
||||
amount: price,
|
||||
status: 'successful',
|
||||
authority: null,
|
||||
user_id: userId,
|
||||
type: 'offer'
|
||||
})
|
||||
|
||||
const receiverShare = Math.round(price * OFFER_COMMISSION_RATE)
|
||||
const offer = await OfferModel.create({
|
||||
sender: userId,
|
||||
receiver: receiverId,
|
||||
transaction_id: payment._id,
|
||||
pendingCommissionAmount: receiverShare,
|
||||
walletAmountUsed: walletAmount
|
||||
})
|
||||
|
||||
await debitWalletForPurchase(userId, walletAmount, {
|
||||
referenceType: 'offer',
|
||||
referenceId: offer._id,
|
||||
description: 'پرداخت آفر از کیف پول'
|
||||
})
|
||||
|
||||
return res.json({ paidFromWallet: true, offerId: offer._id })
|
||||
}
|
||||
|
||||
const amount = toRial(gatewayAmount)
|
||||
const platformSuffix = platform ? `&platform=${encodeURIComponent(platform)}` : ''
|
||||
const callbackUrl = `https://api.modstagram.com/api/v1/offers/payment-web?receiverId=${receiverId}&offerType=${offerType}&userId=${userId}${platformSuffix}`
|
||||
const callbackUrl = `https://api.modstagram.com/api/v1/offers/payment-web?receiverId=${receiverId}&offerType=${offerType}&userId=${userId}&walletAmount=${walletAmount}${platformSuffix}`
|
||||
|
||||
const zarinpalRes = await axios.post(
|
||||
'https://api.zarinpal.com/pg/v4/payment/request.json',
|
||||
@@ -942,16 +986,21 @@ const handleOfferPaymentCallback = async (req, res) => {
|
||||
const isAndroid = req.query.platform === 'android'
|
||||
|
||||
try {
|
||||
const { Authority, Status, receiverId, offerType, userId } = req.query
|
||||
const { Authority, Status, receiverId, offerType, userId, walletAmount } = req.query
|
||||
const receiverUserForFail = receiverId ? await UserModel.findById(receiverId).select('user_name') : null
|
||||
const failUsername = receiverUserForFail?.user_name || ''
|
||||
const failParams = `receiverId=${receiverId || ''}&offerType=${offerType || ''}&username=${encodeURIComponent(failUsername)}`
|
||||
|
||||
if (Status !== 'OK') {
|
||||
return isAndroid
|
||||
? res.redirect('modstagram://FailedPay?type=offer')
|
||||
: res.redirect(`${process.env.APP_SITE}/offer/payment/failed`)
|
||||
? res.redirect(`modstagram://FailedPay?type=offer&${failParams}`)
|
||||
: res.redirect(`${process.env.APP_SITE}/offer/payment/failed?userId=${receiverId || ''}&offerType=${offerType || ''}`)
|
||||
}
|
||||
|
||||
const price = await getOfferPriceFromDatabase(offerType) // ریال
|
||||
const amount = Number(price) * 10
|
||||
const offerPriceToman = await getOfferPriceFromDatabase(offerType) // تومان
|
||||
const usedWalletAmount = Number(walletAmount) || 0
|
||||
const gatewayAmountToman = offerPriceToman - usedWalletAmount
|
||||
const amount = toRial(gatewayAmountToman)
|
||||
|
||||
const verify = await axios.post(
|
||||
'https://api.zarinpal.com/pg/v4/payment/verify.json',
|
||||
@@ -967,7 +1016,7 @@ const handleOfferPaymentCallback = async (req, res) => {
|
||||
console.log('🔔payment:', data.code)
|
||||
if (data.code === 100) {
|
||||
const payment = await PaymentModel.create({
|
||||
amount: (amount / 10),
|
||||
amount: gatewayAmountToman,
|
||||
status: 'successful',
|
||||
authority: Authority,
|
||||
ref_id: data.ref_id,
|
||||
@@ -977,7 +1026,6 @@ const handleOfferPaymentCallback = async (req, res) => {
|
||||
|
||||
console.log('🔔payment:', payment)
|
||||
|
||||
const offerPriceToman = amount / 10
|
||||
const receiverShare = Math.round(offerPriceToman * OFFER_COMMISSION_RATE)
|
||||
|
||||
// سهم گیرنده فقط با پاسخگویی ظرف ۱۲ ساعت به اولین پیام دایرکت واریز میشود
|
||||
@@ -986,7 +1034,16 @@ const handleOfferPaymentCallback = async (req, res) => {
|
||||
sender: userId,
|
||||
receiver: receiverId,
|
||||
transaction_id: payment._id,
|
||||
pendingCommissionAmount: receiverShare
|
||||
pendingCommissionAmount: receiverShare,
|
||||
walletAmountUsed: usedWalletAmount
|
||||
})
|
||||
|
||||
// درگاه فقط بخشِ باقیمونده رو تایید کرد؛ حالا که پرداخت قطعاً موفق شد، سهمِ کیف
|
||||
// پول هم واقعاً کسر میشه (نگاه کن به utils/walletSplit.js)
|
||||
await debitWalletForPurchase(userId, usedWalletAmount, {
|
||||
referenceType: 'offer',
|
||||
referenceId: offer._id,
|
||||
description: 'پرداخت آفر از کیف پول'
|
||||
})
|
||||
|
||||
// صفحه/دیپلینکِ موفقیت برای دکمهی «ارسال پیام» به شناسه (و نامکاربریِ) گیرنده
|
||||
@@ -1005,13 +1062,14 @@ const handleOfferPaymentCallback = async (req, res) => {
|
||||
}
|
||||
|
||||
return isAndroid
|
||||
? res.redirect('modstagram://FailedPay?type=offer')
|
||||
: res.redirect(`${process.env.APP_SITE}/offer/payment/failed`)
|
||||
? res.redirect(`modstagram://FailedPay?type=offer&${failParams}`)
|
||||
: res.redirect(`${process.env.APP_SITE}/offer/payment/failed?userId=${receiverId || ''}&offerType=${offerType || ''}`)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
const { receiverId, offerType } = req.query
|
||||
return isAndroid
|
||||
? res.redirect('modstagram://FailedPay?type=offer')
|
||||
: res.redirect(`${process.env.APP_SITE}/offer/payment/failed`)
|
||||
? res.redirect(`modstagram://FailedPay?type=offer&receiverId=${receiverId || ''}&offerType=${offerType || ''}`)
|
||||
: res.redirect(`${process.env.APP_SITE}/offer/payment/failed?userId=${receiverId || ''}&offerType=${offerType || ''}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ const OrderModel = require('../../../models/OrderModel')
|
||||
const PaymentModel = require('../../../models/PaymentModel')
|
||||
const ShopModel = require('../../../models/ShopModel')
|
||||
const { notifyUser } = require('../../../utils/notifyUser')
|
||||
const { resolveWalletSplit, debitWalletForPurchase } = require('../../../utils/walletSplit')
|
||||
|
||||
const MERCHANT_ID = process.env.ZARINPAL_MERCHANT_ID || process.env.MERCHENT_CODE
|
||||
|
||||
@@ -13,7 +14,7 @@ function toRial (tomanAmount) {
|
||||
|
||||
const initiateOrderPayment = async (req, res) => {
|
||||
try {
|
||||
const { orderId } = req.body
|
||||
const { orderId, useWallet } = req.body
|
||||
if (!orderId) {
|
||||
return res.status(422).json({ error: true, message: 'سفارش مشخص نشده است' })
|
||||
}
|
||||
@@ -29,6 +30,42 @@ const initiateOrderPayment = async (req, res) => {
|
||||
return res.status(400).json({ error: true, message: 'این سفارش قابل پرداخت نیست' })
|
||||
}
|
||||
|
||||
const { walletAmount, gatewayAmount, fullyCovered } = await resolveWalletSplit(
|
||||
req.user._id,
|
||||
order.total_amount,
|
||||
useWallet
|
||||
)
|
||||
|
||||
// کاملاً از کیف پول پوشش داده شد — اصلاً نیازی به درگاه نیست
|
||||
if (fullyCovered) {
|
||||
await debitWalletForPurchase(req.user._id, walletAmount, {
|
||||
referenceType: 'order',
|
||||
referenceId: order._id,
|
||||
description: `پرداخت سفارش شماره ${order._id} از کیف پول`
|
||||
})
|
||||
order.walletAmountUsed = walletAmount
|
||||
order.status = 'processing'
|
||||
await order.save()
|
||||
await PaymentModel.create({
|
||||
amount: order.total_amount,
|
||||
status: 'successful',
|
||||
authority: null,
|
||||
user_id: order.buyer,
|
||||
order_id: order._id,
|
||||
type: 'shop_order'
|
||||
})
|
||||
const shop = await ShopModel.findById(order.shop)
|
||||
if (shop) {
|
||||
await notifyUser(req.app.get('io'), shop.owner, {
|
||||
project_post_id: order._id,
|
||||
type: 'shop_order',
|
||||
title: 'سفارش جدید',
|
||||
description: `یک سفارش جدید برای فروشگاه ${shop.name} ثبت و پرداخت شد.`
|
||||
})
|
||||
}
|
||||
return res.json({ paidFromWallet: true })
|
||||
}
|
||||
|
||||
const callbackBase = process.env.APP_SITE_CallBack || process.env.APP_URL
|
||||
if (!callbackBase) {
|
||||
return res.status(500).json({ error: true, message: 'آدرس بازگشت پرداخت تنظیم نشده است' })
|
||||
@@ -38,7 +75,7 @@ const initiateOrderPayment = async (req, res) => {
|
||||
'https://api.zarinpal.com/pg/v4/payment/request.json',
|
||||
{
|
||||
merchant_id: MERCHANT_ID,
|
||||
amount: toRial(order.total_amount),
|
||||
amount: toRial(gatewayAmount),
|
||||
description: `پرداخت سفارش شماره ${order._id}`,
|
||||
callback_url: `${callbackBase}/payment/shop-order/callback?orderId=${order._id}`
|
||||
},
|
||||
@@ -48,6 +85,9 @@ const initiateOrderPayment = async (req, res) => {
|
||||
const result = response.data?.data
|
||||
if (result?.code === 100 && result?.authority) {
|
||||
order.paymentAuthority = result.authority
|
||||
// فقط «قصدِ» استفاده از این مقدار کیف پول رو یادداشت میکنه — کسرِ واقعی توی
|
||||
// کالبکِ موفقیت انجام میشه (نگاه کن به بالای فایل: utils/walletSplit.js)
|
||||
order.walletAmountUsed = walletAmount
|
||||
await order.save()
|
||||
return res.json({ authority: result.authority })
|
||||
}
|
||||
@@ -77,8 +117,15 @@ const handleOrderPaymentCallback = async (req, res) => {
|
||||
return res.redirect(failRedirect)
|
||||
}
|
||||
|
||||
// این مقدار توی initiateOrderPayment فقط «یادداشت» شده بود (نگاه کن به utils/
|
||||
// walletSplit.js) — کیف پول تا همین لحظه دستنخورده مونده، پس اگه پرداختِ درگاه
|
||||
// ناموفق/رهاشده باشه، چیزی برای برگردوندن وجود نداره
|
||||
const walletAmount = order.walletAmountUsed || 0
|
||||
const gatewayAmount = order.total_amount - walletAmount
|
||||
|
||||
if (Status !== 'OK') {
|
||||
order.status = 'failed'
|
||||
order.walletAmountUsed = 0
|
||||
await order.save()
|
||||
await PaymentModel.create({
|
||||
amount: order.total_amount,
|
||||
@@ -102,15 +149,23 @@ const handleOrderPaymentCallback = async (req, res) => {
|
||||
{
|
||||
merchant_id: MERCHANT_ID,
|
||||
authority: Authority,
|
||||
amount: toRial(order.total_amount)
|
||||
amount: toRial(gatewayAmount)
|
||||
},
|
||||
{ headers: { 'Content-Type': 'application/json' } }
|
||||
)
|
||||
|
||||
const data = verify.data?.data
|
||||
if (data?.code === 100 || data?.code === 101) {
|
||||
// درگاه فقط بخشِ باقیمونده رو تایید کرد؛ حالا که پرداخت قطعاً موفق شد، سهمِ
|
||||
// کیف پول هم واقعاً کسر میشه
|
||||
await debitWalletForPurchase(order.buyer, walletAmount, {
|
||||
referenceType: 'order',
|
||||
referenceId: order._id,
|
||||
description: `پرداخت سفارش شماره ${order._id} از کیف پول`
|
||||
})
|
||||
|
||||
await PaymentModel.create({
|
||||
amount: order.total_amount,
|
||||
amount: gatewayAmount,
|
||||
status: 'successful',
|
||||
authority: Authority,
|
||||
user_id: order.buyer,
|
||||
@@ -135,6 +190,7 @@ const handleOrderPaymentCallback = async (req, res) => {
|
||||
}
|
||||
|
||||
order.status = 'failed'
|
||||
order.walletAmountUsed = 0
|
||||
await order.save()
|
||||
await PaymentModel.create({
|
||||
amount: order.total_amount,
|
||||
|
||||
@@ -72,9 +72,25 @@ const registerUser = async (req, res, next) => {
|
||||
const otp = generateOTP()
|
||||
const otpSentAt = new Date()
|
||||
|
||||
const setFields = { mobile, otp: String(otp), otpSentAt }
|
||||
|
||||
// سیستم معرفی به دوستان: کدِ معرفی فقط همین یکبار (اولین باری که این شماره اصلاً
|
||||
// ثبتنام میشه) ثبت میشه — اگه existingUser از قبل referred_by داشته باشه (مثلاً
|
||||
// کاربر OTP رو دوباره درخواست کرده)، دستنخورده میمونه؛ پاداشِ واقعی فقط با
|
||||
// تکمیلِ ثبتنام (setUserType) پرداخت میشه، نه همینجا
|
||||
const referralCode = typeof req.body.referral_code === 'string'
|
||||
? req.body.referral_code.trim()
|
||||
: null
|
||||
if (referralCode && !existingUser?.referred_by) {
|
||||
const referrer = await UserModel.findOne({ referral_code: referralCode }).select('_id')
|
||||
if (referrer && String(referrer._id) !== String(existingUser?._id)) {
|
||||
setFields.referred_by = referrer._id
|
||||
}
|
||||
}
|
||||
|
||||
await UserModel.findOneAndUpdate(
|
||||
{ mobile },
|
||||
{ $set: { mobile, otp: String(otp), otpSentAt } },
|
||||
{ $set: setFields },
|
||||
{ upsert: true, new: true, setDefaultsOnInsert: true }
|
||||
)
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
/* eslint-disable camelcase */
|
||||
const UserModel = require('../../../models/UserModel')
|
||||
const WalletTransactionModel = require('../../../models/WalletTransactionModel')
|
||||
const jwt = require('jsonwebtoken')
|
||||
|
||||
const REFERRAL_REWARD_AMOUNT = 10000
|
||||
|
||||
const setUserType = async (req, res, next) => {
|
||||
try {
|
||||
const { user_type } = req.body
|
||||
@@ -29,6 +32,25 @@ const setUserType = async (req, res, next) => {
|
||||
|
||||
// همه کاربران با نوع «user» ثبت میشوند
|
||||
user.user_type = normalizedType
|
||||
|
||||
// سیستم معرفی به دوستان: اینجا آخرین قدمِ ثبتنامه (طبقِ authSteps.js —
|
||||
// getRegistrationStep فقط وقتی user_type ست بشه «complete» میشه)، پس دقیقاً
|
||||
// همین لحظهست که «این کاربر واقعاً ثبتنام کرد» معنی پیدا میکنه، نه صرفِ وارد
|
||||
// کردنِ شماره موبایل (که ممکنه هیچوقت تکمیل نشه). referral_rewarded جلوی
|
||||
// پرداختِ دوباره رو میگیره اگه این مسیر به هر دلیلی دوباره صدا زده بشه
|
||||
if (user.referred_by && !user.referral_rewarded) {
|
||||
await WalletTransactionModel.create({
|
||||
user: user.referred_by,
|
||||
bucket: 'gift',
|
||||
type: 'credit',
|
||||
amount: REFERRAL_REWARD_AMOUNT,
|
||||
referenceType: 'payment',
|
||||
referenceId: user._id,
|
||||
description: 'پاداش معرفی کاربر جدید به مدستاگرام'
|
||||
})
|
||||
user.referral_rewarded = true
|
||||
}
|
||||
|
||||
await user.save()
|
||||
|
||||
return res.json({
|
||||
|
||||
@@ -278,7 +278,7 @@ async function getRankedExplorePosts ({
|
||||
const userIds = [...new Set(posts.map((p) => String(p.user_id)))]
|
||||
const users = await UserModel.find({ _id: { $in: userIds } })
|
||||
.select(
|
||||
'_id user_name first_name last_name expertise sub_expertise province city user_level show_location profile_image is_verified verify_badge user_score rate blocked_users'
|
||||
'_id user_name first_name last_name expertise sub_expertise province city user_level show_location profile_image is_verified verify_badge user_score rate blocked_users cooperation_type cooperation_abroad lat lng'
|
||||
)
|
||||
.lean()
|
||||
|
||||
@@ -442,7 +442,7 @@ async function getPredictedTrendPosts ({
|
||||
const userIds = [...new Set(scored.map((p) => String(p.user_id)))]
|
||||
const users = await UserModel.find({ _id: { $in: userIds } })
|
||||
.select(
|
||||
'_id user_name first_name last_name expertise sub_expertise province city user_level show_location profile_image is_verified verify_badge user_score rate blocked_users'
|
||||
'_id user_name first_name last_name expertise sub_expertise province city user_level show_location profile_image is_verified verify_badge user_score rate blocked_users cooperation_type cooperation_abroad lat lng'
|
||||
)
|
||||
.lean()
|
||||
|
||||
@@ -1313,7 +1313,7 @@ const getPostsWeb = async (req, res, next) => {
|
||||
const userIds = [...new Set(posts.map((p) => String(p.user_id)))]
|
||||
const users = await UserModel.find({ _id: { $in: userIds } })
|
||||
.select(
|
||||
'_id user_name first_name last_name expertise sub_expertise province city user_level show_location profile_image is_verified verify_badge user_score rate lat lng blocked_users'
|
||||
'_id user_name first_name last_name expertise sub_expertise province city user_level show_location profile_image is_verified verify_badge user_score rate lat lng blocked_users cooperation_type cooperation_abroad'
|
||||
)
|
||||
.lean()
|
||||
|
||||
@@ -1338,7 +1338,7 @@ const getPostsWeb = async (req, res, next) => {
|
||||
.lean()
|
||||
const userIds = [...new Set(explorePosts.map((p) => String(p.user_id)))]
|
||||
const usersMap = await UserModel.find({ _id: { $in: userIds } })
|
||||
.select('_id user_name first_name last_name expertise sub_expertise province city user_level show_location profile_image is_verified verify_badge user_score rate blocked_users')
|
||||
.select('_id user_name first_name last_name expertise sub_expertise province city user_level show_location profile_image is_verified verify_badge user_score rate blocked_users cooperation_type cooperation_abroad lat lng')
|
||||
.lean()
|
||||
const usersById = Object.fromEntries(usersMap.map((u) => [String(u._id), u]))
|
||||
|
||||
@@ -1422,7 +1422,7 @@ const getPostsWeb = async (req, res, next) => {
|
||||
}
|
||||
}
|
||||
|
||||
const userSelectFields = '_id user_name first_name last_name expertise sub_expertise province city user_level show_location profile_image is_verified verify_badge user_score rate pinned_post_ids'
|
||||
const userSelectFields = '_id user_name first_name last_name expertise sub_expertise province city user_level show_location profile_image is_verified verify_badge user_score rate pinned_post_ids cooperation_type cooperation_abroad lat lng'
|
||||
let users = await UserModel.find(userFilter)
|
||||
.select(userSelectFields)
|
||||
.lean()
|
||||
|
||||
54
controllers/application/users/referralController.js
Normal file
54
controllers/application/users/referralController.js
Normal file
@@ -0,0 +1,54 @@
|
||||
const crypto = require('crypto')
|
||||
const UserModel = require('../../../models/UserModel')
|
||||
|
||||
// بدون حروف/ارقامِ بههمشبیه (0/O، 1/I/L) — چون این کد قراره دستی هم کپی/تایپ بشه
|
||||
const CODE_ALPHABET = 'ABCDEFGHJKMNPQRSTUVWXYZ23456789'
|
||||
const CODE_LENGTH = 8
|
||||
const MAX_ATTEMPTS = 5
|
||||
|
||||
function generateCode () {
|
||||
const bytes = crypto.randomBytes(CODE_LENGTH)
|
||||
let code = ''
|
||||
for (let i = 0; i < CODE_LENGTH; i++) {
|
||||
code += CODE_ALPHABET[bytes[i] % CODE_ALPHABET.length]
|
||||
}
|
||||
return code
|
||||
}
|
||||
|
||||
/**
|
||||
* کدِ معرفیِ کاربرِ لاگینشده رو برمیگردونه؛ اگه هنوز نداشته باشه (referral_code
|
||||
* روی همهی کاربرهای قدیمی null هست چون این فیلد قبلاً مرده بود) با retry-on-collision
|
||||
* یکی میسازه. لینکِ نهاییِ اشتراکگذاری رو فرانت (وب/اندروید) با همین کد میسازه:
|
||||
* https://modstagram.com/users/<username>?ref=<code>
|
||||
*/
|
||||
const getReferralCode = async (req, res, next) => {
|
||||
try {
|
||||
const userId = req.user._id
|
||||
const user = await UserModel.findById(userId).select('referral_code')
|
||||
if (!user) return res.status(404).json({ error: true, message: 'کاربر یافت نشد' })
|
||||
|
||||
if (user.referral_code) {
|
||||
return res.json({ referral_code: user.referral_code })
|
||||
}
|
||||
|
||||
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
||||
const code = generateCode()
|
||||
try {
|
||||
user.referral_code = code
|
||||
await user.save()
|
||||
return res.json({ referral_code: code })
|
||||
} catch (error) {
|
||||
// E11000 duplicate key — کد بهطور اتفاقی قبلاً برای یه کاربر دیگه ساخته شده؛
|
||||
// بینهایتبعیده (32^8 حالت) ولی retry ساده و بیضرره
|
||||
if (error?.code === 11000) continue
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(500).json({ error: true, message: 'ساخت کد معرفی ناموفق بود، دوباره تلاش کنید' })
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { getReferralCode }
|
||||
@@ -193,7 +193,7 @@ const requestWithdrawal = async (req, res, next) => {
|
||||
|
||||
const initiateWalletCharge = async (req, res, next) => {
|
||||
try {
|
||||
const { amount } = req.body
|
||||
const { amount, platform } = req.body
|
||||
const chargeAmount = Number(amount)
|
||||
if (!Number.isFinite(chargeAmount) || chargeAmount <= 0) {
|
||||
return walletError(res, 422, 'مبلغ نامعتبر است')
|
||||
@@ -204,13 +204,14 @@ const initiateWalletCharge = async (req, res, next) => {
|
||||
return walletError(res, 500, 'آدرس بازگشت پرداخت تنظیم نشده است')
|
||||
}
|
||||
|
||||
const platformSuffix = platform ? `&platform=${encodeURIComponent(platform)}` : ''
|
||||
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}`
|
||||
callback_url: `${callbackBase}/wallet/charge/callback?userId=${req.user._id}&amount=${chargeAmount}${platformSuffix}`
|
||||
},
|
||||
{ headers: { 'Content-Type': 'application/json' } }
|
||||
)
|
||||
@@ -228,9 +229,14 @@ const initiateWalletCharge = async (req, res, next) => {
|
||||
}
|
||||
|
||||
const handleWalletChargeCallback = async (req, res) => {
|
||||
const { Authority, Status, userId, amount } = req.query
|
||||
const failRedirect = `${process.env.APP_SITE}/settings/wallet?charge=failed`
|
||||
const successRedirect = `${process.env.APP_SITE}/settings/wallet?charge=success`
|
||||
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`
|
||||
|
||||
try {
|
||||
if (Status !== 'OK') return res.redirect(failRedirect)
|
||||
|
||||
Reference in New Issue
Block a user