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

@@ -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(

View File

@@ -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 || ''}`)
}
}

View File

@@ -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,