full
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
163
controllers/application/payment/shopOrderPaymentController.js
Normal file
163
controllers/application/payment/shopOrderPaymentController.js
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user