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

@@ -9,6 +9,16 @@ const NotificationModel = require('../../../models/NotificationModel')
const UserModel = require('../../../models/UserModel')
const AdvertisingTypeModel = require('../../../models/AdvertisingTypeModel')
const AdvertisingModel = require('../../../models/AdvertisingModel')
const {
canUseFreeCreation,
consumeFreeCreation,
freeCreationLimitResponse,
} = require('../../../utils/freeCreationLimit')
const {
PROJECT_ACCEPT_RATE,
PROJECT_COMPLETION_COMMISSION_RATE,
creditProjectShare
} = require('../../../utils/projectCommission')
const apiKey = process.env.MERCHENT_CODE // Replace with your ZarinPal API Key
@@ -45,7 +55,11 @@ const initiatePayment = async (req, res) => {
const user = await UserModel.findById(userId)
if (item.name === 'free' && item.price === 0) {
if (user.daily_free_request > 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
await user.save()
const project = await ProjectModel.findById(projectId)
@@ -54,7 +68,7 @@ const initiatePayment = async (req, res) => {
return
}
project.payment_status = 'done' // Set project status to 'done'
project.status = 'paid' // Set project status to 'done'
project.status = 'temp_accept' // Set project status to 'done'
await project.save()
res.status(200).json({ message: 'پروژه رایگان با موفقیت ایجاد شد', projectId, type: 'free' })
} else {
@@ -128,7 +142,7 @@ const handlePaymentCallback = async (req, res) => {
return
}
project.payment_status = 'done' // Set project status to 'done'
project.status = 'paid' // Set project status to 'done'
project.status = 'temp_accept' // Set project status to 'done'
await project.save()
// Send event based on payment status
@@ -206,7 +220,7 @@ const handleSuccessfulPayment = async (req, res) => {
// Update the project status to 'done' or 'paid'
project.payment_status = 'done'
project.status = 'paid'
project.status = 'temp_accept'
await project.save()
// Emit a success event
@@ -283,12 +297,12 @@ const paymentRequestStepOne = async (req, res) => {
if (!request) {
return res.status(404).json({ error: 'Request not found' })
}
const halfAmount = request.price / 2
const acceptAmount = Math.round(Number(request.price) * PROJECT_ACCEPT_RATE)
// Proceed with ZarinPal payment request
const response = await axios.post('https://www.zarinpal.com/pg/rest/WebGate/PaymentRequest.json', {
MerchantID: apiKey,
Amount: parseInt(halfAmount + halfAmount / 100 * 9),
Amount: parseInt(acceptAmount + acceptAmount / 100 * 9),
Description: `پرداخت برای سفارش شماره ${projectId}`,
CallbackURL: `${process.env.APP_URL}/projects/payment-request-step-one-callback?projectId=${projectId}&requestId=${requestId}&userId=${userId}` // Callback URL including requestId
})
@@ -406,7 +420,7 @@ const paymentRequestStepOneCallback = async (req, res) => {
if (!request) {
return res.status(404).json({ error: 'Request not found' })
}
const price = request.price / 2
const price = Math.round(Number(request.price) * PROJECT_ACCEPT_RATE)
const verificationResponse = await axios.post(verificationUrl, {
MerchantID: apiKey,
Authority: authority,
@@ -441,6 +455,15 @@ const paymentRequestStepOneCallback = async (req, res) => {
project.final_price = request.price
project.final_time = request.time
await project.save()
await creditProjectShare(
req.app.get('io'),
request.user,
price,
payment._id,
`پیش‌پرداخت پذیرش همکاری برای پروژه «${project.title || ''}»`
)
// Send notification to selected user
const notification = new NotificationModel({
user_id: request.user,
@@ -451,38 +474,6 @@ const paymentRequestStepOneCallback = async (req, res) => {
})
await notification.save()
// Sms
const userReciver = await UserModel.findById(request.user)
const data = JSON.stringify({
mobile: userReciver?.mobile,
templateId: '302876',
parameters: [
{ name: 'USER', value: userReciver?.first_name + ' ' + userReciver?.last_name },
{ name: 'PROJECT', value: project?.title }
]
})
const config = {
method: 'post',
url: 'https://api.sms.ir/v1/send/verify',
headers: {
'Content-Type': 'application/json',
Accept: 'text/plain',
'x-api-key': '2JKxGbTzE64t0YTkjip6RYHnm9mq1haMlidF8WWwQunwubHFVW6iaMmfFQbMXEDt'
},
data
}
axios(config)
.then(function (response) {
})
.catch(function (error) {
console.log(error)
})
// End Sms
// Send event based on payment status
if ('eventEmitter' in req.app) {
if (status === 'OK') {
@@ -558,6 +549,14 @@ const paymentRequestStepTwoCallback = async (req, res) => {
project.installments.push({ installment_number: 2, amount: secondInstallmentAmount, due_date: new Date() })
await project.save()
await creditProjectShare(
req.app.get('io'),
project.selected_user,
Math.round(secondInstallmentAmount * (1 - PROJECT_COMPLETION_COMMISSION_RATE)),
payment._id,
`سهم همکاری از پرداخت مرحله‌ای پروژه «${project.title || ''}» (پس از کسر ۱۰٪ کارمزد)`
)
// Send event based on payment status
if ('eventEmitter' in req.app) {
if (status === 'OK') {
@@ -644,6 +643,15 @@ const paymentRequestStepAllCallback = async (req, res) => {
// Update project's installment details
project.installments.push({ installment_number: 2, amount: secondInstallmentAmount, due_date: new Date() })
await project.save()
await creditProjectShare(
req.app.get('io'),
project.selected_user,
Math.round(secondInstallmentAmount * (1 - PROJECT_COMPLETION_COMMISSION_RATE)),
payment._id,
`سهم همکاری از پرداخت مرحله‌ای پروژه «${project.title || ''}» (پس از کسر ۱۰٪ کارمزد)`
)
const notification = new NotificationModel({
user_id: project.selected_user,
project_post_id: projectId,
@@ -705,7 +713,11 @@ const initiateAdvertisingPayment = async (req, res) => {
const user = await UserModel.findById(userId)
if (item.name === 'free' && item.price === 0) {
if (user.daily_free_request > 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
await user.save()
const project = await ProjectModel.findById(advertisingId)
@@ -714,7 +726,7 @@ const initiateAdvertisingPayment = async (req, res) => {
return
}
project.payment_status = 'done' // Set project status to 'done'
project.status = 'paid' // Set project status to 'done'
project.status = 'temp_accept' // Set project status to 'done'
await project.save()
res.status(200).json({ message: 'پروژه رایگان با موفقیت ایجاد شد', advertisingId, type: 'free' })
} else {
@@ -787,7 +799,7 @@ const handleAdvertisingPaymentCallback = async (req, res) => {
return
}
advertising.payment_status = 'done' // Set advertising status to 'done'
advertising.status = 'paid' // Set advertising status to 'done'
advertising.status = 'temp_accept' // Set advertising status to 'done'
await advertising.save()
// Send event based on payment status
@@ -866,7 +878,7 @@ const handleSuccessfulAdvertisingPayment = async (req, res) => {
// Update the advertising status to 'done' or 'paid'
advertising.payment_status = 'done'
advertising.status = 'paid'
advertising.status = 'temp_accept'
await advertising.save()
// Emit a success event
@@ -941,7 +953,11 @@ const republishAdvertisingPayment = async (req, res) => {
const user = await UserModel.findById(userId)
if (item.name === 'free' && item.price === 0) {
if (user.daily_free_request > 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
await user.save()
const project = await ProjectModel.findById(advertisingId)
@@ -950,7 +966,7 @@ const republishAdvertisingPayment = async (req, res) => {
return
}
project.payment_status = 'done' // Set project status to 'done'
project.status = 'paid' // Set project status to 'done'
project.status = 'temp_accept' // Set project status to 'done'
await project.save()
res.status(200).json({ message: 'پروژه رایگان با موفقیت ایجاد شد', advertisingId, type: 'free' })
} else {

View File

@@ -12,8 +12,15 @@ const AdvertisingModel = require('../../../models/AdvertisingModel');
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 apiKey = "c7c41e8a-918f-4741-bcd5-58f3bc51db73"; // اصلاح نام متغیر محیطی
const OFFER_COMMISSION_RATE = 0.3
const apiKey =
process.env.ZARINPAL_MERCHANT_ID ||
process.env.MERCHENT_CODE ||
'c7c41e8a-918f-4741-bcd5-58f3bc51db73'
// Function to get price based on item name from database
const getPriceFromDatabase = async (itemName) => {
@@ -42,58 +49,109 @@ const getOfferPriceFromDatabase = async (offerType) => {
// Controller function for initiating payment
const initiatePaymentWeb = async (req, res) => {
try {
const token = req.header('Authorization').split(' ')[1]
if (!token) return res.status(401).send('Access Denied')
// eslint-disable-next-line no-unused-vars
const authHeader = req.header('Authorization') || ''
const token = authHeader.startsWith('Bearer ')
? authHeader.slice(7).trim()
: authHeader.trim()
if (!token) return res.status(401).json({ error: true, message: 'Access Denied' })
const decodedToken = jwt.verify(token, process.env.APP_SECRET)
const userId = decodedToken.id
const itemName = req.body.item_name // Get item name from request body
const price = await getPriceFromDatabase(itemName) // Get price from database
const projectId = req.body.projectId // Get project ID from request body
// Save project ID for later use in callback URL
req.session.projectId = projectId
const item = await TypeAndPriceModel.findOne({ name: itemName })
const user = await UserModel.findById(userId)
const itemName = req.body.item_name
const projectId = req.body.projectId
if (item.name === 'free' && item.price === 0) {
if (user.daily_free_request > 0) {
user.daily_free_request -= 1
await user.save()
const project = await ProjectModel.findById(projectId)
if (!project) {
res.status(404).send('Project not found')
return
}
project.payment_status = 'done' // Set project status to 'done'
project.status = 'paid' // Set project status to 'done'
await project.save()
res.status(200).json({ message: 'پروژه رایگان با موفقیت ایجاد شد', projectId, type: 'free' })
} else {
res.status(403).json({ message: 'شما قبلاً از درخواست رایگان امروز استفاده کرده‌اید.' })
if (!itemName || !projectId) {
return res.status(422).json({
error: true,
message: 'نوع پروژه یا شناسه پروژه ارسال نشده است'
})
}
const item = await TypeAndPriceModel.findOne({ name: itemName })
if (!item) {
return res.status(404).json({
error: true,
message: `نوع پروژه «${itemName}» در سیستم یافت نشد`
})
}
const price = Number(item.price) || 0
const user = await UserModel.findById(userId)
if (!user) {
return res.status(422).json({ error: true, message: 'کاربر یافت نشد' })
}
if (req.session) {
req.session.projectId = projectId
}
if (item.name === 'free' || price === 0) {
if (!user.daily_free_request || user.daily_free_request <= 0) {
return res.status(403).json({
error: true,
message: 'شما قبلاً از درخواست رایگان امروز استفاده کرده‌اید.'
})
}
} else {
const response = await axios.post('https://www.zarinpal.com/pg/rest/WebGate/PaymentRequest.json', {
user.daily_free_request -= 1
await user.save()
const project = await ProjectModel.findById(projectId)
if (!project) {
return res.status(404).json({ error: true, message: 'پروژه یافت نشد' })
}
project.payment_status = 'done'
project.status = 'temp_accept'
await project.save()
return res.status(200).json({
message: 'پروژه رایگان با موفقیت ایجاد شد',
projectId,
type: 'free'
})
}
// مثل بیلبورد وب: کال‌بک از دامنه سایت (proxy) تا زرین‌پال دامنه را بپذیرد
const callbackBase =
process.env.APP_SITE_CallBack ||
process.env.APP_URL ||
process.env.API_URL ||
''
if (!callbackBase) {
return res.status(500).json({
error: true,
message: 'آدرس بازگشت پرداخت تنظیم نشده است'
})
}
const response = await axios.post(
'https://www.zarinpal.com/pg/rest/WebGate/PaymentRequest.json',
{
MerchantID: apiKey,
Amount: Number(price),
Description: `پرداخت برای سفارش شماره ${projectId}`,
CallbackURL: `${process.env.APP_URL}/projects/payment-web?projectId=${projectId}&itemName=${itemName}&userId=${userId}` // Assuming your backend URL for handling payment callback
})
CallbackURL: `${callbackBase}/projects/payment-web?projectId=${projectId}&itemName=${encodeURIComponent(itemName)}&userId=${userId}`
},
{ timeout: 20000 }
)
const data = response.data
if (data.Status === 100) {
// Payment request was successful
const authority = data.Authority // The payment authority (شناسه پرداخت)
res.status(200).json({ authority })
} else {
// Payment request failed
console.error('Error occurred while initiating payment:', data)
res.status(500).json({ error: 'Error occurred while initiating payment' })
}
const data = response.data
if (data.Status === 100 && data.Authority) {
return res.status(200).json({ authority: data.Authority })
}
console.error('ZarinPal initiate error:', data)
return res.status(502).json({
error: true,
message: 'درگاه پرداخت پاسخ نامعتبر داد. لطفاً دوباره تلاش کنید.',
zarinpalStatus: data?.Status
})
} catch (error) {
// Error occurred while getting price from database or sending the request
console.error('Error occurred:', error)
res.status(500).json({ error: 'Internal server error' })
console.error('initiatePaymentWeb error:', error?.response?.data || error?.message || error)
const zarinpalMsg =
error?.response?.data?.errors?.message ||
error?.response?.data?.message
return res.status(500).json({
error: true,
message: zarinpalMsg || error?.message || 'خطا در شروع پرداخت'
})
}
}
// Controller function for handling payment callback from ZarinPal
@@ -138,7 +196,7 @@ const handlePaymentCallbackWeb = async (req, res) => {
return
}
project.payment_status = 'done' // Set project status to 'done'
project.status = 'paid' // Set project status to 'done'
project.status = 'temp_accept' // Set project status to 'done'
await project.save()
// Send event based on payment status
@@ -185,6 +243,12 @@ const handlePaymentCallbackWeb = async (req, res) => {
}
}
/**
* Accepting a collaborator's request now requires the creator to pay 30% of
* the agreed price upfront — this initiates that ZarinPal payment. The actual
* accept (selecting the user, crediting their wallet) happens in
* paymentRequestAcceptWebCallback once the payment verifies.
*/
const paymentRequestAcceptWeb = async (req, res) => {
try {
const token = req.header('Authorization').split(' ')[1]
@@ -192,7 +256,6 @@ const paymentRequestAcceptWeb = async (req, res) => {
const decodedToken = jwt.verify(token, process.env.APP_SECRET)
const userId = decodedToken.id
const projectId = req.body.projectId
// Find the project
const project = await ProjectModel.findById(projectId)
if (!project) {
return res.status(404).json({ error: 'Project not found' })
@@ -208,21 +271,114 @@ const paymentRequestAcceptWeb = async (req, res) => {
})
}
// Get the final price from the request model
const requestId = req.body.requestId
const request = await RequestModel.findById(requestId)
if (!request) {
return res.status(404).json({ error: 'Request not found' })
}
const acceptAmount = Math.round(Number(request.price) * PROJECT_ACCEPT_RATE)
const callbackBase =
process.env.APP_SITE_CallBack ||
process.env.APP_URL ||
process.env.API_URL ||
''
if (!callbackBase) {
return res.status(500).json({ error: true, message: 'آدرس بازگشت پرداخت تنظیم نشده است' })
}
const response = await axios.post(
'https://www.zarinpal.com/pg/rest/WebGate/PaymentRequest.json',
{
MerchantID: apiKey,
Amount: acceptAmount,
Description: `پرداخت پذیرش همکاری برای پروژه ${project.title || projectId}`,
CallbackURL: `${callbackBase}/projects/payment-request-step-one-callback/web?projectId=${projectId}&requestId=${requestId}&userId=${userId}`
},
{ timeout: 20000 }
)
const data = response.data
if (data.Status === 100 && data.Authority) {
return res.status(200).json({
authority: data.Authority,
paymentUrl: `https://www.zarinpal.com/pg/StartPay/${data.Authority}`
})
}
console.error('paymentRequestAcceptWeb ZarinPal error:', data)
return res.status(502).json({ error: true, message: 'درگاه پرداخت پاسخ نامعتبر داد. لطفاً دوباره تلاش کنید.' })
} catch (error) {
console.error('paymentRequestAcceptWeb error:', error?.response?.data || error?.message || error)
return res.status(500).json({ error: 'Internal server error' })
}
}
const paymentRequestAcceptWebCallback = async (req, res) => {
const { Authority, Status, projectId, requestId, userId } = req.query
const failRedirect = `${process.env.APP_SITE}/projects/payment/failed?projectId=${projectId || ''}`
try {
if (Status !== 'OK') return res.redirect(failRedirect)
const request = await RequestModel.findById(requestId)
const project = await ProjectModel.findById(projectId)
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`)
}
const acceptAmount = Math.round(Number(request.price) * PROJECT_ACCEPT_RATE)
const verificationResponse = await axios.post(
'https://www.zarinpal.com/pg/rest/WebGate/PaymentVerification.json',
{ MerchantID: apiKey, Authority, Amount: acceptAmount }
)
const verificationData = verificationResponse.data
if (verificationData.Status !== 100 && verificationData.Status !== 101) {
const payment = new PaymentModel({
amount: acceptAmount,
status: 'failed',
authority: Authority,
user_id: userId,
project_id: projectId,
type: 'installment'
})
await payment.save()
return res.redirect(failRedirect)
}
const payment = new PaymentModel({
amount: acceptAmount,
status: 'successful',
authority: Authority,
user_id: userId,
project_id: projectId,
type: 'installment',
installment_step: 1
})
await payment.save()
request.status = 'accepted'
await request.save()
project.selected_user = request.user
project.status = 'ongoing'
project.installments.push({ installment_number: 1, amount: acceptAmount, due_date: new Date() })
project.final_price = request.price
project.final_time = request.time
await project.save()
// Send notification to selected user
await creditProjectShare(
req.app.get('io'),
request.user,
acceptAmount,
payment._id,
`پیش‌پرداخت پذیرش همکاری برای پروژه «${project.title || ''}»`
)
const notification = new NotificationModel({
user_id: request.user,
project_post_id: projectId,
@@ -232,39 +388,140 @@ const paymentRequestAcceptWeb = async (req, res) => {
})
await notification.save()
// Sms
const userReciver = await UserModel.findById(request.user)
const data = JSON.stringify({
mobile: userReciver?.mobile,
templateId: '302876',
parameters: [
{ name: 'USER', value: userReciver?.first_name + ' ' + userReciver?.last_name },
{ name: 'PROJECT', value: project?.title }
]
})
const config = {
method: 'post',
url: 'https://api.sms.ir/v1/send/verify',
headers: {
'Content-Type': 'application/json',
Accept: 'text/plain',
'x-api-key': '2JKxGbTzE64t0YTkjip6RYHnm9mq1haMlidF8WWwQunwubHFVW6iaMmfFQbMXEDt'
},
data
}
axios(config)
.then(function (response) {
})
.catch(function (error) {
console.log(error)
})
res.status(200).json({
message: 'درخواست با موفقیت تایید شد'
})
return res.redirect(`${process.env.APP_SITE}/projects/payment/success?projectId=${projectId}&type=accept`)
} catch (error) {
// Error occurred while getting price from database or sending the request
console.error('Error occurred:', error)
res.status(500).json({ error: 'Internal server error' })
console.error('paymentRequestAcceptWebCallback error:', error?.response?.data || error?.message || error)
return res.redirect(failRedirect)
}
}
/** Initiates the remaining-70%-minus-commission payment when the employer marks a project complete. */
const initiateProjectCompletionPaymentWeb = async (req, res) => {
try {
const token = req.header('Authorization').split(' ')[1]
if (!token) return res.status(401).send('Access Denied')
const decodedToken = jwt.verify(token, process.env.APP_SECRET)
const userId = decodedToken.id
const { projectId } = req.body
const project = await ProjectModel.findById(projectId)
if (!project) return res.status(404).json({ error: true, message: 'پروژه یافت نشد' })
// eslint-disable-next-line eqeqeq
if (userId != project.creator_id.toString()) {
return res.status(403).json({ error: true, message: 'شما کارفرمای این پروژه نیستید' })
}
if (!project.selected_user) {
return res.status(400).json({ error: true, message: 'برای این پروژه هنوز کاربری انتخاب نشده است' })
}
if (project.completionShareCredited) {
return res.status(400).json({ error: true, message: 'مبلغ باقی‌مانده قبلاً پرداخت شده است' })
}
const acceptAmount = Math.round(Number(project.final_price) * PROJECT_ACCEPT_RATE)
const remainingAmount = Math.round(Number(project.final_price)) - acceptAmount
if (remainingAmount <= 0) {
return res.status(400).json({ error: true, message: 'مبلغی برای پرداخت باقی نمانده است' })
}
const callbackBase =
process.env.APP_SITE_CallBack ||
process.env.APP_URL ||
process.env.API_URL ||
''
if (!callbackBase) {
return res.status(500).json({ error: true, message: 'آدرس بازگشت پرداخت تنظیم نشده است' })
}
const response = await axios.post(
'https://www.zarinpal.com/pg/rest/WebGate/PaymentRequest.json',
{
MerchantID: apiKey,
Amount: remainingAmount,
Description: `پرداخت باقی‌مانده پروژه ${project.title || projectId}`,
CallbackURL: `${callbackBase}/projects/payment-completion-callback/web?projectId=${projectId}&userId=${userId}`
},
{ timeout: 20000 }
)
const data = response.data
if (data.Status === 100 && data.Authority) {
return res.status(200).json({
authority: data.Authority,
paymentUrl: `https://www.zarinpal.com/pg/StartPay/${data.Authority}`
})
}
console.error('initiateProjectCompletionPaymentWeb ZarinPal error:', data)
return res.status(502).json({ error: true, message: 'درگاه پرداخت پاسخ نامعتبر داد. لطفاً دوباره تلاش کنید.' })
} catch (error) {
console.error('initiateProjectCompletionPaymentWeb error:', error?.response?.data || error?.message || error)
return res.status(500).json({ error: 'Internal server error' })
}
}
const handleProjectCompletionPaymentCallbackWeb = async (req, res) => {
const { Authority, Status, projectId, userId } = req.query
const failRedirect = `${process.env.APP_SITE}/projects/payment/failed?projectId=${projectId || ''}`
try {
if (Status !== 'OK') return res.redirect(failRedirect)
const project = await ProjectModel.findById(projectId)
if (!project || !project.selected_user) return res.redirect(failRedirect)
if (project.completionShareCredited) {
return res.redirect(`${process.env.APP_SITE}/projects/payment/success?projectId=${projectId}&type=completion`)
}
const acceptAmount = Math.round(Number(project.final_price) * PROJECT_ACCEPT_RATE)
const remainingAmount = Math.round(Number(project.final_price)) - acceptAmount
const verificationResponse = await axios.post(
'https://www.zarinpal.com/pg/rest/WebGate/PaymentVerification.json',
{ MerchantID: apiKey, Authority, Amount: remainingAmount }
)
const verificationData = verificationResponse.data
if (verificationData.Status !== 100 && verificationData.Status !== 101) {
const payment = new PaymentModel({
amount: remainingAmount,
status: 'failed',
authority: Authority,
user_id: userId,
project_id: projectId,
type: 'installment'
})
await payment.save()
return res.redirect(failRedirect)
}
const payment = new PaymentModel({
amount: remainingAmount,
status: 'successful',
authority: Authority,
user_id: userId,
project_id: projectId,
type: 'installment',
installment_step: 'completion'
})
await payment.save()
project.installments.push({ installment_number: 2, amount: remainingAmount, due_date: new Date() })
project.completionShareCredited = true
await project.save()
const netShare = Math.round(remainingAmount * (1 - PROJECT_COMPLETION_COMMISSION_RATE))
await creditProjectShare(
req.app.get('io'),
project.selected_user,
netShare,
payment._id,
`تسویه نهایی پروژه «${project.title || ''}» (پس از کسر ۱۰٪ کارمزد)`
)
return res.redirect(`${process.env.APP_SITE}/projects/payment/success?projectId=${projectId}&type=completion`)
} catch (error) {
console.error('handleProjectCompletionPaymentCallbackWeb error:', error?.response?.data || error?.message || error)
return res.redirect(failRedirect)
}
}
const initiateAdvertisingPaymentWeb = async (req, res) => {
@@ -294,7 +551,7 @@ const initiateAdvertisingPaymentWeb = async (req, res) => {
return
}
project.payment_status = 'done' // Set project status to 'done'
project.status = 'paid' // Set project status to 'done'
project.status = 'temp_accept' // Set project status to 'done'
await project.save()
res.status(200).json({ message: 'بیلبورد رایگان با موفقیت ایجاد شد', advertisingId, type: 'free' })
} else {
@@ -367,7 +624,7 @@ const handleAdvertisingPaymentCallbackWeb = async (req, res) => {
return
}
advertising.payment_status = 'done' // Set advertising status to 'done'
advertising.status = 'paid' // Set advertising status to 'done'
advertising.status = 'temp_accept' // Set advertising status to 'done'
await advertising.save()
// Send event based on payment status
@@ -442,7 +699,7 @@ const republishAdvertisingPaymentWeb = async (req, res) => {
return
}
project.payment_status = 'done' // Set project status to 'done'
project.status = 'paid' // Set project status to 'done'
project.status = 'temp_accept' // Set project status to 'done'
await project.save()
res.status(200).json({ message: 'پروژه رایگان با موفقیت ایجاد شد', advertisingId, type: 'free' })
} else {
@@ -650,7 +907,7 @@ const initiateOfferPaymentWeb = async (req, res) => {
const decoded = jwt.verify(token, process.env.APP_SECRET);
const userId = decoded.id;
const { receiverId, offerType } = req.body;
const { receiverId, offerType, platform } = req.body;
if (!receiverId || !offerType)
return res.status(400).json({ error: 'Invalid data' });
@@ -678,8 +935,24 @@ const initiateOfferPaymentWeb = async (req, res) => {
});
}
// 🎁 گیرنده «دریافت پرداختی از چت» رو غیرفعال کرده — درخواست همکاری براش همیشه
// رایگانه، مستقل از سهمیه‌ی رایگان ماهانه‌ی فرستنده
if (receiver.offer_payment_enabled === false) {
const offer = await OfferModel.create({
sender: userId,
receiver: receiverId,
transaction_id: null,
});
return res.json({
type: 'free',
offerId: offer._id,
});
}
const amount = Number(price) * 10; // ✅ ریال – بدون ×10
const callbackUrl = `https://api.modstagram.com/api/v1/offers/payment-web?receiverId=${receiverId}&offerType=${offerType}&userId=${userId}`;
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 zarinpalRes = await axios.post(
'https://api.zarinpal.com/pg/v4/payment/request.json',
@@ -866,11 +1139,15 @@ const handleOfferPaymentCallback = async (req, res) => {
console.log('🔔 CALLBACK QUERY:', req.query);
const isAndroid = req.query.platform === 'android';
try {
const { Authority, Status, receiverId, offerType, userId } = req.query;
if (Status !== 'OK')
return res.redirect(`${process.env.APP_SITE}/offer/payment/failed`);
return isAndroid
? res.redirect(`modstagram://FailedPay?type=offer`)
: res.redirect(`${process.env.APP_SITE}/offer/payment/failed`);
const price = await getOfferPriceFromDatabase(offerType); // ریال
const amount = Number(price) * 10;
@@ -899,21 +1176,33 @@ const handleOfferPaymentCallback = async (req, res) => {
console.log('🔔payment:', payment);
const offerPriceToman = amount / 10
const receiverShare = Math.round(offerPriceToman * OFFER_COMMISSION_RATE)
// سهم گیرنده فقط با پاسخ‌گویی ظرف ۲۴ ساعت به اولین پیام دایرکت واریز می‌شود
// (نگاه کنید به trackOfferMessageForCommission که موقع ارسال پیام صدا زده می‌شود)
const offer = await OfferModel.create({
sender: userId,
receiver: receiverId,
transaction_id: payment._id,
pendingCommissionAmount: receiverShare,
});
return res.redirect(
`${process.env.APP_SITE}/offer/payment/success?offerId=${offer._id}`
);
return isAndroid
? res.redirect(`modstagram://SuccessPay?type=offer&offerId=${offer._id}`)
: res.redirect(
`${process.env.APP_SITE}/offer/payment/success?offerId=${offer._id}`
);
}
return res.redirect(`${process.env.APP_SITE}/offer/payment/failed`);
return isAndroid
? res.redirect(`modstagram://FailedPay?type=offer`)
: res.redirect(`${process.env.APP_SITE}/offer/payment/failed`);
} catch (err) {
console.error(err);
return res.redirect(`${process.env.APP_SITE}/offer/payment/failed`);
return isAndroid
? res.redirect(`modstagram://FailedPay?type=offer`)
: res.redirect(`${process.env.APP_SITE}/offer/payment/failed`);
}
};
@@ -926,6 +1215,9 @@ module.exports = {
initiatePaymentWeb,
handlePaymentCallbackWeb,
paymentRequestAcceptWeb,
paymentRequestAcceptWebCallback,
initiateProjectCompletionPaymentWeb,
handleProjectCompletionPaymentCallbackWeb,
initiateAdvertisingPaymentWeb,
handleAdvertisingPaymentCallbackWeb,
republishAdvertisingPaymentWeb,

View File

@@ -0,0 +1,163 @@
const axios = require('axios')
const OrderModel = require('../../../models/OrderModel')
const PaymentModel = require('../../../models/PaymentModel')
const ShopModel = require('../../../models/ShopModel')
const { notifyUser } = require('../../../utils/notifyUser')
const MERCHANT_ID = process.env.ZARINPAL_MERCHANT_ID || process.env.MERCHENT_CODE
/** Toman (stored) -> Rial (ZarinPal API v4 unit) */
function toRial (tomanAmount) {
return Number(tomanAmount) * 10
}
const initiateOrderPayment = async (req, res) => {
try {
const { orderId } = req.body
if (!orderId) {
return res.status(422).json({ error: true, message: 'سفارش مشخص نشده است' })
}
const order = await OrderModel.findById(orderId)
if (!order) {
return res.status(404).json({ error: true, message: 'سفارش یافت نشد' })
}
if (String(order.buyer) !== String(req.user._id)) {
return res.status(403).json({ error: true, message: 'دسترسی به این سفارش ندارید' })
}
if (order.status !== 'pending_payment') {
return res.status(400).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 response = await axios.post(
'https://api.zarinpal.com/pg/v4/payment/request.json',
{
merchant_id: MERCHANT_ID,
amount: toRial(order.total_amount),
description: `پرداخت سفارش شماره ${order._id}`,
callback_url: `${callbackBase}/payment/shop-order/callback?orderId=${order._id}`
},
{ headers: { 'Content-Type': 'application/json' } }
)
const result = response.data?.data
if (result?.code === 100 && result?.authority) {
order.paymentAuthority = result.authority
await order.save()
return res.json({ authority: result.authority })
}
console.error('ZarinPal initiate error:', response.data)
return res.status(502).json({ error: true, message: 'درگاه پرداخت پاسخ نامعتبر داد. لطفاً دوباره تلاش کنید.' })
} catch (error) {
console.error('initiateOrderPayment error:', error?.response?.data || error.message)
return res.status(500).json({ error: true, message: 'خطا در شروع پرداخت' })
}
}
const handleOrderPaymentCallback = async (req, res) => {
const { orderId, Authority, Status } = req.query
const failRedirect = `${process.env.APP_SITE}/shops/payment/failed?orderId=${orderId || ''}`
const successRedirect = (id) => `${process.env.APP_SITE}/shops/payment/success?orderId=${id}`
const io = req.app.get('io')
try {
const order = await OrderModel.findById(orderId)
if (!order) return res.redirect(failRedirect)
// سفارش ممکن است در فاصله رفتن به درگاه و بازگشت، توسط کرون لغو شده باشد
// (مثلاً عبور از مهلت ۶۰ دقیقه‌ای) — از فعال کردن دوباره‌ی یک سفارش لغوشده جلوگیری می‌شود
if (order.status !== 'pending_payment') {
return res.redirect(failRedirect)
}
if (Status !== 'OK') {
order.status = 'failed'
await order.save()
await PaymentModel.create({
amount: order.total_amount,
status: 'failed',
authority: Authority,
user_id: order.buyer,
order_id: order._id,
type: 'shop_order'
})
await notifyUser(io, order.buyer, {
project_post_id: order._id,
type: 'shop_order',
title: 'پرداخت ناموفق',
description: 'پرداخت سفارش شما ناموفق بود. می‌توانید دوباره تلاش کنید.'
})
return res.redirect(failRedirect)
}
const verify = await axios.post(
'https://api.zarinpal.com/pg/v4/payment/verify.json',
{
merchant_id: MERCHANT_ID,
authority: Authority,
amount: toRial(order.total_amount)
},
{ headers: { 'Content-Type': 'application/json' } }
)
const data = verify.data?.data
if (data?.code === 100 || data?.code === 101) {
await PaymentModel.create({
amount: order.total_amount,
status: 'successful',
authority: Authority,
user_id: order.buyer,
order_id: order._id,
type: 'shop_order'
})
order.status = 'processing'
await order.save()
const shop = await ShopModel.findById(order.shop)
if (shop) {
await notifyUser(io, shop.owner, {
project_post_id: order._id,
type: 'shop_order',
title: 'سفارش جدید',
description: `یک سفارش جدید برای فروشگاه ${shop.name} ثبت و پرداخت شد.`
})
}
return res.redirect(successRedirect(order._id))
}
order.status = 'failed'
await order.save()
await PaymentModel.create({
amount: order.total_amount,
status: 'failed',
authority: Authority,
user_id: order.buyer,
order_id: order._id,
type: 'shop_order'
})
await notifyUser(io, order.buyer, {
project_post_id: order._id,
type: 'shop_order',
title: 'پرداخت ناموفق',
description: 'پرداخت سفارش شما ناموفق بود. می‌توانید دوباره تلاش کنید.'
})
return res.redirect(failRedirect)
} catch (error) {
console.error('handleOrderPaymentCallback error:', error?.response?.data || error.message)
return res.redirect(failRedirect)
}
}
module.exports = {
initiateOrderPayment,
handleOrderPaymentCallback
}