check
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -433,7 +433,7 @@ const getAdvertisingsWeb = async (req, res, next) => {
|
||||
const { page = 1, limit = 10, province, city, category, search, sort } = req.query
|
||||
|
||||
const filter = {
|
||||
status: billboardPublicStatusFilter(),
|
||||
status: billboardPublicStatusFilter()
|
||||
}
|
||||
if (province) {
|
||||
const provinceFind = await ProvinceModel.findOne({ id: province }, { _id: 0 })
|
||||
@@ -830,22 +830,16 @@ const addComment = async (req, res, next) => {
|
||||
|
||||
const decodedToken = jwt.verify(token, process.env.APP_SECRET)
|
||||
const userId = decodedToken.id
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const user = await UserModel.findById(decodedToken.id)
|
||||
// if (user.is_verified !== 'verified') {
|
||||
// return res.status(422).json({
|
||||
// error: true,
|
||||
// message: 'مدارک شما تایید نشده است'
|
||||
// })
|
||||
// }
|
||||
const newComment = await saveCommentWithModerationStatus(
|
||||
AdvertisingComment,
|
||||
{
|
||||
advertisingId,
|
||||
userId,
|
||||
text,
|
||||
text
|
||||
},
|
||||
'text'
|
||||
'text',
|
||||
user?.verify_badge
|
||||
)
|
||||
|
||||
const advertising = await AdvertisingModel.findById(advertisingId)
|
||||
|
||||
@@ -13,6 +13,12 @@ function toDateOnlyString (date) {
|
||||
return new Date(date).toISOString().slice(0, 10)
|
||||
}
|
||||
|
||||
// درگاه قدیمیِ REST زرینپال (/pg/rest/WebGate) منسوخ شده — مبلغ به API نسخهی v4
|
||||
// باید به ریال ارسال بشه، نه تومان (برخلاف API قدیمی)
|
||||
function toRial (tomanAmount) {
|
||||
return Number(tomanAmount) * 10
|
||||
}
|
||||
|
||||
/** Initiates the 30% deposit payment for a pending booking. */
|
||||
const initiateBookingPayment = async (req, res) => {
|
||||
try {
|
||||
@@ -37,27 +43,27 @@ const initiateBookingPayment = async (req, res) => {
|
||||
}
|
||||
|
||||
const response = await axios.post(
|
||||
'https://www.zarinpal.com/pg/rest/WebGate/PaymentRequest.json',
|
||||
'https://api.zarinpal.com/pg/v4/payment/request.json',
|
||||
{
|
||||
MerchantID: apiKey,
|
||||
Amount: booking.depositAmount,
|
||||
Description: `بیعانه رزرو خدمت ${booking.serviceTitle}`,
|
||||
CallbackURL: `${callbackBase}/bookings/payment-callback/web?bookingId=${booking._id}`
|
||||
merchant_id: apiKey,
|
||||
amount: toRial(booking.depositAmount),
|
||||
description: `بیعانه رزرو خدمت ${booking.serviceTitle}`,
|
||||
callback_url: `${callbackBase}/bookings/payment-callback/web?bookingId=${booking._id}`
|
||||
},
|
||||
{ timeout: 20000 }
|
||||
{ headers: { 'Content-Type': 'application/json' }, timeout: 20000 }
|
||||
)
|
||||
|
||||
const data = response.data
|
||||
if (data.Status === 100 && data.Authority) {
|
||||
booking.paymentAuthority = data.Authority
|
||||
const result = response.data?.data
|
||||
if (result?.code === 100 && result?.authority) {
|
||||
booking.paymentAuthority = result.authority
|
||||
await booking.save()
|
||||
return res.status(200).json({
|
||||
authority: data.Authority,
|
||||
paymentUrl: `https://www.zarinpal.com/pg/StartPay/${data.Authority}`
|
||||
authority: result.authority,
|
||||
paymentUrl: `https://www.zarinpal.com/pg/StartPay/${result.authority}`
|
||||
})
|
||||
}
|
||||
|
||||
console.error('initiateBookingPayment ZarinPal error:', data)
|
||||
console.error('initiateBookingPayment ZarinPal error:', response.data)
|
||||
return res.status(502).json({ error: true, message: 'درگاه پرداخت پاسخ نامعتبر داد. لطفاً دوباره تلاش کنید.' })
|
||||
} catch (error) {
|
||||
console.error('initiateBookingPayment error:', error?.response?.data || error?.message || error)
|
||||
@@ -81,12 +87,13 @@ const handleBookingPaymentCallback = async (req, res) => {
|
||||
}
|
||||
|
||||
const verificationResponse = await axios.post(
|
||||
'https://www.zarinpal.com/pg/rest/WebGate/PaymentVerification.json',
|
||||
{ MerchantID: apiKey, Authority, Amount: booking.depositAmount }
|
||||
'https://api.zarinpal.com/pg/v4/payment/verify.json',
|
||||
{ merchant_id: apiKey, authority: Authority, amount: toRial(booking.depositAmount) },
|
||||
{ headers: { 'Content-Type': 'application/json' } }
|
||||
)
|
||||
|
||||
const verificationData = verificationResponse.data
|
||||
if (verificationData.Status !== 100 && verificationData.Status !== 101) {
|
||||
const verificationData = verificationResponse.data?.data
|
||||
if (verificationData?.code !== 100 && verificationData?.code !== 101) {
|
||||
await PaymentModel.create({
|
||||
amount: booking.depositAmount,
|
||||
status: 'failed',
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
const jwt = require('jsonwebtoken')
|
||||
const PaymentModel = require('../../../models/PaymentModel')
|
||||
const ProjectModel = require('../../../models/ProjectModel')
|
||||
const moment = require('moment-jalaali')
|
||||
|
||||
const getFinancial = async (req, res) => {
|
||||
try {
|
||||
@@ -15,40 +14,29 @@ const getFinancial = async (req, res) => {
|
||||
limit: req.query.limit || 10, // حداکثر تعداد موارد برای هر صفحه
|
||||
sort: { createdAt: -1 }
|
||||
}
|
||||
// Find all payments associated with the user
|
||||
// همهی پرداختهای کاربر (پروژه، تبلیغات، هدیه، فروشگاه، رزرو، دوره و ...)، نه فقط
|
||||
// پرداختهای مرتبط با پروژه — project_title فقط وقتی project_id واقعاً ست شده باشه پر میشه
|
||||
const financial = await PaymentModel.paginate(
|
||||
{ user_id: userId }, // فیلتر
|
||||
options // گزینههای پیجینیشن
|
||||
)
|
||||
// Prepare the data to be sent back
|
||||
const userPayments = await Promise.all(financial.docs.map(async (payment) => {
|
||||
// Find the project associated with the payment
|
||||
const project = await ProjectModel.findById(payment.project_id)
|
||||
|
||||
if (!project) {
|
||||
return null
|
||||
}
|
||||
// استفاده از کتابخانه moment-jalaali برای تبدیل تاریخ و زمان به شمسی
|
||||
const shamsiDate = moment(payment.createdAt).format('jYYYY-jMM-jDD')
|
||||
const shamsiTime = moment(payment.createdAt).format('HH:mm')
|
||||
|
||||
// Extract the required information
|
||||
const userPayment = {
|
||||
_id: payment._id,
|
||||
project_title: project.title,
|
||||
project_id: project._id,
|
||||
payment_date: shamsiDate, // Format date as YYYY-MM-DD
|
||||
payment_time: shamsiTime,
|
||||
iban: project.creator_id ? project.creator_id.iban : null, // Check if creator_id exists before accessing iban
|
||||
amount: payment.amount,
|
||||
payment_status: payment.status
|
||||
}
|
||||
|
||||
return userPayment
|
||||
}))
|
||||
|
||||
// Filter out null values
|
||||
const validUserPayments = userPayments.filter(payment => payment !== null)
|
||||
if (financial.docs.length) {
|
||||
const projectIds = financial.docs
|
||||
.map((payment) => payment.project_id)
|
||||
.filter(Boolean)
|
||||
const projects = projectIds.length
|
||||
? await ProjectModel.find({ _id: { $in: projectIds } }).select('title').lean()
|
||||
: []
|
||||
const titleById = new Map(projects.map((p) => [String(p._id), p.title]))
|
||||
financial.docs = financial.docs.map((payment) => {
|
||||
const doc = typeof payment.toObject === 'function' ? payment.toObject() : payment
|
||||
return {
|
||||
...doc,
|
||||
project_title: doc.project_id ? titleById.get(String(doc.project_id)) : undefined
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Send the payments back to the client
|
||||
res.status(200).json({
|
||||
|
||||
@@ -89,8 +89,8 @@ const getUserOffers = async (req, res, next) => {
|
||||
const userId = decodedToken.id
|
||||
const { page = 1, limit = 10, status_filter, id } = req.query // افزودن پارامترهای صفحهبندی به درخواست
|
||||
let filter = {}
|
||||
let filterbyid = {receiver: id}
|
||||
let filterbyidsender = {sender: id}
|
||||
const filterbyid = { receiver: id }
|
||||
const filterbyidsender = { sender: id }
|
||||
if (status_filter === 'درخواست') {
|
||||
filter = { sender: userId }
|
||||
} else {
|
||||
@@ -116,9 +116,9 @@ const getUserOffers = async (req, res, next) => {
|
||||
|
||||
// دریافت آفرهای صفحهبندی شده
|
||||
let offer = await OfferModel.paginate(filter, options)
|
||||
let offerbyid = await OfferModel.paginate(filterbyid, options)
|
||||
let offerbyidsender = await OfferModel.paginate(filterbyidsender, options)
|
||||
|
||||
const offerbyid = await OfferModel.paginate(filterbyid, options)
|
||||
const offerbyidsender = await OfferModel.paginate(filterbyidsender, options)
|
||||
|
||||
const totalPages = offer.totalPages
|
||||
const totalItems = offer.totalDocs
|
||||
|
||||
@@ -224,7 +224,7 @@ const handleSuccessfulOfferPayment = async (req, res) => {
|
||||
error: true,
|
||||
message: canUseFreeCreation(user)
|
||||
? 'سهمیه درخواست رایگان ماهانه شما تمام شده است.'
|
||||
: freeCreationLimitResponse().message,
|
||||
: freeCreationLimitResponse().message
|
||||
})
|
||||
} else {
|
||||
// Retrieve offer price based on type
|
||||
@@ -352,7 +352,8 @@ const createOfferComment = async (req, res, next) => {
|
||||
return res.status(400).send({ message: 'Rating must be between 1 and 5' })
|
||||
}
|
||||
|
||||
const commentStatus = resolveCommentModerationStatus(comment)
|
||||
const creatorUser = await UserModel.findById(userId).select('verify_badge').lean()
|
||||
const commentStatus = resolveCommentModerationStatus(comment, creatorUser?.verify_badge)
|
||||
|
||||
const newComment = new CommentModel({
|
||||
user: user_id,
|
||||
|
||||
@@ -368,6 +368,64 @@ const confirmReceipt = async (req, res, next) => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* لغو سفارش توسط خریدار — فقط تا وقتی وضعیت "on_hold" (در انتظار بررسی) باشه.
|
||||
* پرداخت مستقیم از درگاه زرینپال میاد، نه از کیفپول (initiateOrderPayment) — و
|
||||
* releaseOrderFunds فقط موقع completed شدنِ سفارش کیفپولِ فروشنده رو شارژ میکنه، پس
|
||||
* توی وضعیتِ on_hold هنوز هیچ مبلغی به فروشنده واریز نشده (walletReleased=false) —
|
||||
* چیزی برای «کسر از فروشنده» وجود نداره. کلِ مبلغ (سهمِ فروشنده + کارمزد، یعنی
|
||||
* total_amount) مستقیم به کیفپولِ خریدار (نه بازگشت به کارت/درگاه) واریز میشه —
|
||||
* دقیقاً همون چیزی که کاربر خواسته: «مبلغ به کیف پول شما منتقل خواهد شد»
|
||||
*/
|
||||
const cancelOrder = async (req, res, next) => {
|
||||
try {
|
||||
const order = await OrderModel.findById(req.params.id)
|
||||
if (!order) return orderError(res, 404, 'سفارش یافت نشد')
|
||||
if (String(order.buyer) !== String(req.user._id)) {
|
||||
return orderError(res, 403, 'دسترسی به این سفارش ندارید')
|
||||
}
|
||||
if (order.status !== 'on_hold') {
|
||||
return orderError(res, 400, 'این سفارش در وضعیتی نیست که قابل کنسل کردن باشد')
|
||||
}
|
||||
|
||||
order.status = 'cancelled'
|
||||
order.cancelReason = 'لغو توسط خریدار'
|
||||
await order.save()
|
||||
|
||||
await WalletTransactionModel.create({
|
||||
user: order.buyer,
|
||||
bucket: 'shop',
|
||||
shop: order.shop,
|
||||
type: 'credit',
|
||||
amount: order.total_amount,
|
||||
referenceType: 'order',
|
||||
referenceId: order._id,
|
||||
description: `بازگشت وجه سفارش کنسلشده ${order._id} به کیف پول`
|
||||
})
|
||||
|
||||
await notifyUser(req.app.get('io'), order.buyer, {
|
||||
project_post_id: order._id,
|
||||
type: 'shop_order',
|
||||
title: 'کنسل شدن سفارش',
|
||||
description: `سفارش شما کنسل شد و مبلغ ${order.total_amount.toLocaleString()} تومان به کیف پول شما واریز شد.`
|
||||
})
|
||||
|
||||
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: `سفارش ${order._id} توسط خریدار کنسل شد.`
|
||||
})
|
||||
}
|
||||
|
||||
return res.json({ message: 'سفارش کنسل شد', order: serializeOrder(order) })
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
const rateOrder = async (req, res, next) => {
|
||||
try {
|
||||
const order = await OrderModel.findById(req.params.id)
|
||||
@@ -476,6 +534,7 @@ module.exports = {
|
||||
updateTrackingCode,
|
||||
getOrderById,
|
||||
confirmReceipt,
|
||||
cancelOrder,
|
||||
rateOrder,
|
||||
reportOrder,
|
||||
createReturnRequest,
|
||||
|
||||
@@ -12,15 +12,29 @@ const AdvertisingModel = require('../../../models/AdvertisingModel')
|
||||
const {
|
||||
canUseFreeCreation,
|
||||
consumeFreeCreation,
|
||||
freeCreationLimitResponse,
|
||||
freeCreationLimitResponse
|
||||
} = require('../../../utils/freeCreationLimit')
|
||||
const {
|
||||
PROJECT_ACCEPT_RATE,
|
||||
PROJECT_COMPLETION_COMMISSION_RATE,
|
||||
creditProjectShare
|
||||
} = require('../../../utils/projectCommission')
|
||||
const { isVerifiedBadge } = require('../../../utils/verifyBadge')
|
||||
|
||||
const apiKey = process.env.MERCHENT_CODE // Replace with your ZarinPal API Key
|
||||
const apiKey = process.env.ZARINPAL_MERCHANT_ID || process.env.MERCHENT_CODE // Replace with your ZarinPal API Key
|
||||
|
||||
// درگاه قدیمیِ REST زرینپال (/pg/rest/WebGate) منسوخ شده — مبلغ به API نسخهی v4
|
||||
// باید به ریال ارسال بشه، نه تومان (برخلاف API قدیمی)
|
||||
function toRial (tomanAmount) {
|
||||
return Number(tomanAmount) * 10
|
||||
}
|
||||
|
||||
// کاربرِ بدون تیک وریفای طلایی/سبز/آبی باید از پنل ادمین تایید بشه تا پروژه/ویترینِ
|
||||
// پرداختشدهش مستقیم توی اپ/وب نمایش داده نشه
|
||||
async function resolveApprovalStatus (userId, acceptedStatus, pendingStatus = 'pending') {
|
||||
const user = await UserModel.findById(userId).select('verify_badge').lean()
|
||||
return isVerifiedBadge(user?.verify_badge) ? acceptedStatus : pendingStatus
|
||||
}
|
||||
|
||||
// Function to get price based on item name from database
|
||||
const getPriceFromDatabase = async (itemName) => {
|
||||
@@ -68,28 +82,31 @@ const initiatePayment = async (req, res) => {
|
||||
return
|
||||
}
|
||||
project.payment_status = 'done' // Set project status to 'done'
|
||||
project.status = 'temp_accept' // 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: 'پروژه رایگان با موفقیت ایجاد شد', projectId, type: 'free' })
|
||||
} else {
|
||||
res.status(403).json({ message: 'شما قبلاً از درخواست رایگان امروز استفاده کردهاید.' })
|
||||
}
|
||||
} else {
|
||||
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?projectId=${projectId}&itemName=${itemName}&userId=${userId}` // Assuming your backend URL for handling payment callback
|
||||
})
|
||||
const response = await axios.post(
|
||||
'https://api.zarinpal.com/pg/v4/payment/request.json',
|
||||
{
|
||||
merchant_id: apiKey,
|
||||
amount: toRial(price),
|
||||
description: `پرداخت برای سفارش شماره ${projectId}`,
|
||||
callback_url: `${process.env.APP_URL}/projects/payment?projectId=${projectId}&itemName=${itemName}&userId=${userId}` // Assuming your backend URL for handling payment callback
|
||||
},
|
||||
{ headers: { 'Content-Type': 'application/json' } }
|
||||
)
|
||||
|
||||
const data = response.data
|
||||
if (data.Status === 100) {
|
||||
const result = response.data?.data
|
||||
if (result?.code === 100 && result?.authority) {
|
||||
// Payment request was successful
|
||||
const authority = data.Authority // The payment authority (شناسه پرداخت)
|
||||
res.status(200).json({ authority })
|
||||
res.status(200).json({ authority: result.authority })
|
||||
} else {
|
||||
// Payment request failed
|
||||
console.error('Error occurred while initiating payment:', data)
|
||||
console.error('Error occurred while initiating payment:', response.data)
|
||||
res.status(500).json({ error: 'Error occurred while initiating payment' })
|
||||
}
|
||||
}
|
||||
@@ -112,16 +129,15 @@ const handlePaymentCallback = async (req, res) => {
|
||||
const userId = req.query.userId // Get project ID from session
|
||||
// Clear project ID from session
|
||||
// Query ZarinPal API to verify payment status
|
||||
const verificationUrl = 'https://www.zarinpal.com/pg/rest/WebGate/PaymentVerification.json'
|
||||
const price = await getPriceFromDatabase(itemName) // Get price from database
|
||||
const verificationResponse = await axios.post(verificationUrl, {
|
||||
MerchantID: apiKey,
|
||||
Authority: authority,
|
||||
Amount: Number(price)
|
||||
})
|
||||
const verificationResponse = await axios.post(
|
||||
'https://api.zarinpal.com/pg/v4/payment/verify.json',
|
||||
{ merchant_id: apiKey, authority, amount: toRial(price) },
|
||||
{ headers: { 'Content-Type': 'application/json' } }
|
||||
)
|
||||
|
||||
const verificationData = verificationResponse.data
|
||||
if (verificationData.Status === 100) {
|
||||
const verificationData = verificationResponse.data?.data
|
||||
if (verificationData?.code === 100 || verificationData?.code === 101) {
|
||||
const payment = new PaymentModel({
|
||||
amount: Number(price),
|
||||
status: 'successful',
|
||||
@@ -142,7 +158,7 @@ const handlePaymentCallback = async (req, res) => {
|
||||
return
|
||||
}
|
||||
project.payment_status = 'done' // Set project status to 'done'
|
||||
project.status = 'temp_accept' // Set project status to 'done'
|
||||
project.status = await resolveApprovalStatus(userId, 'temp_accept') // Set project status to 'done'
|
||||
await project.save()
|
||||
|
||||
// Send event based on payment status
|
||||
@@ -189,95 +205,6 @@ const handlePaymentCallback = async (req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Bazaar
|
||||
const handleSuccessfulPayment = async (req, res) => {
|
||||
try {
|
||||
const { projectId } = req.body
|
||||
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 project = await ProjectModel.findById(projectId)
|
||||
if (!project) {
|
||||
res.status(404).send('Project not found')
|
||||
return
|
||||
}
|
||||
|
||||
const price = await getPriceFromDatabase(project.project_type)
|
||||
|
||||
// Create a new payment entry
|
||||
const payment = new PaymentModel({
|
||||
amount: Number(price),
|
||||
status: 'successful',
|
||||
authority: '', // Add authority if available
|
||||
user_id: userId,
|
||||
project_id: projectId,
|
||||
type: 'create',
|
||||
installment_step: null
|
||||
})
|
||||
await payment.save()
|
||||
|
||||
// Update the project status to 'done' or 'paid'
|
||||
project.payment_status = 'done'
|
||||
project.status = 'temp_accept'
|
||||
await project.save()
|
||||
|
||||
// Emit a success event
|
||||
if ('eventEmitter' in req.app) {
|
||||
req.app.get('eventEmitter').emit('paymentSuccess', { message: 'Payment successful' })
|
||||
} else {
|
||||
console.error('Error: eventEmitter is not defined or not properly configured.')
|
||||
}
|
||||
|
||||
res.status(200).json({ message: 'Payment recorded successfully', projectId, price })
|
||||
} catch (error) {
|
||||
console.error('Error occurred:', error)
|
||||
res.status(500).json({ error: 'Internal server error' })
|
||||
}
|
||||
}
|
||||
// Controller function for handling failed payment
|
||||
const handleFailedPayment = async (req, res) => {
|
||||
try {
|
||||
const { projectId } = req.body
|
||||
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 project = await ProjectModel.findById(projectId)
|
||||
if (!project) {
|
||||
res.status(404).send('Project not found')
|
||||
return
|
||||
}
|
||||
|
||||
const price = await getPriceFromDatabase(project.project_type)
|
||||
|
||||
// Create a new payment entry
|
||||
const payment = new PaymentModel({
|
||||
amount: Number(price),
|
||||
status: 'failed',
|
||||
authority: '', // Add authority if available
|
||||
user_id: userId,
|
||||
project_id: projectId,
|
||||
type: 'create'
|
||||
})
|
||||
await payment.save()
|
||||
|
||||
// Emit a failure event
|
||||
if ('eventEmitter' in req.app) {
|
||||
req.app.get('eventEmitter').emit('paymentFailed', { message: 'Payment failed' })
|
||||
} else {
|
||||
console.error('Error: eventEmitter is not defined or not properly configured.')
|
||||
}
|
||||
|
||||
res.status(200).json({ message: 'Payment failed', projectId, price })
|
||||
} catch (error) {
|
||||
console.error('Error occurred:', error)
|
||||
res.status(500).json({ error: 'Internal server error' })
|
||||
}
|
||||
}
|
||||
|
||||
const paymentRequestStepOne = async (req, res) => {
|
||||
try {
|
||||
const token = req.header('Authorization').split(' ')[1]
|
||||
@@ -300,21 +227,24 @@ const paymentRequestStepOne = async (req, res) => {
|
||||
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(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
|
||||
})
|
||||
const response = await axios.post(
|
||||
'https://api.zarinpal.com/pg/v4/payment/request.json',
|
||||
{
|
||||
merchant_id: apiKey,
|
||||
amount: toRial(acceptAmount),
|
||||
description: `پرداخت برای سفارش شماره ${projectId}`,
|
||||
callback_url: `${process.env.APP_URL}/projects/payment-request-step-one-callback?projectId=${projectId}&requestId=${requestId}&userId=${userId}` // Callback URL including requestId
|
||||
},
|
||||
{ headers: { 'Content-Type': 'application/json' } }
|
||||
)
|
||||
|
||||
const data = response.data
|
||||
if (data.Status === 100) {
|
||||
const result = response.data?.data
|
||||
if (result?.code === 100 && result?.authority) {
|
||||
// Payment request was successful
|
||||
const authority = data.Authority // The payment authority
|
||||
res.status(200).json({ authority })
|
||||
res.status(200).json({ authority: result.authority })
|
||||
} else {
|
||||
// Payment request failed
|
||||
console.error('Error occurred while initiating payment:', data)
|
||||
console.error('Error occurred while initiating payment:', response.data)
|
||||
res.status(500).json({ error: 'Error occurred while initiating payment' })
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -341,21 +271,24 @@ const paymentRequestStepTwo = async (req, res) => {
|
||||
const totalAmount = project.final_price
|
||||
const secondInstallmentAmount = totalAmount / 4
|
||||
// Proceed with ZarinPal payment request for the second installment
|
||||
const response = await axios.post('https://www.zarinpal.com/pg/rest/WebGate/PaymentRequest.json', {
|
||||
MerchantID: apiKey,
|
||||
Amount: parseInt(secondInstallmentAmount + secondInstallmentAmount / 100 * 9),
|
||||
Description: `پرداخت برای سفارش شماره ${projectId}`,
|
||||
CallbackURL: `${process.env.APP_URL}/projects/payment-request-step-two-callback?projectId=${projectId}&userId=${userId}&installmentNumber=${installmentNumber}` // Callback URL
|
||||
})
|
||||
const response = await axios.post(
|
||||
'https://api.zarinpal.com/pg/v4/payment/request.json',
|
||||
{
|
||||
merchant_id: apiKey,
|
||||
amount: toRial(secondInstallmentAmount),
|
||||
description: `پرداخت برای سفارش شماره ${projectId}`,
|
||||
callback_url: `${process.env.APP_URL}/projects/payment-request-step-two-callback?projectId=${projectId}&userId=${userId}&installmentNumber=${installmentNumber}` // Callback URL
|
||||
},
|
||||
{ headers: { 'Content-Type': 'application/json' } }
|
||||
)
|
||||
|
||||
const data = response.data
|
||||
if (data.Status === 100) {
|
||||
const result = response.data?.data
|
||||
if (result?.code === 100 && result?.authority) {
|
||||
// Payment request was successful
|
||||
const authority = data.Authority // The payment authority
|
||||
res.status(200).json({ authority })
|
||||
res.status(200).json({ authority: result.authority })
|
||||
} else {
|
||||
// Payment request failed
|
||||
console.error('Error occurred while initiating payment:', data)
|
||||
console.error('Error occurred while initiating payment:', response.data)
|
||||
res.status(500).json({ error: 'Error occurred while initiating payment' })
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -383,21 +316,24 @@ const paymentRequestStepAll = async (req, res) => {
|
||||
const secondInstallmentAmount = totalAmount / 2
|
||||
|
||||
// Proceed with ZarinPal payment request for the second installment
|
||||
const response = await axios.post('https://www.zarinpal.com/pg/rest/WebGate/PaymentRequest.json', {
|
||||
MerchantID: apiKey,
|
||||
Amount: parseInt(secondInstallmentAmount + secondInstallmentAmount / 100 * 9),
|
||||
Description: `پرداخت برای سفارش شماره ${projectId}`,
|
||||
CallbackURL: `${process.env.APP_URL}/projects/payment-request-step-all-callback?projectId=${projectId}&userId=${userId}` // Callback URL
|
||||
})
|
||||
const response = await axios.post(
|
||||
'https://api.zarinpal.com/pg/v4/payment/request.json',
|
||||
{
|
||||
merchant_id: apiKey,
|
||||
amount: toRial(secondInstallmentAmount),
|
||||
description: `پرداخت برای سفارش شماره ${projectId}`,
|
||||
callback_url: `${process.env.APP_URL}/projects/payment-request-step-all-callback?projectId=${projectId}&userId=${userId}` // Callback URL
|
||||
},
|
||||
{ headers: { 'Content-Type': 'application/json' } }
|
||||
)
|
||||
|
||||
const data = response.data
|
||||
if (data.Status === 100) {
|
||||
const result = response.data?.data
|
||||
if (result?.code === 100 && result?.authority) {
|
||||
// Payment request was successful
|
||||
const authority = data.Authority // The payment authority
|
||||
res.status(200).json({ authority })
|
||||
res.status(200).json({ authority: result.authority })
|
||||
} else {
|
||||
// Payment request failed
|
||||
console.error('Error occurred while initiating payment:', data)
|
||||
console.error('Error occurred while initiating payment:', response.data)
|
||||
res.status(500).json({ error: 'Error occurred while initiating payment' })
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -415,20 +351,19 @@ const paymentRequestStepOneCallback = async (req, res) => {
|
||||
const userId = req.query.userId
|
||||
|
||||
// Verify payment status with ZarinPal
|
||||
const verificationUrl = 'https://www.zarinpal.com/pg/rest/WebGate/PaymentVerification.json'
|
||||
const request = await RequestModel.findById(requestId)
|
||||
if (!request) {
|
||||
return res.status(404).json({ error: 'Request not found' })
|
||||
}
|
||||
const price = Math.round(Number(request.price) * PROJECT_ACCEPT_RATE)
|
||||
const verificationResponse = await axios.post(verificationUrl, {
|
||||
MerchantID: apiKey,
|
||||
Authority: authority,
|
||||
Amount: parseInt(price + price / 100 * 9)
|
||||
})
|
||||
const verificationResponse = await axios.post(
|
||||
'https://api.zarinpal.com/pg/v4/payment/verify.json',
|
||||
{ merchant_id: apiKey, authority, amount: toRial(price) },
|
||||
{ headers: { 'Content-Type': 'application/json' } }
|
||||
)
|
||||
|
||||
const verificationData = verificationResponse.data
|
||||
if (verificationData.Status === 100) {
|
||||
const verificationData = verificationResponse.data?.data
|
||||
if (verificationData?.code === 100 || verificationData?.code === 101) {
|
||||
// Payment verification is successful
|
||||
const payment = new PaymentModel({
|
||||
amount: price,
|
||||
@@ -517,21 +452,20 @@ const paymentRequestStepTwoCallback = async (req, res) => {
|
||||
const installmentNumber = req.query.installmentNumber
|
||||
|
||||
// Verify payment status with ZarinPal
|
||||
const verificationUrl = 'https://www.zarinpal.com/pg/rest/WebGate/PaymentVerification.json'
|
||||
const project = await ProjectModel.findById(projectId)
|
||||
if (!project) {
|
||||
return res.status(404).json({ error: 'Project not found' })
|
||||
}
|
||||
const totalAmount = project.final_price
|
||||
const secondInstallmentAmount = totalAmount / 4
|
||||
const verificationResponse = await axios.post(verificationUrl, {
|
||||
MerchantID: apiKey,
|
||||
Authority: authority,
|
||||
Amount: parseInt(secondInstallmentAmount + secondInstallmentAmount / 100 * 9)
|
||||
})
|
||||
const verificationResponse = await axios.post(
|
||||
'https://api.zarinpal.com/pg/v4/payment/verify.json',
|
||||
{ merchant_id: apiKey, authority, amount: toRial(secondInstallmentAmount) },
|
||||
{ headers: { 'Content-Type': 'application/json' } }
|
||||
)
|
||||
|
||||
const verificationData = verificationResponse.data
|
||||
if (verificationData.Status === 100) {
|
||||
const verificationData = verificationResponse.data?.data
|
||||
if (verificationData?.code === 100 || verificationData?.code === 101) {
|
||||
// Payment verification is successful
|
||||
const payment = new PaymentModel({
|
||||
amount: secondInstallmentAmount,
|
||||
@@ -612,21 +546,20 @@ const paymentRequestStepAllCallback = async (req, res) => {
|
||||
const userId = req.query.userId
|
||||
|
||||
// Verify payment status with ZarinPal
|
||||
const verificationUrl = 'https://www.zarinpal.com/pg/rest/WebGate/PaymentVerification.json'
|
||||
const project = await ProjectModel.findById(projectId)
|
||||
if (!project) {
|
||||
return res.status(404).json({ error: 'Project not found' })
|
||||
}
|
||||
const totalAmount = project.final_price
|
||||
const secondInstallmentAmount = totalAmount / 2
|
||||
const verificationResponse = await axios.post(verificationUrl, {
|
||||
MerchantID: apiKey,
|
||||
Authority: authority,
|
||||
Amount: parseInt(secondInstallmentAmount + secondInstallmentAmount / 100 * 9)
|
||||
})
|
||||
const verificationResponse = await axios.post(
|
||||
'https://api.zarinpal.com/pg/v4/payment/verify.json',
|
||||
{ merchant_id: apiKey, authority, amount: toRial(secondInstallmentAmount) },
|
||||
{ headers: { 'Content-Type': 'application/json' } }
|
||||
)
|
||||
|
||||
const verificationData = verificationResponse.data
|
||||
if (verificationData.Status === 100) {
|
||||
const verificationData = verificationResponse.data?.data
|
||||
if (verificationData?.code === 100 || verificationData?.code === 101) {
|
||||
// Payment verification is successful
|
||||
const payment = new PaymentModel({
|
||||
amount: secondInstallmentAmount,
|
||||
@@ -726,27 +659,30 @@ const initiateAdvertisingPayment = async (req, res) => {
|
||||
return
|
||||
}
|
||||
project.payment_status = 'done' // Set project status to 'done'
|
||||
project.status = 'temp_accept' // 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' })
|
||||
} else {
|
||||
res.status(403).json({ message: 'شما قبلاً از درخواست رایگان امروز استفاده کردهاید.' })
|
||||
}
|
||||
} else {
|
||||
const response = await axios.post('https://www.zarinpal.com/pg/rest/WebGate/PaymentRequest.json', {
|
||||
MerchantID: apiKey,
|
||||
Amount: showDiscount === true || showDiscount === 'true' ? Number(price + 20000) : Number(price),
|
||||
Description: `پرداخت برای سفارش شماره ${advertisingId}`,
|
||||
CallbackURL: `${process.env.APP_URL}/advertising/payment?advertisingId=${advertisingId}&itemName=${itemName}&userId=${userId}&showDiscount=${showDiscount}` // Assuming your backend URL for handling payment callback
|
||||
})
|
||||
const data = response.data
|
||||
if (data.Status === 100) {
|
||||
const response = await axios.post(
|
||||
'https://api.zarinpal.com/pg/v4/payment/request.json',
|
||||
{
|
||||
merchant_id: apiKey,
|
||||
amount: toRial(showDiscount === true || showDiscount === 'true' ? Number(price + 20000) : Number(price)),
|
||||
description: `پرداخت برای سفارش شماره ${advertisingId}`,
|
||||
callback_url: `${process.env.APP_URL}/advertising/payment?advertisingId=${advertisingId}&itemName=${itemName}&userId=${userId}&showDiscount=${showDiscount}` // Assuming your backend URL for handling payment callback
|
||||
},
|
||||
{ headers: { 'Content-Type': 'application/json' } }
|
||||
)
|
||||
const result = response.data?.data
|
||||
if (result?.code === 100 && result?.authority) {
|
||||
// Payment request was successful
|
||||
const authority = data.Authority // The payment authority (شناسه پرداخت)
|
||||
res.status(200).json({ authority })
|
||||
res.status(200).json({ authority: result.authority })
|
||||
} else {
|
||||
// Payment request failed
|
||||
console.error('Error occurred while initiating payment:', data)
|
||||
console.error('Error occurred while initiating payment:', response.data)
|
||||
res.status(500).json({ error: 'Error occurred while initiating payment' })
|
||||
}
|
||||
}
|
||||
@@ -769,16 +705,19 @@ const handleAdvertisingPaymentCallback = async (req, res) => {
|
||||
// Clear project ID from session
|
||||
// Query ZarinPal API to verify payment status
|
||||
|
||||
const verificationUrl = 'https://www.zarinpal.com/pg/rest/WebGate/PaymentVerification.json'
|
||||
const price = await getAdvertisingPriceFromDatabase(itemName) // Get price from database
|
||||
const verificationResponse = await axios.post(verificationUrl, {
|
||||
MerchantID: apiKey,
|
||||
Authority: authority,
|
||||
Amount: showDiscount === true || showDiscount === 'true' ? Number(price + 20000) : Number(price)
|
||||
})
|
||||
const verificationResponse = await axios.post(
|
||||
'https://api.zarinpal.com/pg/v4/payment/verify.json',
|
||||
{
|
||||
merchant_id: apiKey,
|
||||
authority,
|
||||
amount: toRial(showDiscount === true || showDiscount === 'true' ? Number(price + 20000) : Number(price))
|
||||
},
|
||||
{ headers: { 'Content-Type': 'application/json' } }
|
||||
)
|
||||
|
||||
const verificationData = verificationResponse.data
|
||||
if (verificationData.Status === 100) {
|
||||
const verificationData = verificationResponse.data?.data
|
||||
if (verificationData?.code === 100 || verificationData?.code === 101) {
|
||||
const payment = new PaymentModel({
|
||||
amount: showDiscount === true || showDiscount === 'true' ? Number(price + 20000) : Number(price),
|
||||
status: 'successful',
|
||||
@@ -799,7 +738,7 @@ const handleAdvertisingPaymentCallback = async (req, res) => {
|
||||
return
|
||||
}
|
||||
advertising.payment_status = 'done' // Set advertising status to 'done'
|
||||
advertising.status = 'temp_accept' // Set advertising status to 'done'
|
||||
advertising.status = await resolveApprovalStatus(userId, 'temp_accept') // Set advertising status to 'done'
|
||||
await advertising.save()
|
||||
|
||||
// Send event based on payment status
|
||||
@@ -846,96 +785,6 @@ const handleAdvertisingPaymentCallback = async (req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Controller function for handling successful advertising payment
|
||||
const handleSuccessfulAdvertisingPayment = async (req, res) => {
|
||||
try {
|
||||
const { advertisingId } = req.body
|
||||
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 showDiscount = req.query.showDiscount // Get project ID from session
|
||||
|
||||
const advertising = await AdvertisingModel.findById(advertisingId)
|
||||
if (!advertising) {
|
||||
res.status(404).send('Advertising not found')
|
||||
return
|
||||
}
|
||||
|
||||
const price = await getAdvertisingPriceFromDatabase(advertising.type)
|
||||
|
||||
// Create a new payment entry
|
||||
const payment = new PaymentModel({
|
||||
amount: showDiscount === true || showDiscount === 'true' ? Number(price + 20000) : Number(price),
|
||||
status: 'successful',
|
||||
authority: '', // Add authority if available
|
||||
user_id: userId,
|
||||
advertising_id: advertisingId,
|
||||
type: 'advertising',
|
||||
installment_step: null
|
||||
})
|
||||
await payment.save()
|
||||
|
||||
// Update the advertising status to 'done' or 'paid'
|
||||
advertising.payment_status = 'done'
|
||||
advertising.status = 'temp_accept'
|
||||
await advertising.save()
|
||||
|
||||
// Emit a success event
|
||||
if ('eventEmitter' in req.app) {
|
||||
req.app.get('eventEmitter').emit('paymentSuccess', { message: 'Payment successful' })
|
||||
} else {
|
||||
console.error('Error: eventEmitter is not defined or not properly configured.')
|
||||
}
|
||||
|
||||
res.status(200).json({ message: 'Payment recorded successfully', advertisingId, price })
|
||||
} catch (error) {
|
||||
console.error('Error occurred:', error)
|
||||
res.status(500).json({ error: 'Internal server error' })
|
||||
}
|
||||
}
|
||||
// Controller function for handling failed advertising payment
|
||||
const handleFailedAdvertisingPayment = async (req, res) => {
|
||||
try {
|
||||
const { advertisingId } = req.body
|
||||
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 showDiscount = req.query.showDiscount // Get project ID from session
|
||||
const advertising = await AdvertisingModel.findById(advertisingId)
|
||||
if (!advertising) {
|
||||
res.status(404).send('Advertising not found')
|
||||
return
|
||||
}
|
||||
|
||||
const price = await getAdvertisingPriceFromDatabase(advertising.type)
|
||||
|
||||
// Create a new payment entry
|
||||
const payment = new PaymentModel({
|
||||
amount: showDiscount === true || showDiscount === 'true' ? Number(price + 20000) : Number(price),
|
||||
status: 'failed',
|
||||
authority: '', // Add authority if available
|
||||
user_id: userId,
|
||||
advertising_id: advertisingId,
|
||||
type: 'advertising'
|
||||
})
|
||||
await payment.save()
|
||||
|
||||
// Emit a failure event
|
||||
if ('eventEmitter' in req.app) {
|
||||
req.app.get('eventEmitter').emit('paymentFailed', { message: 'Payment failed' })
|
||||
} else {
|
||||
console.error('Error: eventEmitter is not defined or not properly configured.')
|
||||
}
|
||||
|
||||
res.status(200).json({ message: 'Payment failed', advertisingId, price })
|
||||
} catch (error) {
|
||||
console.error('Error occurred:', error)
|
||||
res.status(500).json({ error: 'Internal server error' })
|
||||
}
|
||||
}
|
||||
|
||||
const republishAdvertisingPayment = async (req, res) => {
|
||||
try {
|
||||
const token = req.header('Authorization').split(' ')[1]
|
||||
@@ -966,27 +815,30 @@ const republishAdvertisingPayment = async (req, res) => {
|
||||
return
|
||||
}
|
||||
project.payment_status = 'done' // Set project status to 'done'
|
||||
project.status = 'temp_accept' // 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' })
|
||||
} else {
|
||||
res.status(403).json({ message: 'شما قبلاً از درخواست رایگان امروز استفاده کردهاید.' })
|
||||
}
|
||||
} else {
|
||||
const response = await axios.post('https://www.zarinpal.com/pg/rest/WebGate/PaymentRequest.json', {
|
||||
MerchantID: apiKey,
|
||||
Amount: showDiscount === true || showDiscount === 'true' ? Number(((price + 20000) * 70) / 100) : Number(((price) * 70) / 100),
|
||||
Description: `پرداخت برای سفارش شماره ${advertisingId}`,
|
||||
CallbackURL: `${process.env.APP_URL}/advertising/republish-verify-payment?advertisingId=${advertisingId}&itemName=${itemName}&userId=${userId}&showDiscount=${showDiscount}` // Assuming your backend URL for handling payment callback
|
||||
})
|
||||
const data = response.data
|
||||
if (data.Status === 100) {
|
||||
const response = await axios.post(
|
||||
'https://api.zarinpal.com/pg/v4/payment/request.json',
|
||||
{
|
||||
merchant_id: apiKey,
|
||||
amount: toRial(showDiscount === true || showDiscount === 'true' ? Number(((price + 20000) * 70) / 100) : Number(((price) * 70) / 100)),
|
||||
description: `پرداخت برای سفارش شماره ${advertisingId}`,
|
||||
callback_url: `${process.env.APP_URL}/advertising/republish-verify-payment?advertisingId=${advertisingId}&itemName=${itemName}&userId=${userId}&showDiscount=${showDiscount}` // Assuming your backend URL for handling payment callback
|
||||
},
|
||||
{ headers: { 'Content-Type': 'application/json' } }
|
||||
)
|
||||
const result = response.data?.data
|
||||
if (result?.code === 100 && result?.authority) {
|
||||
// Payment request was successful
|
||||
const authority = data.Authority // The payment authority (شناسه پرداخت)
|
||||
res.status(200).json({ authority })
|
||||
res.status(200).json({ authority: result.authority })
|
||||
} else {
|
||||
// Payment request failed
|
||||
console.error('Error occurred while initiating payment:', data)
|
||||
console.error('Error occurred while initiating payment:', response.data)
|
||||
res.status(500).json({ error: 'Error occurred while initiating payment' })
|
||||
}
|
||||
}
|
||||
@@ -1008,16 +860,19 @@ const handleAdvertisingRepublishPaymentCallback = async (req, res) => {
|
||||
const showDiscount = req.query.showDiscount // Get project ID from session
|
||||
// Clear project ID from session
|
||||
// Query ZarinPal API to verify payment status
|
||||
const verificationUrl = 'https://www.zarinpal.com/pg/rest/WebGate/PaymentVerification.json'
|
||||
const price = await getAdvertisingPriceFromDatabase(itemName) // Get price from database
|
||||
const verificationResponse = await axios.post(verificationUrl, {
|
||||
MerchantID: apiKey,
|
||||
Authority: authority,
|
||||
Amount: showDiscount === true || showDiscount === 'true' ? Number(((price + 20000) * 70) / 100) : Number(((price) * 70) / 100)
|
||||
})
|
||||
const verificationResponse = await axios.post(
|
||||
'https://api.zarinpal.com/pg/v4/payment/verify.json',
|
||||
{
|
||||
merchant_id: apiKey,
|
||||
authority,
|
||||
amount: toRial(showDiscount === true || showDiscount === 'true' ? Number(((price + 20000) * 70) / 100) : Number(((price) * 70) / 100))
|
||||
},
|
||||
{ headers: { 'Content-Type': 'application/json' } }
|
||||
)
|
||||
|
||||
const verificationData = verificationResponse.data
|
||||
if (verificationData.Status === 100) {
|
||||
const verificationData = verificationResponse.data?.data
|
||||
if (verificationData?.code === 100 || verificationData?.code === 101) {
|
||||
const payment = new PaymentModel({
|
||||
amount: showDiscount === true || showDiscount === 'true' ? Number(((price + 20000) * 70) / 100) : Number(((price) * 70) / 100),
|
||||
status: 'successful',
|
||||
@@ -1038,7 +893,7 @@ const handleAdvertisingRepublishPaymentCallback = async (req, res) => {
|
||||
return
|
||||
}
|
||||
advertising.payment_status = 'done' // Set advertising status to 'done'
|
||||
advertising.status = 'accepted' // Set advertising status to 'done'
|
||||
advertising.status = await resolveApprovalStatus(userId, 'accepted') // Set advertising status to 'done'
|
||||
advertising.acceptedAt = new Date()
|
||||
await advertising.save()
|
||||
|
||||
@@ -1085,100 +940,6 @@ const handleAdvertisingRepublishPaymentCallback = async (req, res) => {
|
||||
res.status(500).send('Internal server error')
|
||||
}
|
||||
}
|
||||
// Controller function for handling successful republish advertising payment
|
||||
const handleSuccessfulAdvertisingRepublishPayment = async (req, res) => {
|
||||
try {
|
||||
const { advertisingId } = req.body
|
||||
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 showDiscount = req.query.showDiscount
|
||||
|
||||
const advertising = await AdvertisingModel.findById(advertisingId)
|
||||
if (!advertising) {
|
||||
res.status(404).send('Advertising not found')
|
||||
return
|
||||
}
|
||||
|
||||
const price = await getAdvertisingPriceFromDatabase(advertising.type)
|
||||
|
||||
// Create a new payment entry
|
||||
const payment = new PaymentModel({
|
||||
amount: showDiscount === true || showDiscount === 'true' ? Number((price + 20000) * 0.7) : Number(price * 0.7),
|
||||
status: 'successful',
|
||||
authority: '', // Add authority if available
|
||||
user_id: userId,
|
||||
advertising_id: advertisingId,
|
||||
type: 'advertising-republish',
|
||||
installment_step: null
|
||||
})
|
||||
await payment.save()
|
||||
|
||||
// Update the advertising status to 'done' or 'accepted'
|
||||
advertising.payment_status = 'done'
|
||||
advertising.status = 'accepted'
|
||||
advertising.acceptedAt = new Date()
|
||||
await advertising.save()
|
||||
|
||||
// Emit a success event
|
||||
if ('eventEmitter' in req.app) {
|
||||
req.app.get('eventEmitter').emit('paymentSuccess', { message: 'Republish payment successful' })
|
||||
} else {
|
||||
console.error('Error: eventEmitter is not defined or not properly configured.')
|
||||
}
|
||||
|
||||
res.status(200).json({ message: 'Republish payment recorded successfully', advertisingId, price })
|
||||
} catch (error) {
|
||||
console.error('Error occurred:', error)
|
||||
res.status(500).json({ error: 'Internal server error' })
|
||||
}
|
||||
}
|
||||
// Controller function for handling failed republish advertising payment
|
||||
const handleFailedAdvertisingRepublishPayment = async (req, res) => {
|
||||
try {
|
||||
const { advertisingId } = req.body
|
||||
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 showDiscount = req.query.showDiscount
|
||||
|
||||
const advertising = await AdvertisingModel.findById(advertisingId)
|
||||
if (!advertising) {
|
||||
res.status(404).send('Advertising not found')
|
||||
return
|
||||
}
|
||||
|
||||
const price = await getAdvertisingPriceFromDatabase(advertising.type)
|
||||
|
||||
// Create a new payment entry
|
||||
const payment = new PaymentModel({
|
||||
amount: showDiscount === true || showDiscount === 'true' ? Number((price + 20000) * 0.7) : Number(price * 0.7),
|
||||
status: 'failed',
|
||||
authority: '', // Add authority if available
|
||||
user_id: userId,
|
||||
advertising_id: advertisingId,
|
||||
type: 'advertising-republish'
|
||||
})
|
||||
await payment.save()
|
||||
|
||||
// Emit a failure event
|
||||
if ('eventEmitter' in req.app) {
|
||||
req.app.get('eventEmitter').emit('paymentFailed', { message: 'Republish payment failed' })
|
||||
} else {
|
||||
console.error('Error: eventEmitter is not defined or not properly configured.')
|
||||
}
|
||||
|
||||
res.status(200).json({ message: 'Republish payment failed', advertisingId, price })
|
||||
} catch (error) {
|
||||
console.error('Error occurred:', error)
|
||||
res.status(500).json({ error: 'Internal server error' })
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
initiatePayment,
|
||||
handlePaymentCallback,
|
||||
@@ -1191,11 +952,5 @@ module.exports = {
|
||||
initiateAdvertisingPayment,
|
||||
handleAdvertisingPaymentCallback,
|
||||
republishAdvertisingPayment,
|
||||
handleAdvertisingRepublishPaymentCallback,
|
||||
handleSuccessfulPayment,
|
||||
handleFailedPayment,
|
||||
handleSuccessfulAdvertisingPayment,
|
||||
handleFailedAdvertisingPayment,
|
||||
handleSuccessfulAdvertisingRepublishPayment,
|
||||
handleFailedAdvertisingRepublishPayment
|
||||
handleAdvertisingRepublishPaymentCallback
|
||||
}
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
/* eslint-disable eqeqeq */
|
||||
|
||||
const TypeAndPriceModel = require('../../../models/TypeAndPriceModel');
|
||||
const ProjectModel = require('../../../models/ProjectModel');
|
||||
const PaymentModel = require('../../../models/PaymentModel');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const RequestModel = require('../../../models/RequestModel');
|
||||
const NotificationModel = require('../../../models/NotificationModel');
|
||||
const UserModel = require('../../../models/UserModel');
|
||||
const AdvertisingTypeModel = require('../../../models/AdvertisingTypeModel');
|
||||
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 TypeAndPriceModel = require('../../../models/TypeAndPriceModel')
|
||||
const ProjectModel = require('../../../models/ProjectModel')
|
||||
const PaymentModel = require('../../../models/PaymentModel')
|
||||
const jwt = require('jsonwebtoken')
|
||||
const RequestModel = require('../../../models/RequestModel')
|
||||
const NotificationModel = require('../../../models/NotificationModel')
|
||||
const UserModel = require('../../../models/UserModel')
|
||||
const AdvertisingTypeModel = require('../../../models/AdvertisingTypeModel')
|
||||
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 { isVerifiedBadge } = require('../../../utils/verifyBadge')
|
||||
|
||||
const OFFER_COMMISSION_RATE = 0.3
|
||||
|
||||
@@ -22,30 +23,43 @@ const apiKey =
|
||||
process.env.MERCHENT_CODE ||
|
||||
'c7c41e8a-918f-4741-bcd5-58f3bc51db73'
|
||||
|
||||
// درگاه قدیمیِ REST زرینپال (/pg/rest/WebGate) منسوخ شده — مبلغ به API نسخهی v4
|
||||
// باید به ریال ارسال بشه، نه تومان (برخلاف API قدیمی)
|
||||
function toRial (tomanAmount) {
|
||||
return Number(tomanAmount) * 10
|
||||
}
|
||||
|
||||
// کاربرِ بدون تیک وریفای طلایی/سبز/آبی باید از پنل ادمین تایید بشه تا پروژه/ویترینِ
|
||||
// پرداختشدهش مستقیم توی اپ/وب نمایش داده نشه
|
||||
async function resolveApprovalStatus (userId, acceptedStatus, pendingStatus = 'pending') {
|
||||
const user = await UserModel.findById(userId).select('verify_badge').lean()
|
||||
return isVerifiedBadge(user?.verify_badge) ? acceptedStatus : pendingStatus
|
||||
}
|
||||
|
||||
// Function to get price based on item name from database
|
||||
const getPriceFromDatabase = async (itemName) => {
|
||||
const item = await TypeAndPriceModel.findOne({ name: itemName });
|
||||
const item = await TypeAndPriceModel.findOne({ name: itemName })
|
||||
if (!item) {
|
||||
throw new Error(`Item "${itemName}" not found in TypeAndPriceModel`);
|
||||
throw new Error(`Item "${itemName}" not found in TypeAndPriceModel`)
|
||||
}
|
||||
return item.price;
|
||||
};
|
||||
return item.price
|
||||
}
|
||||
|
||||
const getAdvertisingPriceFromDatabase = async (itemName) => {
|
||||
const item = await AdvertisingTypeModel.findOne({ name: itemName });
|
||||
const item = await AdvertisingTypeModel.findOne({ name: itemName })
|
||||
if (!item) {
|
||||
throw new Error(`Item "${itemName}" not found in AdvertisingTypeModel`);
|
||||
throw new Error(`Item "${itemName}" not found in AdvertisingTypeModel`)
|
||||
}
|
||||
return item.price;
|
||||
};
|
||||
return item.price
|
||||
}
|
||||
|
||||
const getOfferPriceFromDatabase = async (offerType) => {
|
||||
const item = await OfferTypeModel.findOne({ name: offerType });
|
||||
const item = await OfferTypeModel.findOne({ name: offerType })
|
||||
if (!item) {
|
||||
throw new Error(`Offer type "${offerType}" not found in OfferTypeModel`);
|
||||
throw new Error(`Offer type "${offerType}" not found in OfferTypeModel`)
|
||||
}
|
||||
return item.price;
|
||||
};
|
||||
return item.price
|
||||
}
|
||||
// Controller function for initiating payment
|
||||
const initiatePaymentWeb = async (req, res) => {
|
||||
try {
|
||||
@@ -99,7 +113,7 @@ const initiatePaymentWeb = async (req, res) => {
|
||||
return res.status(404).json({ error: true, message: 'پروژه یافت نشد' })
|
||||
}
|
||||
project.payment_status = 'done'
|
||||
project.status = 'temp_accept'
|
||||
project.status = await resolveApprovalStatus(userId, 'temp_accept')
|
||||
await project.save()
|
||||
return res.status(200).json({
|
||||
message: 'پروژه رایگان با موفقیت ایجاد شد',
|
||||
@@ -122,26 +136,26 @@ const initiatePaymentWeb = async (req, res) => {
|
||||
}
|
||||
|
||||
const response = await axios.post(
|
||||
'https://www.zarinpal.com/pg/rest/WebGate/PaymentRequest.json',
|
||||
'https://api.zarinpal.com/pg/v4/payment/request.json',
|
||||
{
|
||||
MerchantID: apiKey,
|
||||
Amount: Number(price),
|
||||
Description: `پرداخت برای سفارش شماره ${projectId}`,
|
||||
CallbackURL: `${callbackBase}/projects/payment-web?projectId=${projectId}&itemName=${encodeURIComponent(itemName)}&userId=${userId}`
|
||||
merchant_id: apiKey,
|
||||
amount: toRial(price),
|
||||
description: `پرداخت برای سفارش شماره ${projectId}`,
|
||||
callback_url: `${callbackBase}/projects/payment-web?projectId=${projectId}&itemName=${encodeURIComponent(itemName)}&userId=${userId}`
|
||||
},
|
||||
{ timeout: 20000 }
|
||||
{ headers: { 'Content-Type': 'application/json' }, timeout: 20000 }
|
||||
)
|
||||
|
||||
const data = response.data
|
||||
if (data.Status === 100 && data.Authority) {
|
||||
return res.status(200).json({ authority: data.Authority })
|
||||
const result = response.data?.data
|
||||
if (result?.code === 100 && result?.authority) {
|
||||
return res.status(200).json({ authority: result.authority })
|
||||
}
|
||||
|
||||
console.error('ZarinPal initiate error:', data)
|
||||
console.error('ZarinPal initiate error:', response.data)
|
||||
return res.status(502).json({
|
||||
error: true,
|
||||
message: 'درگاه پرداخت پاسخ نامعتبر داد. لطفاً دوباره تلاش کنید.',
|
||||
zarinpalStatus: data?.Status
|
||||
zarinpalStatus: result?.code
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('initiatePaymentWeb error:', error?.response?.data || error?.message || error)
|
||||
@@ -166,16 +180,15 @@ const handlePaymentCallbackWeb = async (req, res) => {
|
||||
const userId = req.query.userId // Get project ID from session
|
||||
// Clear project ID from session
|
||||
// Query ZarinPal API to verify payment status
|
||||
const verificationUrl = 'https://www.zarinpal.com/pg/rest/WebGate/PaymentVerification.json'
|
||||
const price = await getPriceFromDatabase(itemName) // Get price from database
|
||||
const verificationResponse = await axios.post(verificationUrl, {
|
||||
MerchantID: apiKey,
|
||||
Authority: authority,
|
||||
Amount: Number(price)
|
||||
})
|
||||
const verificationResponse = await axios.post(
|
||||
'https://api.zarinpal.com/pg/v4/payment/verify.json',
|
||||
{ merchant_id: apiKey, authority, amount: toRial(price) },
|
||||
{ headers: { 'Content-Type': 'application/json' } }
|
||||
)
|
||||
|
||||
const verificationData = verificationResponse.data
|
||||
if (verificationData.Status === 100) {
|
||||
const verificationData = verificationResponse.data?.data
|
||||
if (verificationData?.code === 100 || verificationData?.code === 101) {
|
||||
const payment = new PaymentModel({
|
||||
amount: Number(price),
|
||||
status: 'successful',
|
||||
@@ -196,7 +209,7 @@ const handlePaymentCallbackWeb = async (req, res) => {
|
||||
return
|
||||
}
|
||||
project.payment_status = 'done' // Set project status to 'done'
|
||||
project.status = 'temp_accept' // Set project status to 'done'
|
||||
project.status = await resolveApprovalStatus(userId, 'temp_accept') // Set project status to 'done'
|
||||
await project.save()
|
||||
|
||||
// Send event based on payment status
|
||||
@@ -288,25 +301,25 @@ const paymentRequestAcceptWeb = async (req, res) => {
|
||||
}
|
||||
|
||||
const response = await axios.post(
|
||||
'https://www.zarinpal.com/pg/rest/WebGate/PaymentRequest.json',
|
||||
'https://api.zarinpal.com/pg/v4/payment/request.json',
|
||||
{
|
||||
MerchantID: apiKey,
|
||||
Amount: acceptAmount,
|
||||
Description: `پرداخت پذیرش همکاری برای پروژه ${project.title || projectId}`,
|
||||
CallbackURL: `${callbackBase}/projects/payment-request-step-one-callback/web?projectId=${projectId}&requestId=${requestId}&userId=${userId}`
|
||||
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}`
|
||||
},
|
||||
{ timeout: 20000 }
|
||||
{ headers: { 'Content-Type': 'application/json' }, timeout: 20000 }
|
||||
)
|
||||
|
||||
const data = response.data
|
||||
if (data.Status === 100 && data.Authority) {
|
||||
const result = response.data?.data
|
||||
if (result?.code === 100 && result?.authority) {
|
||||
return res.status(200).json({
|
||||
authority: data.Authority,
|
||||
paymentUrl: `https://www.zarinpal.com/pg/StartPay/${data.Authority}`
|
||||
authority: result.authority,
|
||||
paymentUrl: `https://www.zarinpal.com/pg/StartPay/${result.authority}`
|
||||
})
|
||||
}
|
||||
|
||||
console.error('paymentRequestAcceptWeb ZarinPal error:', data)
|
||||
console.error('paymentRequestAcceptWeb ZarinPal error:', response.data)
|
||||
return res.status(502).json({ error: true, message: 'درگاه پرداخت پاسخ نامعتبر داد. لطفاً دوباره تلاش کنید.' })
|
||||
} catch (error) {
|
||||
console.error('paymentRequestAcceptWeb error:', error?.response?.data || error?.message || error)
|
||||
@@ -332,12 +345,13 @@ const paymentRequestAcceptWebCallback = async (req, res) => {
|
||||
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 }
|
||||
'https://api.zarinpal.com/pg/v4/payment/verify.json',
|
||||
{ merchant_id: apiKey, authority: Authority, amount: toRial(acceptAmount) },
|
||||
{ headers: { 'Content-Type': 'application/json' } }
|
||||
)
|
||||
|
||||
const verificationData = verificationResponse.data
|
||||
if (verificationData.Status !== 100 && verificationData.Status !== 101) {
|
||||
const verificationData = verificationResponse.data?.data
|
||||
if (verificationData?.code !== 100 && verificationData?.code !== 101) {
|
||||
const payment = new PaymentModel({
|
||||
amount: acceptAmount,
|
||||
status: 'failed',
|
||||
@@ -433,25 +447,25 @@ const initiateProjectCompletionPaymentWeb = async (req, res) => {
|
||||
}
|
||||
|
||||
const response = await axios.post(
|
||||
'https://www.zarinpal.com/pg/rest/WebGate/PaymentRequest.json',
|
||||
'https://api.zarinpal.com/pg/v4/payment/request.json',
|
||||
{
|
||||
MerchantID: apiKey,
|
||||
Amount: remainingAmount,
|
||||
Description: `پرداخت باقیمانده پروژه ${project.title || projectId}`,
|
||||
CallbackURL: `${callbackBase}/projects/payment-completion-callback/web?projectId=${projectId}&userId=${userId}`
|
||||
merchant_id: apiKey,
|
||||
amount: toRial(remainingAmount),
|
||||
description: `پرداخت باقیمانده پروژه ${project.title || projectId}`,
|
||||
callback_url: `${callbackBase}/projects/payment-completion-callback/web?projectId=${projectId}&userId=${userId}`
|
||||
},
|
||||
{ timeout: 20000 }
|
||||
{ headers: { 'Content-Type': 'application/json' }, timeout: 20000 }
|
||||
)
|
||||
|
||||
const data = response.data
|
||||
if (data.Status === 100 && data.Authority) {
|
||||
const result = response.data?.data
|
||||
if (result?.code === 100 && result?.authority) {
|
||||
return res.status(200).json({
|
||||
authority: data.Authority,
|
||||
paymentUrl: `https://www.zarinpal.com/pg/StartPay/${data.Authority}`
|
||||
authority: result.authority,
|
||||
paymentUrl: `https://www.zarinpal.com/pg/StartPay/${result.authority}`
|
||||
})
|
||||
}
|
||||
|
||||
console.error('initiateProjectCompletionPaymentWeb ZarinPal error:', data)
|
||||
console.error('initiateProjectCompletionPaymentWeb ZarinPal error:', response.data)
|
||||
return res.status(502).json({ error: true, message: 'درگاه پرداخت پاسخ نامعتبر داد. لطفاً دوباره تلاش کنید.' })
|
||||
} catch (error) {
|
||||
console.error('initiateProjectCompletionPaymentWeb error:', error?.response?.data || error?.message || error)
|
||||
@@ -476,12 +490,13 @@ const handleProjectCompletionPaymentCallbackWeb = async (req, res) => {
|
||||
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 }
|
||||
'https://api.zarinpal.com/pg/v4/payment/verify.json',
|
||||
{ merchant_id: apiKey, authority: Authority, amount: toRial(remainingAmount) },
|
||||
{ headers: { 'Content-Type': 'application/json' } }
|
||||
)
|
||||
|
||||
const verificationData = verificationResponse.data
|
||||
if (verificationData.Status !== 100 && verificationData.Status !== 101) {
|
||||
const verificationData = verificationResponse.data?.data
|
||||
if (verificationData?.code !== 100 && verificationData?.code !== 101) {
|
||||
const payment = new PaymentModel({
|
||||
amount: remainingAmount,
|
||||
status: 'failed',
|
||||
@@ -551,27 +566,30 @@ const initiateAdvertisingPaymentWeb = async (req, res) => {
|
||||
return
|
||||
}
|
||||
project.payment_status = 'done' // Set project status to 'done'
|
||||
project.status = 'temp_accept' // 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' })
|
||||
} else {
|
||||
res.status(403).json({ message: 'شما قبلاً از بیلبورد رایگان این ماه استفاده کردهاید.' })
|
||||
}
|
||||
} else {
|
||||
const response = await axios.post('https://www.zarinpal.com/pg/rest/WebGate/PaymentRequest.json', {
|
||||
MerchantID: process.env.ZARINPAL_MERCHANT_ID,
|
||||
Amount: showDiscount === true || showDiscount === 'true' ? Number(price + 20000) : Number(price),
|
||||
Description: `پرداخت برای سفارش شماره ${advertisingId}`,
|
||||
CallbackURL: `${process.env.APP_SITE_CallBack}/advertising/payment-web?advertisingId=${advertisingId}&itemName=${itemName}&userId=${userId}&showDiscount=${showDiscount}` // Assuming your backend URL for handling payment callback
|
||||
})
|
||||
const data = response.data
|
||||
if (data.Status === 100) {
|
||||
const response = await axios.post(
|
||||
'https://api.zarinpal.com/pg/v4/payment/request.json',
|
||||
{
|
||||
merchant_id: process.env.ZARINPAL_MERCHANT_ID,
|
||||
amount: toRial(showDiscount === true || showDiscount === 'true' ? Number(price + 20000) : Number(price)),
|
||||
description: `پرداخت برای سفارش شماره ${advertisingId}`,
|
||||
callback_url: `${process.env.APP_SITE_CallBack}/advertising/payment-web?advertisingId=${advertisingId}&itemName=${itemName}&userId=${userId}&showDiscount=${showDiscount}` // Assuming your backend URL for handling payment callback
|
||||
},
|
||||
{ headers: { 'Content-Type': 'application/json' } }
|
||||
)
|
||||
const result = response.data?.data
|
||||
if (result?.code === 100 && result?.authority) {
|
||||
// Payment request was successful
|
||||
const authority = data.Authority // The payment authority (شناسه پرداخت)
|
||||
res.status(200).json({ authority })
|
||||
res.status(200).json({ authority: result.authority })
|
||||
} else {
|
||||
// Payment request failed
|
||||
console.error('Error occurred while initiating payment:', data)
|
||||
console.error('Error occurred while initiating payment:', response.data)
|
||||
res.status(500).json({ error: 'Error occurred while initiating payment' })
|
||||
}
|
||||
}
|
||||
@@ -594,16 +612,19 @@ const handleAdvertisingPaymentCallbackWeb = async (req, res) => {
|
||||
// Clear project ID from session
|
||||
// Query ZarinPal API to verify payment status
|
||||
|
||||
const verificationUrl = 'https://www.zarinpal.com/pg/rest/WebGate/PaymentVerification.json'
|
||||
const price = await getAdvertisingPriceFromDatabase(itemName) // Get price from database
|
||||
const verificationResponse = await axios.post(verificationUrl, {
|
||||
MerchantID: apiKey,
|
||||
Authority: authority,
|
||||
Amount: showDiscount === true || showDiscount === 'true' ? Number(price + 20000) : Number(price)
|
||||
})
|
||||
const verificationResponse = await axios.post(
|
||||
'https://api.zarinpal.com/pg/v4/payment/verify.json',
|
||||
{
|
||||
merchant_id: apiKey,
|
||||
authority,
|
||||
amount: toRial(showDiscount === true || showDiscount === 'true' ? Number(price + 20000) : Number(price))
|
||||
},
|
||||
{ headers: { 'Content-Type': 'application/json' } }
|
||||
)
|
||||
|
||||
const verificationData = verificationResponse.data
|
||||
if (verificationData.Status === 100) {
|
||||
const verificationData = verificationResponse.data?.data
|
||||
if (verificationData?.code === 100 || verificationData?.code === 101) {
|
||||
const payment = new PaymentModel({
|
||||
amount: showDiscount === true || showDiscount === 'true' ? Number(price + 20000) : Number(price),
|
||||
status: 'successful',
|
||||
@@ -624,7 +645,7 @@ const handleAdvertisingPaymentCallbackWeb = async (req, res) => {
|
||||
return
|
||||
}
|
||||
advertising.payment_status = 'done' // Set advertising status to 'done'
|
||||
advertising.status = 'temp_accept' // Set advertising status to 'done'
|
||||
advertising.status = await resolveApprovalStatus(userId, 'temp_accept') // Set advertising status to 'done'
|
||||
await advertising.save()
|
||||
|
||||
// Send event based on payment status
|
||||
@@ -699,27 +720,30 @@ const republishAdvertisingPaymentWeb = async (req, res) => {
|
||||
return
|
||||
}
|
||||
project.payment_status = 'done' // Set project status to 'done'
|
||||
project.status = 'temp_accept' // 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' })
|
||||
} else {
|
||||
res.status(403).json({ message: 'شما قبلاً از درخواست رایگان امروز استفاده کردهاید.' })
|
||||
}
|
||||
} else {
|
||||
const response = await axios.post('https://www.zarinpal.com/pg/rest/WebGate/PaymentRequest.json', {
|
||||
MerchantID: apiKey,
|
||||
Amount: showDiscount === true || showDiscount === 'true' ? Number(((price + 20000) * 70) / 100) : Number(((price) * 70) / 100),
|
||||
Description: `پرداخت برای سفارش شماره ${advertisingId}`,
|
||||
CallbackURL: `${process.env.APP_URL}/advertising/republish-verify-payment-web?advertisingId=${advertisingId}&itemName=${itemName}&userId=${userId}&showDiscount=${showDiscount}` // Assuming your backend URL for handling payment callback
|
||||
})
|
||||
const data = response.data
|
||||
if (data.Status === 100) {
|
||||
const response = await axios.post(
|
||||
'https://api.zarinpal.com/pg/v4/payment/request.json',
|
||||
{
|
||||
merchant_id: apiKey,
|
||||
amount: toRial(showDiscount === true || showDiscount === 'true' ? Number(((price + 20000) * 70) / 100) : Number(((price) * 70) / 100)),
|
||||
description: `پرداخت برای سفارش شماره ${advertisingId}`,
|
||||
callback_url: `${process.env.APP_URL}/advertising/republish-verify-payment-web?advertisingId=${advertisingId}&itemName=${itemName}&userId=${userId}&showDiscount=${showDiscount}` // Assuming your backend URL for handling payment callback
|
||||
},
|
||||
{ headers: { 'Content-Type': 'application/json' } }
|
||||
)
|
||||
const result = response.data?.data
|
||||
if (result?.code === 100 && result?.authority) {
|
||||
// Payment request was successful
|
||||
const authority = data.Authority // The payment authority (شناسه پرداخت)
|
||||
res.status(200).json({ authority })
|
||||
res.status(200).json({ authority: result.authority })
|
||||
} else {
|
||||
// Payment request failed
|
||||
console.error('Error occurred while initiating payment:', data)
|
||||
console.error('Error occurred while initiating payment:', response.data)
|
||||
res.status(500).json({ error: 'Error occurred while initiating payment' })
|
||||
}
|
||||
}
|
||||
@@ -741,16 +765,19 @@ const handleAdvertisingRepublishPaymentCallbackWeb = async (req, res) => {
|
||||
const showDiscount = req.query.showDiscount // Get project ID from session
|
||||
// Clear project ID from session
|
||||
// Query ZarinPal API to verify payment status
|
||||
const verificationUrl = 'https://www.zarinpal.com/pg/rest/WebGate/PaymentVerification.json'
|
||||
const price = await getAdvertisingPriceFromDatabase(itemName) // Get price from database
|
||||
const verificationResponse = await axios.post(verificationUrl, {
|
||||
MerchantID: apiKey,
|
||||
Authority: authority,
|
||||
Amount: showDiscount === true || showDiscount === 'true' ? Number(((price + 20000) * 70) / 100) : Number(((price) * 70) / 100)
|
||||
})
|
||||
const verificationResponse = await axios.post(
|
||||
'https://api.zarinpal.com/pg/v4/payment/verify.json',
|
||||
{
|
||||
merchant_id: apiKey,
|
||||
authority,
|
||||
amount: toRial(showDiscount === true || showDiscount === 'true' ? Number(((price + 20000) * 70) / 100) : Number(((price) * 70) / 100))
|
||||
},
|
||||
{ headers: { 'Content-Type': 'application/json' } }
|
||||
)
|
||||
|
||||
const verificationData = verificationResponse.data
|
||||
if (verificationData.Status === 100) {
|
||||
const verificationData = verificationResponse.data?.data
|
||||
if (verificationData?.code === 100 || verificationData?.code === 101) {
|
||||
const payment = new PaymentModel({
|
||||
amount: showDiscount === true || showDiscount === 'true' ? Number(((price + 20000) * 70) / 100) : Number(((price) * 70) / 100),
|
||||
status: 'successful',
|
||||
@@ -771,7 +798,7 @@ const handleAdvertisingRepublishPaymentCallbackWeb = async (req, res) => {
|
||||
return
|
||||
}
|
||||
advertising.payment_status = 'done' // Set advertising status to 'done'
|
||||
advertising.status = 'accepted' // Set advertising status to 'done'
|
||||
advertising.status = await resolveApprovalStatus(userId, 'accepted') // Set advertising status to 'done'
|
||||
advertising.acceptedAt = new Date()
|
||||
await advertising.save()
|
||||
|
||||
@@ -818,121 +845,38 @@ const handleAdvertisingRepublishPaymentCallbackWeb = async (req, res) => {
|
||||
res.status(500).send('Internal server error')
|
||||
}
|
||||
}
|
||||
// const initiateOfferPaymentWeb = async (req, res) => {
|
||||
// try {
|
||||
// const token = req.header('Authorization')?.split(' ')[1];
|
||||
// if (!token) {
|
||||
// return res.status(401).json({ error: 'Access Denied: No token provided' });
|
||||
// }
|
||||
|
||||
// let decodedToken;
|
||||
// try {
|
||||
// decodedToken = jwt.verify(token, process.env.APP_SECRET);
|
||||
// } catch (err) {
|
||||
// return res.status(401).json({ error: 'Invalid token' });
|
||||
// }
|
||||
|
||||
// const userId = decodedToken.id;
|
||||
// const { receiverId, offerType } = req.body;
|
||||
|
||||
// // اعتبارسنجی ورودیها
|
||||
// if (!receiverId || !offerType) {
|
||||
// return res.status(400).json({ error: 'receiverId and offerType are required' });
|
||||
// }
|
||||
|
||||
// const user = await UserModel.findById(userId);
|
||||
// if (!user) {
|
||||
// return res.status(404).json({ error: 'User not found' });
|
||||
// }
|
||||
|
||||
// const receiverUser = await UserModel.findById(receiverId);
|
||||
// if (!receiverUser) {
|
||||
// return res.status(404).json({ error: 'Receiver not found' });
|
||||
// }
|
||||
|
||||
// let price;
|
||||
// if (user.monthly_free_offer > 0 && offerType === 'free') {
|
||||
// price = 0;
|
||||
// user.monthly_free_offer -= 1;
|
||||
// await user.save();
|
||||
// } else {
|
||||
// price = await getOfferPriceFromDatabase(offerType);
|
||||
// }
|
||||
|
||||
// if (price === 0) {
|
||||
// const offer = new OfferModel({
|
||||
// sender: userId,
|
||||
// receiver: receiverId,
|
||||
// transaction_id: null,
|
||||
// });
|
||||
// await offer.save();
|
||||
// return res.status(200).json({ message: 'آفر رایگان با موفقیت ثبت شد', offerId: offer._id, type: 'free' });
|
||||
// }
|
||||
// console.log('🔹 Initiating ZarinPal Payment');
|
||||
// console.log('MerchantID:', apiKey);
|
||||
// console.log('Amount:', Number(price * 10));
|
||||
// console.log('Description:', `پرداخت برای ارسال آفر به ${receiverUser.first_name}`);
|
||||
// console.log('CallbackURL:', `${process.env.APP_URL}/offers/payment-web?receiverId=${receiverId}&offerType=${offerType}&userId=${userId}`);
|
||||
|
||||
// const ZARINPAL_URL = 'https://api.zarinpal.com/pg/v4/payment/request.json';
|
||||
// //
|
||||
// const response = await axios.post(ZARINPAL_URL, {
|
||||
// merchant_id: process.env.ZARINPAL_MERCHANT_ID, // ❌ MerchantID نیست!
|
||||
// amount: Number(price), // ✅ به ریال
|
||||
// description: `پرداخت برای ارسال آفر به ${receiverUser.first_name}`,
|
||||
// callback_url: `${process.env.APP_SITE_CallBack}/offer/payment-web?receiverId=${receiverId}&offerType=${offerType}&userId=${userId}`,
|
||||
// }, { headers: { 'Content-Type': 'application/json' } });
|
||||
|
||||
// const result = response.data.data; // ✅ data.data
|
||||
|
||||
// if (result.code === 100) { // ✅ code === 100
|
||||
// return res.status(200).json({
|
||||
// authority: result.authority,
|
||||
// paymentUrl: `https://www.zarinpal.com/pg/StartPay/${result.authority}`
|
||||
// });
|
||||
// } else {
|
||||
// console.error('ZarinPal error:', data);
|
||||
// return res.status(500).json({ error: `Failed to initiate payment: ZarinPal Status ${data.Status}` });
|
||||
// }
|
||||
// } catch (error) {
|
||||
// console.error('❌ Error in initiateOfferPaymentWeb:', error.message, error.stack);
|
||||
// return res.status(500).json({ error: error.message || 'Internal server error' });
|
||||
// }
|
||||
// }
|
||||
const initiateOfferPaymentWeb = async (req, res) => {
|
||||
try {
|
||||
const token = req.header('Authorization')?.split(' ')[1];
|
||||
if (!token) return res.status(401).json({ error: 'No token' });
|
||||
const token = req.header('Authorization')?.split(' ')[1]
|
||||
if (!token) return res.status(401).json({ error: 'No token' })
|
||||
|
||||
const decoded = jwt.verify(token, process.env.APP_SECRET);
|
||||
const userId = decoded.id;
|
||||
const decoded = jwt.verify(token, process.env.APP_SECRET)
|
||||
const userId = decoded.id
|
||||
|
||||
const { receiverId, offerType, platform } = req.body;
|
||||
if (!receiverId || !offerType)
|
||||
return res.status(400).json({ error: 'Invalid data' });
|
||||
const { receiverId, offerType, platform } = 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 user = await UserModel.findById(userId)
|
||||
const receiver = await UserModel.findById(receiverId)
|
||||
if (!user || !receiver) { return res.status(404).json({ error: 'User not found' }) }
|
||||
|
||||
let price = await getOfferPriceFromDatabase(offerType); // ✅ ریال
|
||||
const price = await getOfferPriceFromDatabase(offerType) // ✅ ریال
|
||||
|
||||
// 🎁 free offer
|
||||
if (offerType === 'free' && user.monthly_free_offer > 0) {
|
||||
const offer = await OfferModel.create({
|
||||
sender: userId,
|
||||
receiver: receiverId,
|
||||
transaction_id: null,
|
||||
});
|
||||
transaction_id: null
|
||||
})
|
||||
|
||||
user.monthly_free_offer -= 1;
|
||||
await user.save();
|
||||
user.monthly_free_offer -= 1
|
||||
await user.save()
|
||||
|
||||
return res.json({
|
||||
type: 'free',
|
||||
offerId: offer._id,
|
||||
});
|
||||
offerId: offer._id
|
||||
})
|
||||
}
|
||||
|
||||
// 🎁 گیرنده «دریافت پرداختی از چت» رو غیرفعال کرده — درخواست همکاری براش همیشه
|
||||
@@ -941,23 +885,23 @@ const initiateOfferPaymentWeb = async (req, res) => {
|
||||
const offer = await OfferModel.create({
|
||||
sender: userId,
|
||||
receiver: receiverId,
|
||||
transaction_id: null,
|
||||
});
|
||||
transaction_id: null
|
||||
})
|
||||
|
||||
return res.json({
|
||||
type: 'free',
|
||||
offerId: offer._id,
|
||||
});
|
||||
offerId: offer._id
|
||||
})
|
||||
}
|
||||
|
||||
const amount = Number(price) * 10; // ✅ ریال – بدون ×10
|
||||
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 amount = Number(price) * 10 // ✅ ریال – بدون ×10
|
||||
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',
|
||||
{
|
||||
merchant_id:"c7c41e8a-918f-4741-bcd5-58f3bc51db73",
|
||||
merchant_id: 'c7c41e8a-918f-4741-bcd5-58f3bc51db73',
|
||||
amount,
|
||||
description: `ارسال آفر به ${receiver.first_name}`,
|
||||
callback_url: callbackUrl
|
||||
@@ -965,216 +909,73 @@ const initiateOfferPaymentWeb = async (req, res) => {
|
||||
// callback_url: `${process.env.APP_SITE_CallBack}/offers/payment-web?receiverId=${receiverId}&offerType=${offerType}&userId=${userId}`,
|
||||
},
|
||||
{ headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
)
|
||||
|
||||
// console.log(`${process.env.APP_SITE_CallBack}/offers/payment-web?receiverId=${receiverId}&offerType=${offerType}&userId=${userId}`);
|
||||
|
||||
|
||||
const result = zarinpalRes.data.data;
|
||||
console.log(result);
|
||||
console.log("ZarinPal Callback URL:", callbackUrl);
|
||||
console.log("Initiate payment request:", { userId, receiverId, offerType, price });
|
||||
|
||||
|
||||
|
||||
|
||||
const result = zarinpalRes.data.data
|
||||
console.log(result)
|
||||
console.log('ZarinPal Callback URL:', callbackUrl)
|
||||
console.log('Initiate payment request:', { userId, receiverId, offerType, price })
|
||||
|
||||
if (result.code === 100) {
|
||||
return res.json({
|
||||
authority: result.authority,
|
||||
paymentUrl: `https://www.zarinpal.com/pg/StartPay/${result.authority}`,
|
||||
});
|
||||
paymentUrl: `https://www.zarinpal.com/pg/StartPay/${result.authority}`
|
||||
})
|
||||
}
|
||||
|
||||
return res.status(500).json({ error: 'ZarinPal error' });
|
||||
return res.status(500).json({ error: 'ZarinPal error' })
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return res.status(500).json({ error: 'Server error' });
|
||||
console.error(err)
|
||||
return res.status(500).json({ error: 'Server error' })
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
// try {
|
||||
// const { receiverId, offerType, userId, Authority, Status } = req.query;
|
||||
|
||||
// // ❗ چک اولیه
|
||||
// if (Status !== 'OK' || !Authority) {
|
||||
// return res.status(400).json({
|
||||
// success: false,
|
||||
// message: 'پرداخت ناموفق یا لغو شد'
|
||||
// });
|
||||
// }
|
||||
|
||||
// if (!receiverId || !offerType || !userId) {
|
||||
// return res.status(400).json({
|
||||
// success: false,
|
||||
// message: 'پارامترهای ناقص'
|
||||
// });
|
||||
// }
|
||||
|
||||
// // پیدا کردن کاربرها
|
||||
// const user = await UserModel.findById(userId);
|
||||
// const receiverUser = await UserModel.findById(receiverId);
|
||||
|
||||
// if (!user || !receiverUser) {
|
||||
// return res.status(404).json({
|
||||
// success: false,
|
||||
// message: 'کاربر یافت نشد'
|
||||
// });
|
||||
// }
|
||||
|
||||
// const price = await getOfferPriceFromDatabase(offerType);
|
||||
|
||||
// const verifyResponse = await axios.post(
|
||||
// 'https://api.zarinpal.com/pg/v4/payment/verify.json',
|
||||
// {
|
||||
// merchant_id: process.env.ZARINPAL_MERCHANT_ID, // یا MERCHENT_CODE
|
||||
// authority: Authority,
|
||||
// amount: price, // به ریال
|
||||
// },
|
||||
// { headers: { 'Content-Type': 'application/json' } }
|
||||
// );
|
||||
|
||||
// const verifyData = verifyResponse.data.data;
|
||||
|
||||
// if (verifyData.code === 100) {
|
||||
// // ✅ پرداخت موفق
|
||||
// const payment = new PaymentModel({
|
||||
// amount: Number(price),
|
||||
// status: 'successful',
|
||||
// authority: Authority,
|
||||
// ref_id: verifyData.ref_id,
|
||||
// user_id: userId,
|
||||
// type: 'offer',
|
||||
// });
|
||||
// await payment.save();
|
||||
|
||||
// const offer = new OfferModel({
|
||||
// sender: userId,
|
||||
// receiver: receiverId,
|
||||
// transaction_id: payment._id,
|
||||
// });
|
||||
// await offer.save();
|
||||
|
||||
// // نوتیفیکیشن و SMS (همون کد قبلی)
|
||||
// const notification = new NotificationModel({
|
||||
// user_id: receiverId,
|
||||
// project_post_id: offer._id,
|
||||
// type: 'new_offer',
|
||||
// title: 'درخواست همکاری جدید',
|
||||
// description: `${user.first_name} یک درخواست همکاری برای شما ارسال کرده است`,
|
||||
// });
|
||||
// await notification.save();
|
||||
|
||||
// const data = JSON.stringify({
|
||||
// mobile: receiverUser?.mobile,
|
||||
// templateId: '569006',
|
||||
// parameters: [
|
||||
// { name: 'FIRSTNAME', value: receiverUser?.first_name },
|
||||
// { name: 'LASTNAME', value: receiverUser?.last_name },
|
||||
// ],
|
||||
// });
|
||||
|
||||
// const config = {
|
||||
// method: 'post',
|
||||
// url: 'https://api.sms.ir/v1/send/verify',
|
||||
// headers: {
|
||||
// 'Content-Type': 'application/json',
|
||||
// Accept: 'text/plain',
|
||||
// 'x-api-key': process.env.SMS_VERIFY_KEY,
|
||||
// },
|
||||
// data,
|
||||
// };
|
||||
|
||||
// await axios(config).catch((error) => {
|
||||
// console.error('SMS error:', error.message);
|
||||
// });
|
||||
|
||||
// if ('eventEmitter' in req.app && status === 'OK') {
|
||||
// req.app.get('eventEmitter').emit('paymentSuccess', { message: 'Payment successful' });
|
||||
// } else {
|
||||
// req.app.get('eventEmitter')?.emit('paymentFailed', { message: 'Payment canceled or failed' });
|
||||
// }
|
||||
|
||||
// res.redirect(`https://modstagram.com/offer/payment/success?userId=${userId}`);
|
||||
|
||||
// return res.status(200).json({
|
||||
// success: true,
|
||||
// message: 'پرداخت و آفر با موفقیت ثبت شد',
|
||||
// ref_id: verifyData.ref_id,
|
||||
// offerId: offer._id
|
||||
// });
|
||||
// } else {
|
||||
// // پرداخت ناموفق
|
||||
// const payment = new PaymentModel({
|
||||
// amount: Number(price),
|
||||
// status: 'failed',
|
||||
// authority: Authority,
|
||||
// user_id: userId,
|
||||
// type: 'offer',
|
||||
// });
|
||||
// await payment.save();
|
||||
// res.redirect(`https://modstagram.com/offer/payment/failed?userId=${userId}`);
|
||||
// return res.status(400).json({
|
||||
// success: false,
|
||||
// message: `خطا در تأیید پرداخت: ${verifyData.code}`
|
||||
// });
|
||||
|
||||
// }
|
||||
// } catch (error) {
|
||||
// console.error('❌ Callback Error:', error.message);
|
||||
// return res.status(500).json({
|
||||
// success: false,
|
||||
// message: 'خطای سرور'
|
||||
// });
|
||||
// }
|
||||
// };
|
||||
}
|
||||
|
||||
const handleOfferPaymentCallback = async (req, res) => {
|
||||
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
||||
res.setHeader('Access-Control-Allow-Origin', '*')
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,OPTIONS')
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')
|
||||
|
||||
console.log('🔔 CALLBACK QUERY:', req.query);
|
||||
console.log('🔔 CALLBACK QUERY:', req.query)
|
||||
|
||||
const isAndroid = req.query.platform === 'android';
|
||||
const isAndroid = req.query.platform === 'android'
|
||||
|
||||
try {
|
||||
const { Authority, Status, receiverId, offerType, userId } = req.query;
|
||||
const { Authority, Status, receiverId, offerType, userId } = req.query
|
||||
|
||||
if (Status !== 'OK')
|
||||
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')
|
||||
: res.redirect(`${process.env.APP_SITE}/offer/payment/failed`)
|
||||
}
|
||||
|
||||
const price = await getOfferPriceFromDatabase(offerType); // ریال
|
||||
const amount = Number(price) * 10;
|
||||
const price = await getOfferPriceFromDatabase(offerType) // ریال
|
||||
const amount = Number(price) * 10
|
||||
|
||||
const verify = await axios.post(
|
||||
'https://api.zarinpal.com/pg/v4/payment/verify.json',
|
||||
{
|
||||
merchant_id: "c7c41e8a-918f-4741-bcd5-58f3bc51db73",
|
||||
merchant_id: 'c7c41e8a-918f-4741-bcd5-58f3bc51db73',
|
||||
authority: Authority,
|
||||
amount,
|
||||
amount
|
||||
},
|
||||
{ headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
)
|
||||
|
||||
const data = verify.data.data;
|
||||
console.log('🔔payment:', data.code);
|
||||
const data = verify.data.data
|
||||
console.log('🔔payment:', data.code)
|
||||
if (data.code === 100) {
|
||||
const payment = await PaymentModel.create({
|
||||
amount : (amount / 10),
|
||||
amount: (amount / 10),
|
||||
status: 'successful',
|
||||
authority: Authority,
|
||||
ref_id: data.ref_id,
|
||||
user_id: userId,
|
||||
type: 'offer',
|
||||
});
|
||||
type: 'offer'
|
||||
})
|
||||
|
||||
console.log('🔔payment:', payment);
|
||||
console.log('🔔payment:', payment)
|
||||
|
||||
const offerPriceToman = amount / 10
|
||||
const receiverShare = Math.round(offerPriceToman * OFFER_COMMISSION_RATE)
|
||||
@@ -1185,31 +986,26 @@ const handleOfferPaymentCallback = async (req, res) => {
|
||||
sender: userId,
|
||||
receiver: receiverId,
|
||||
transaction_id: payment._id,
|
||||
pendingCommissionAmount: receiverShare,
|
||||
});
|
||||
pendingCommissionAmount: receiverShare
|
||||
})
|
||||
|
||||
return isAndroid
|
||||
? res.redirect(`modstagram://SuccessPay?type=offer&offerId=${offer._id}`)
|
||||
: res.redirect(
|
||||
`${process.env.APP_SITE}/offer/payment/success?offerId=${offer._id}`
|
||||
);
|
||||
`${process.env.APP_SITE}/offer/payment/success?offerId=${offer._id}`
|
||||
)
|
||||
}
|
||||
|
||||
return isAndroid
|
||||
? res.redirect(`modstagram://FailedPay?type=offer`)
|
||||
: res.redirect(`${process.env.APP_SITE}/offer/payment/failed`);
|
||||
? res.redirect('modstagram://FailedPay?type=offer')
|
||||
: res.redirect(`${process.env.APP_SITE}/offer/payment/failed`)
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
console.error(err)
|
||||
return isAndroid
|
||||
? res.redirect(`modstagram://FailedPay?type=offer`)
|
||||
: res.redirect(`${process.env.APP_SITE}/offer/payment/failed`);
|
||||
? res.redirect('modstagram://FailedPay?type=offer')
|
||||
: res.redirect(`${process.env.APP_SITE}/offer/payment/failed`)
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
initiatePaymentWeb,
|
||||
@@ -1223,5 +1019,5 @@ module.exports = {
|
||||
republishAdvertisingPaymentWeb,
|
||||
handleAdvertisingRepublishPaymentCallbackWeb,
|
||||
initiateOfferPaymentWeb,
|
||||
handleOfferPaymentCallback,
|
||||
};
|
||||
handleOfferPaymentCallback
|
||||
}
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
/* eslint-disable camelcase */
|
||||
const PostModel = require('../../../models/PostModel');
|
||||
const UserModel = require('../../../models/UserModel');
|
||||
const CourseModel = require('../../../models/CourseModel');
|
||||
const ProjectModel = require('../../../models/ProjectModel');
|
||||
const AdvertisingModel = require('../../../models/AdvertisingModel');
|
||||
const ShopModel = require('../../../models/ShopModel');
|
||||
const ShopProductListingModel = require('../../../models/ShopProductListingModel');
|
||||
const { CountryModel, ProvinceModel, CityModel } = require('../../../models/StateCity');
|
||||
const { resolveLinkedEntity } = require('../../../utils/resolveLinkedEntity');
|
||||
const { check, validationResult } = require('express-validator');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const path = require('path');
|
||||
const { default: mongoose } = require('mongoose');
|
||||
const { viewerIsBlockedBy } = require('../../../utils/blockVisibility');
|
||||
const { attachSinglePostEngagement } = require('../../../utils/postEngagement');
|
||||
const { isHiddenFromPublic } = require('../../../utils/accountLifecycle');
|
||||
const { viewerCanSeeUserContent } = require('../../../utils/privateProfile');
|
||||
const { isPostPubliclyVisible } = require('../../../utils/postModeration');
|
||||
const { postStatusFilter, PUBLIC_POST_STATUSES, commentStatusFilter } = require('../../../utils/moderationStatus');
|
||||
const { resolvePostModerationStatus } = require('../../../utils/contentModeration');
|
||||
const { savePostWithModerationStatus } = require('../../../utils/postStatusSave');
|
||||
const { createTagNotifications } = require('../../../utils/tagNotification');
|
||||
const { notifyFollowers } = require('../../../utils/notifyFollowers');
|
||||
const PostModel = require('../../../models/PostModel')
|
||||
const UserModel = require('../../../models/UserModel')
|
||||
const CourseModel = require('../../../models/CourseModel')
|
||||
const ProjectModel = require('../../../models/ProjectModel')
|
||||
const AdvertisingModel = require('../../../models/AdvertisingModel')
|
||||
const ShopModel = require('../../../models/ShopModel')
|
||||
const ShopProductListingModel = require('../../../models/ShopProductListingModel')
|
||||
const { CountryModel, ProvinceModel, CityModel } = require('../../../models/StateCity')
|
||||
const { resolveLinkedEntity } = require('../../../utils/resolveLinkedEntity')
|
||||
const { check, validationResult } = require('express-validator')
|
||||
const jwt = require('jsonwebtoken')
|
||||
const path = require('path')
|
||||
const { default: mongoose } = require('mongoose')
|
||||
const { viewerIsBlockedBy } = require('../../../utils/blockVisibility')
|
||||
const { attachSinglePostEngagement } = require('../../../utils/postEngagement')
|
||||
const { isHiddenFromPublic } = require('../../../utils/accountLifecycle')
|
||||
const { viewerCanSeeUserContent } = require('../../../utils/privateProfile')
|
||||
const { isPostPubliclyVisible } = require('../../../utils/postModeration')
|
||||
const { postStatusFilter, PUBLIC_POST_STATUSES, commentStatusFilter } = require('../../../utils/moderationStatus')
|
||||
const { resolvePostModerationStatus } = require('../../../utils/contentModeration')
|
||||
const { savePostWithModerationStatus } = require('../../../utils/postStatusSave')
|
||||
const { createTagNotifications } = require('../../../utils/tagNotification')
|
||||
const { notifyFollowers } = require('../../../utils/notifyFollowers')
|
||||
|
||||
let processPostVideoInBackground = async () => {};
|
||||
let processPostVideoInBackground = async () => {}
|
||||
try {
|
||||
({ processPostVideoInBackground } = require('../../../utils/postVideoProcessing'));
|
||||
({ processPostVideoInBackground } = require('../../../utils/postVideoProcessing'))
|
||||
} catch (error) {
|
||||
console.warn('[postController] postVideoProcessing unavailable:', error.message);
|
||||
console.warn('[postController] postVideoProcessing unavailable:', error.message)
|
||||
}
|
||||
|
||||
const createPostValidationRules = () => {
|
||||
@@ -35,81 +35,81 @@ const createPostValidationRules = () => {
|
||||
check('caption')
|
||||
.optional({ nullable: true })
|
||||
.isLength({ max: 2000 })
|
||||
.withMessage('کپشن حداکثر ۲۰۰۰ کاراکتر'),
|
||||
];
|
||||
};
|
||||
.withMessage('کپشن حداکثر ۲۰۰۰ کاراکتر')
|
||||
]
|
||||
}
|
||||
|
||||
const createPost = async (req, res) => {
|
||||
try {
|
||||
console.log('DEBUG: createPost called', {
|
||||
filesCount: Array.isArray(req.files) ? req.files.length : 0,
|
||||
hasCaption: Boolean(req.body?.caption),
|
||||
userId: String(req.user?._id || req.user?.id || ''),
|
||||
});
|
||||
userId: String(req.user?._id || req.user?.id || '')
|
||||
})
|
||||
|
||||
const { caption } = req.body;
|
||||
const files = req.files || [];
|
||||
const userId = req.user?._id || req.user?.id;
|
||||
const { caption } = req.body
|
||||
const files = req.files || []
|
||||
const userId = req.user?._id || req.user?.id
|
||||
if (!userId) {
|
||||
return res.status(401).json({ error: true, message: 'توکن ارائه نشده است' });
|
||||
return res.status(401).json({ error: true, message: 'توکن ارائه نشده است' })
|
||||
}
|
||||
|
||||
let taggedUserIds = [];
|
||||
let taggedUserIds = []
|
||||
if (req.body.tagged_user_ids) {
|
||||
const raw = req.body.tagged_user_ids;
|
||||
let parsed = [];
|
||||
const raw = req.body.tagged_user_ids
|
||||
let parsed = []
|
||||
try {
|
||||
parsed = Array.isArray(raw) ? raw : (typeof raw === 'string' ? JSON.parse(raw) : []);
|
||||
parsed = Array.isArray(raw) ? raw : (typeof raw === 'string' ? JSON.parse(raw) : [])
|
||||
} catch {
|
||||
parsed = [];
|
||||
parsed = []
|
||||
}
|
||||
taggedUserIds = parsed
|
||||
.filter((id) => mongoose.Types.ObjectId.isValid(id))
|
||||
.slice(0, 50);
|
||||
.slice(0, 50)
|
||||
}
|
||||
|
||||
// اعتبارسنجی همهی آیتمهای قابللینک (پکیج/پروژه/بیلبورد/فروشگاه/کالا/رزرو) از یک تابع
|
||||
// مشترک — همون شرط مالکیت+وضعیت که قبلاً اینجا پراکنده بود، حالا با استوری هم به اشتراک
|
||||
// گذاشته میشه (utils/resolveLinkedEntity.js)
|
||||
const linkedCourseId = await resolveLinkedEntity('pkg', req.body.linked_course_id, userId);
|
||||
const linkedProjectId = await resolveLinkedEntity('project', req.body.linked_project_id, userId);
|
||||
const linkedBillboardId = await resolveLinkedEntity('billboard', req.body.linked_billboard_id, userId);
|
||||
const linkedShopId = await resolveLinkedEntity('shop', req.body.linked_shop_id, userId);
|
||||
const linkedShopProductId = await resolveLinkedEntity('shopproduct', req.body.linked_shop_product_id, userId);
|
||||
const linkedBookingId = await resolveLinkedEntity('booking', req.body.linked_booking_id, userId);
|
||||
const linkedCourseId = await resolveLinkedEntity('pkg', req.body.linked_course_id, userId)
|
||||
const linkedProjectId = await resolveLinkedEntity('project', req.body.linked_project_id, userId)
|
||||
const linkedBillboardId = await resolveLinkedEntity('billboard', req.body.linked_billboard_id, userId)
|
||||
const linkedShopId = await resolveLinkedEntity('shop', req.body.linked_shop_id, userId)
|
||||
const linkedShopProductId = await resolveLinkedEntity('shopproduct', req.body.linked_shop_product_id, userId)
|
||||
const linkedBookingId = await resolveLinkedEntity('booking', req.body.linked_booking_id, userId)
|
||||
|
||||
if (!files || files.length === 0) {
|
||||
console.log('DEBUG: No files provided');
|
||||
return res.status(400).json({ error: true, message: 'هیچ فایلی آپلود نشده است' });
|
||||
console.log('DEBUG: No files provided')
|
||||
return res.status(400).json({ error: true, message: 'هیچ فایلی آپلود نشده است' })
|
||||
}
|
||||
|
||||
const captionText =
|
||||
caption != null && String(caption).trim()
|
||||
? String(caption).trim().slice(0, 2000)
|
||||
: null;
|
||||
: null
|
||||
|
||||
const filePaths = files.map((file) => ({
|
||||
path: file.path,
|
||||
type: (file.mimetype || file.type || '').startsWith('video/') ? 'video' : 'image',
|
||||
}));
|
||||
type: (file.mimetype || file.type || '').startsWith('video/') ? 'video' : 'image'
|
||||
}))
|
||||
|
||||
const postMediaType = filePaths.some((file) => file.type === 'video')
|
||||
? 'video'
|
||||
: 'image';
|
||||
: 'image'
|
||||
|
||||
const moderation = resolvePostModerationStatus({ caption: captionText, filePaths });
|
||||
const author = await UserModel.findById(userId)
|
||||
.select('post_location_enabled lat lng verify_badge')
|
||||
.lean()
|
||||
|
||||
const moderation = resolvePostModerationStatus({ caption: captionText, filePaths, verifyBadge: author?.verify_badge })
|
||||
if (moderation.status === 'reject') {
|
||||
return res.status(422).json({
|
||||
error: true,
|
||||
message: 'متن پست با قوانین همخوانی ندارد و ثبت نشد.',
|
||||
});
|
||||
message: 'متن پست با قوانین همخوانی ندارد و ثبت نشد.'
|
||||
})
|
||||
}
|
||||
|
||||
console.log('DEBUG: Saving post to MongoDB, files:', filePaths.length);
|
||||
|
||||
const author = await UserModel.findById(userId)
|
||||
.select('post_location_enabled lat lng')
|
||||
.lean();
|
||||
console.log('DEBUG: Saving post to MongoDB, files:', filePaths.length)
|
||||
|
||||
const postPayload = {
|
||||
user_id: userId,
|
||||
@@ -117,26 +117,26 @@ const createPost = async (req, res) => {
|
||||
files: filePaths,
|
||||
type: postMediaType,
|
||||
tagged_users: taggedUserIds,
|
||||
status: moderation.status,
|
||||
};
|
||||
status: moderation.status
|
||||
}
|
||||
|
||||
if (linkedCourseId) {
|
||||
postPayload.linked_course_id = linkedCourseId;
|
||||
postPayload.linked_course_id = linkedCourseId
|
||||
}
|
||||
if (linkedProjectId) {
|
||||
postPayload.linked_project_id = linkedProjectId;
|
||||
postPayload.linked_project_id = linkedProjectId
|
||||
}
|
||||
if (linkedBillboardId) {
|
||||
postPayload.linked_billboard_id = linkedBillboardId;
|
||||
postPayload.linked_billboard_id = linkedBillboardId
|
||||
}
|
||||
if (linkedShopId) {
|
||||
postPayload.linked_shop_id = linkedShopId;
|
||||
postPayload.linked_shop_id = linkedShopId
|
||||
}
|
||||
if (linkedShopProductId) {
|
||||
postPayload.linked_shop_product_id = linkedShopProductId;
|
||||
postPayload.linked_shop_product_id = linkedShopProductId
|
||||
}
|
||||
if (linkedBookingId) {
|
||||
postPayload.linked_booking_id = linkedBookingId;
|
||||
postPayload.linked_booking_id = linkedBookingId
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -146,77 +146,77 @@ const createPost = async (req, res) => {
|
||||
String(author.lat).trim() &&
|
||||
String(author.lng).trim()
|
||||
) {
|
||||
postPayload.lat = String(author.lat);
|
||||
postPayload.lng = String(author.lng);
|
||||
postPayload.lat = String(author.lat)
|
||||
postPayload.lng = String(author.lng)
|
||||
}
|
||||
|
||||
// کشور/شهر/استان پست — اختیاری، جدا از موقعیت پروفایل کاربر؛ کاربر خودش موقع ثبت پست انتخاب میکنه
|
||||
if (req.body.country_id) {
|
||||
const countryFind = await CountryModel.findOne({ id: req.body.country_id }, { _id: 0 });
|
||||
const countryFind = await CountryModel.findOne({ id: req.body.country_id }, { _id: 0 })
|
||||
if (countryFind) {
|
||||
postPayload.country = { id: countryFind.id, name: countryFind.name_fa, iso2: countryFind.iso2, slug: countryFind.slug };
|
||||
postPayload.country = { id: countryFind.id, name: countryFind.name_fa, iso2: countryFind.iso2, slug: countryFind.slug }
|
||||
}
|
||||
}
|
||||
if (req.body.province_id) {
|
||||
const provinceFind = await ProvinceModel.findOne({ id: req.body.province_id }, { _id: 0 });
|
||||
const provinceFind = await ProvinceModel.findOne({ id: req.body.province_id }, { _id: 0 })
|
||||
if (provinceFind) {
|
||||
postPayload.province = provinceFind;
|
||||
postPayload.province = provinceFind
|
||||
}
|
||||
}
|
||||
if (req.body.city_id) {
|
||||
const cityFind = await CityModel.findOne({ id: req.body.city_id });
|
||||
const cityFind = await CityModel.findOne({ id: req.body.city_id })
|
||||
if (cityFind) {
|
||||
postPayload.city = cityFind;
|
||||
postPayload.city = cityFind
|
||||
}
|
||||
}
|
||||
|
||||
const post = await savePostWithModerationStatus(PostModel, postPayload);
|
||||
console.log('DEBUG: Post saved:', post._id);
|
||||
const post = await savePostWithModerationStatus(PostModel, postPayload)
|
||||
console.log('DEBUG: Post saved:', post._id)
|
||||
|
||||
if (taggedUserIds.length > 0) {
|
||||
await createTagNotifications({
|
||||
taggerId: userId,
|
||||
taggedUserIds,
|
||||
postId: post._id
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
notifyFollowers(userId, post).catch((err) => {
|
||||
console.error('[notifyFollowers] background error:', err.message);
|
||||
});
|
||||
console.error('[notifyFollowers] background error:', err.message)
|
||||
})
|
||||
|
||||
const firstVideo = filePaths.find((file) => file.type === 'video');
|
||||
const firstVideo = filePaths.find((file) => file.type === 'video')
|
||||
if (firstVideo?.path) {
|
||||
processPostVideoInBackground(firstVideo.path, post._id).catch((err) => {
|
||||
console.error('[postVideoProcessing] background error:', err.message);
|
||||
});
|
||||
console.error('[postVideoProcessing] background error:', err.message)
|
||||
})
|
||||
}
|
||||
|
||||
res.status(201).json({
|
||||
message: 'پست ثبت شد و در حالت تایید موقت نمایش داده میشود',
|
||||
postId: post._id,
|
||||
status: post.status,
|
||||
});
|
||||
status: post.status
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('DEBUG: createPost error:', err.message);
|
||||
console.error('DEBUG: createPost error:', err.message)
|
||||
if (err?.name === 'ValidationError') {
|
||||
return res.status(500).json({
|
||||
error: true,
|
||||
message: 'خطا در ثبت پست. مدل پست روی سرور بهروز نیست.',
|
||||
});
|
||||
message: 'خطا در ثبت پست. مدل پست روی سرور بهروز نیست.'
|
||||
})
|
||||
}
|
||||
res.status(500).json({ error: true, message: `خطا در سرور: ${err.message}` });
|
||||
res.status(500).json({ error: true, message: `خطا در سرور: ${err.message}` })
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function toPublicFilePath(filePath) {
|
||||
if (!filePath || typeof filePath !== 'string') return filePath;
|
||||
let s = filePath.replace(/\\/g, '/');
|
||||
const storageIdx = s.indexOf('/storage/');
|
||||
function toPublicFilePath (filePath) {
|
||||
if (!filePath || typeof filePath !== 'string') return filePath
|
||||
const s = filePath.replace(/\\/g, '/')
|
||||
const storageIdx = s.indexOf('/storage/')
|
||||
if (storageIdx >= 0) {
|
||||
return s.slice(storageIdx + '/storage'.length);
|
||||
return s.slice(storageIdx + '/storage'.length)
|
||||
}
|
||||
return s;
|
||||
return s
|
||||
}
|
||||
|
||||
const updatePost = async (req, res) => {
|
||||
@@ -231,148 +231,145 @@ const updatePost = async (req, res) => {
|
||||
}
|
||||
}
|
||||
if (!userId) {
|
||||
return res.status(401).json({ error: true, message: 'دسترسی غیرمجاز' });
|
||||
return res.status(401).json({ error: true, message: 'دسترسی غیرمجاز' })
|
||||
}
|
||||
const { postId, caption } = req.body;
|
||||
const files = req.files || [];
|
||||
const { postId, caption } = req.body
|
||||
const files = req.files || []
|
||||
|
||||
if (!postId || !mongoose.Types.ObjectId.isValid(postId)) {
|
||||
return res.status(400).json({ error: true, message: 'شناسه پست نامعتبر است' });
|
||||
return res.status(400).json({ error: true, message: 'شناسه پست نامعتبر است' })
|
||||
}
|
||||
|
||||
const post = await PostModel.findById(postId);
|
||||
const post = await PostModel.findById(postId)
|
||||
if (!post) {
|
||||
return res.status(404).json({ error: true, message: 'پست یافت نشد' });
|
||||
return res.status(404).json({ error: true, message: 'پست یافت نشد' })
|
||||
}
|
||||
|
||||
if (String(post.user_id) !== String(userId)) {
|
||||
return res.status(403).json({ error: true, message: 'اجازه ویرایش این پست را ندارید' });
|
||||
return res.status(403).json({ error: true, message: 'اجازه ویرایش این پست را ندارید' })
|
||||
}
|
||||
|
||||
let keepFiles = [];
|
||||
let keepFiles = []
|
||||
if (req.body.keep_files) {
|
||||
try {
|
||||
keepFiles = Array.isArray(req.body.keep_files)
|
||||
? req.body.keep_files
|
||||
: JSON.parse(req.body.keep_files);
|
||||
: JSON.parse(req.body.keep_files)
|
||||
} catch {
|
||||
keepFiles = [];
|
||||
keepFiles = []
|
||||
}
|
||||
}
|
||||
|
||||
const existingByPath = new Map(
|
||||
(post.files || []).map((f) => [toPublicFilePath(f.path), f])
|
||||
);
|
||||
)
|
||||
|
||||
const orderedExisting = [];
|
||||
const orderedExisting = []
|
||||
for (const item of keepFiles) {
|
||||
const rawPath = typeof item === 'string' ? item : item?.path;
|
||||
if (!rawPath) continue;
|
||||
const key = toPublicFilePath(rawPath);
|
||||
const rawPath = typeof item === 'string' ? item : item?.path
|
||||
if (!rawPath) continue
|
||||
const key = toPublicFilePath(rawPath)
|
||||
const found = existingByPath.get(key) ||
|
||||
(post.files || []).find((f) => toPublicFilePath(f.path) === key || f.path === rawPath);
|
||||
(post.files || []).find((f) => toPublicFilePath(f.path) === key || f.path === rawPath)
|
||||
if (found) {
|
||||
orderedExisting.push({
|
||||
path: found.path,
|
||||
type: found.type === 'video' ? 'video' : 'image',
|
||||
});
|
||||
type: found.type === 'video' ? 'video' : 'image'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const newFilePaths = (files || []).map((file) => ({
|
||||
path: file.path,
|
||||
type: (file.mimetype || file.type || '').startsWith('video/') ? 'video' : 'image',
|
||||
}));
|
||||
type: (file.mimetype || file.type || '').startsWith('video/') ? 'video' : 'image'
|
||||
}))
|
||||
|
||||
// اگر media_order ارسال شده: existing:index یا new:index
|
||||
let finalFiles = [...orderedExisting, ...newFilePaths];
|
||||
let finalFiles = [...orderedExisting, ...newFilePaths]
|
||||
if (req.body.media_order) {
|
||||
let order = [];
|
||||
let order = []
|
||||
try {
|
||||
order = Array.isArray(req.body.media_order)
|
||||
? req.body.media_order
|
||||
: JSON.parse(req.body.media_order);
|
||||
: JSON.parse(req.body.media_order)
|
||||
} catch {
|
||||
order = [];
|
||||
order = []
|
||||
}
|
||||
if (Array.isArray(order) && order.length) {
|
||||
const rebuilt = [];
|
||||
const rebuilt = []
|
||||
for (const token of order) {
|
||||
const value = String(token || '');
|
||||
const value = String(token || '')
|
||||
if (value.startsWith('existing:')) {
|
||||
const idx = Number(value.slice('existing:'.length));
|
||||
if (orderedExisting[idx]) rebuilt.push(orderedExisting[idx]);
|
||||
const idx = Number(value.slice('existing:'.length))
|
||||
if (orderedExisting[idx]) rebuilt.push(orderedExisting[idx])
|
||||
} else if (value.startsWith('new:')) {
|
||||
const idx = Number(value.slice('new:'.length));
|
||||
if (newFilePaths[idx]) rebuilt.push(newFilePaths[idx]);
|
||||
const idx = Number(value.slice('new:'.length))
|
||||
if (newFilePaths[idx]) rebuilt.push(newFilePaths[idx])
|
||||
}
|
||||
}
|
||||
if (rebuilt.length) finalFiles = rebuilt;
|
||||
if (rebuilt.length) finalFiles = rebuilt
|
||||
}
|
||||
}
|
||||
|
||||
if (!finalFiles.length) {
|
||||
return res.status(400).json({ error: true, message: 'حداقل یک فایل برای پست لازم است' });
|
||||
return res.status(400).json({ error: true, message: 'حداقل یک فایل برای پست لازم است' })
|
||||
}
|
||||
|
||||
if (caption != null) {
|
||||
const captionText = String(caption).trim()
|
||||
? String(caption).trim().slice(0, 2000)
|
||||
: null;
|
||||
: null
|
||||
const moderation = resolvePostModerationStatus({
|
||||
caption: captionText,
|
||||
filePaths: finalFiles,
|
||||
});
|
||||
filePaths: finalFiles
|
||||
})
|
||||
if (moderation.status === 'reject') {
|
||||
return res.status(422).json({
|
||||
error: true,
|
||||
message: 'متن پست با قوانین همخوانی ندارد و ثبت نشد.',
|
||||
});
|
||||
message: 'متن پست با قوانین همخوانی ندارد و ثبت نشد.'
|
||||
})
|
||||
}
|
||||
post.caption = captionText;
|
||||
post.status = moderation.status;
|
||||
post.caption = captionText
|
||||
post.status = moderation.status
|
||||
}
|
||||
|
||||
let taggedUserIds = post.tagged_users || [];
|
||||
let taggedUserIds = post.tagged_users || []
|
||||
if (req.body.tagged_user_ids !== undefined) {
|
||||
const raw = req.body.tagged_user_ids;
|
||||
let parsed = [];
|
||||
const raw = req.body.tagged_user_ids
|
||||
let parsed = []
|
||||
try {
|
||||
parsed = Array.isArray(raw) ? raw : (typeof raw === 'string' ? JSON.parse(raw) : []);
|
||||
parsed = Array.isArray(raw) ? raw : (typeof raw === 'string' ? JSON.parse(raw) : [])
|
||||
} catch {
|
||||
parsed = [];
|
||||
parsed = []
|
||||
}
|
||||
taggedUserIds = parsed
|
||||
.filter((id) => mongoose.Types.ObjectId.isValid(id))
|
||||
.slice(0, 50);
|
||||
post.tagged_users = taggedUserIds;
|
||||
.slice(0, 50)
|
||||
post.tagged_users = taggedUserIds
|
||||
}
|
||||
|
||||
post.files = finalFiles;
|
||||
post.type = finalFiles.some((f) => f.type === 'video') ? 'video' : 'image';
|
||||
post.updatedAt = new Date();
|
||||
await post.save();
|
||||
post.files = finalFiles
|
||||
post.type = finalFiles.some((f) => f.type === 'video') ? 'video' : 'image'
|
||||
post.updatedAt = new Date()
|
||||
await post.save()
|
||||
|
||||
const firstVideo = finalFiles.find((file) => file.type === 'video');
|
||||
const firstVideo = finalFiles.find((file) => file.type === 'video')
|
||||
if (firstVideo?.path) {
|
||||
processPostVideoInBackground(firstVideo.path, post._id).catch((err) => {
|
||||
console.error('[postVideoProcessing] background error:', err.message);
|
||||
});
|
||||
console.error('[postVideoProcessing] background error:', err.message)
|
||||
})
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
message: 'پست با موفقیت ویرایش شد',
|
||||
postId: post._id,
|
||||
status: post.status,
|
||||
});
|
||||
status: post.status
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('DEBUG: updatePost error:', err.message);
|
||||
return res.status(500).json({ error: true, message: `خطا در سرور: ${err.message}` });
|
||||
console.error('DEBUG: updatePost error:', err.message)
|
||||
return res.status(500).json({ error: true, message: `خطا در سرور: ${err.message}` })
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
const getUserPosts = async (req, res, next) => {
|
||||
try {
|
||||
@@ -468,7 +465,7 @@ const getUserPostsWeb = async (req, res, next) => {
|
||||
posts: [],
|
||||
postsCount: 0,
|
||||
totalPages: 0,
|
||||
totalItems: 0,
|
||||
totalItems: 0
|
||||
})
|
||||
}
|
||||
|
||||
@@ -477,7 +474,7 @@ const getUserPostsWeb = async (req, res, next) => {
|
||||
posts: [],
|
||||
postsCount: 0,
|
||||
totalPages: 0,
|
||||
totalItems: 0,
|
||||
totalItems: 0
|
||||
})
|
||||
}
|
||||
|
||||
@@ -492,7 +489,7 @@ const getUserPostsWeb = async (req, res, next) => {
|
||||
postsCount: 0,
|
||||
totalPages: 0,
|
||||
totalItems: 0,
|
||||
privateLocked: true,
|
||||
privateLocked: true
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -524,8 +521,8 @@ const getUserPostsWeb = async (req, res, next) => {
|
||||
$or: [
|
||||
{ report_hidden: false },
|
||||
{ report_hidden: { $exists: false } },
|
||||
{ report_hidden: null },
|
||||
],
|
||||
{ report_hidden: null }
|
||||
]
|
||||
},
|
||||
options
|
||||
)
|
||||
@@ -611,14 +608,14 @@ const getPostByIdWeb = async (req, res, next) => {
|
||||
if (userReqId) {
|
||||
const like = await LikeModel.findOne({
|
||||
userId: userReqId,
|
||||
postId: post._id,
|
||||
postId: post._id
|
||||
}).lean()
|
||||
is_liked = !!like
|
||||
}
|
||||
|
||||
const commentsCount = await CommentModel.countDocuments({
|
||||
post: post._id,
|
||||
status: commentStatusFilter(),
|
||||
status: commentStatusFilter()
|
||||
})
|
||||
|
||||
let enrichedPost = {
|
||||
@@ -635,13 +632,13 @@ const getPostByIdWeb = async (req, res, next) => {
|
||||
profile_image: user.profile_image || '',
|
||||
likesCount: post.likes ? post.likes.length : 0,
|
||||
is_liked,
|
||||
commentsCount,
|
||||
commentsCount
|
||||
}
|
||||
|
||||
enrichedPost = await attachSinglePostEngagement(enrichedPost, userReqId)
|
||||
|
||||
return res.status(200).json({
|
||||
post: enrichedPost,
|
||||
post: enrichedPost
|
||||
})
|
||||
} catch (error) {
|
||||
next(error)
|
||||
|
||||
@@ -209,6 +209,13 @@ const resetAlgorithm = async (req, res) => {
|
||||
try {
|
||||
const userId = req.user._id
|
||||
await ExploreInteractionModel.deleteMany({ viewerId: userId })
|
||||
// فقط پاککردن تاریخچهی بازدید کافی نبود — buildUserExploreSignals همچنان از
|
||||
// روی لایک/کامنت/آفرِ واقعیِ کاربر، ترجیحات قبلی رو دوباره میساخت. با ثبت این
|
||||
// تاریخ، اون سیگنالها هم فقط از این لحظه به بعد دوباره جمع میشن
|
||||
await UserModel.updateOne(
|
||||
{ _id: userId },
|
||||
{ $set: { algorithm_reset_at: new Date() } }
|
||||
)
|
||||
const { resetUserFeedCache } = require('../../../utils/exploreAlgorithm')
|
||||
resetUserFeedCache(String(userId))
|
||||
return res.status(200).json({ message: 'الگوریتم بازنشانی شد' })
|
||||
|
||||
@@ -8,8 +8,9 @@ const {
|
||||
canCreateProject,
|
||||
respondProjectProfileIncomplete
|
||||
} = require('../../../utils/projectProfile')
|
||||
const { isVerifiedBadge } = require('../../../utils/verifyBadge')
|
||||
|
||||
async function activateFreeProjectIfEligible(user, project) {
|
||||
async function activateFreeProjectIfEligible (user, project) {
|
||||
if (project.project_type !== 'free') return { activated: false }
|
||||
|
||||
if (!user.daily_free_request || user.daily_free_request <= 0) {
|
||||
@@ -22,12 +23,14 @@ async function activateFreeProjectIfEligible(user, project) {
|
||||
user.daily_free_request -= 1
|
||||
await user.save()
|
||||
project.payment_status = 'done'
|
||||
project.status = 'temp_accept'
|
||||
// کاربرِ بدون تیک وریفای طلایی/سبز/آبی باید از پنل ادمین تایید بشه تا پروژهش
|
||||
// مستقیم توی اپ/وب نمایش داده نشه
|
||||
project.status = isVerifiedBadge(user.verify_badge) ? 'temp_accept' : 'pending'
|
||||
await project.save()
|
||||
return { activated: true }
|
||||
}
|
||||
|
||||
function normalizeRoles(roles) {
|
||||
function normalizeRoles (roles) {
|
||||
if (!Array.isArray(roles)) return []
|
||||
return roles.slice(0, 5).map((role) => ({
|
||||
expertise: role.expertise,
|
||||
@@ -49,7 +52,7 @@ function normalizeRoles(roles) {
|
||||
})).filter((role) => role.expertise)
|
||||
}
|
||||
|
||||
function buildAgeLabel(age, ageMin, ageMax) {
|
||||
function buildAgeLabel (age, ageMin, ageMax) {
|
||||
if (age) return age
|
||||
if (ageMin != null && ageMax != null) return `${ageMin}-${ageMax}`
|
||||
if (ageMin != null) return `${ageMin}+`
|
||||
|
||||
@@ -51,8 +51,11 @@ const REQUEST_USER_SELECT =
|
||||
'user_level first_name last_name user_name is_verified verify_badge user_score rate profile_image'
|
||||
|
||||
const enrichProjectsWithRequests = async (projectDocs) => {
|
||||
// با lean:true دیگه project یه Mongoose document نیست، یه آبجکتِ سادهست —
|
||||
// پس toObject() نداره و project._doc هم undefined میشه (که spreadکردنش
|
||||
// آبجکتِ خالی میداد و همهی فیلدهای پروژه رو پاک میکرد)
|
||||
const docs = projectDocs.map((project) =>
|
||||
project.toObject ? project.toObject() : { ...project._doc }
|
||||
typeof project.toObject === 'function' ? project.toObject() : { ...project }
|
||||
)
|
||||
|
||||
const projectIds = docs
|
||||
@@ -121,7 +124,8 @@ const getProjects = async (req, res, next) => {
|
||||
const options = {
|
||||
page: parseInt(page), // تبدیل صفحه به عدد صحیح
|
||||
limit: parseInt(limit), // تبدیل محدودیت به عدد صحیح
|
||||
sort: most_price === 'true' ? { offer_price: -1 } : { requested_users: 'desc' }
|
||||
sort: most_price === 'true' ? { offer_price: -1 } : { requested_users: 'desc' },
|
||||
lean: true
|
||||
}
|
||||
const projects = await ProjectModel.paginate(filter, options)
|
||||
|
||||
@@ -136,7 +140,8 @@ const getProjects = async (req, res, next) => {
|
||||
const options = {
|
||||
page: parseInt(page), // تبدیل صفحه به عدد صحیح
|
||||
limit: parseInt(limit), // تبدیل محدودیت به عدد صحیح
|
||||
sort: { acceptedAt: -1, createdAt: -1 } // مرتبسازی بر اساس acceptedAt و سپس createdAt
|
||||
sort: { acceptedAt: -1, createdAt: -1 }, // مرتبسازی بر اساس acceptedAt و سپس createdAt
|
||||
lean: true
|
||||
}
|
||||
const projects = await ProjectModel.paginate(filter, options)
|
||||
let newProj = await enrichProjectsWithRequests(projects.docs)
|
||||
|
||||
@@ -37,6 +37,8 @@ const saveProjectComment = async ({
|
||||
return { error: { status: 400, message: 'Rating must be between 1 and 5' } }
|
||||
}
|
||||
|
||||
const creatorUser = await UserModel.findById(creatorId).select('verify_badge').lean()
|
||||
|
||||
const newComment = await saveCommentWithModerationStatus(
|
||||
CommentModel,
|
||||
{
|
||||
@@ -45,9 +47,10 @@ const saveProjectComment = async ({
|
||||
creator: creatorId,
|
||||
rating: ratingValue,
|
||||
comment,
|
||||
comment_for: commentFor,
|
||||
comment_for: commentFor
|
||||
},
|
||||
'comment'
|
||||
'comment',
|
||||
creatorUser?.verify_badge
|
||||
)
|
||||
|
||||
if (isNewRating && commentFor === 'user') {
|
||||
|
||||
@@ -455,9 +455,9 @@ const getDiscoverFeed = async (req, res, next) => {
|
||||
const catalogIds = [...new Set(docs.map((d) => String(d.catalogProduct)).filter(Boolean))]
|
||||
const shopCounts = catalogIds.length
|
||||
? await ShopProductListingModel.aggregate([
|
||||
{ $match: { ...matchStage, catalogProduct: { $in: catalogIds.map((id) => new mongoose.Types.ObjectId(id)) } } },
|
||||
{ $group: { _id: '$catalogProduct', count: { $sum: 1 } } }
|
||||
])
|
||||
{ $match: { ...matchStage, catalogProduct: { $in: catalogIds.map((id) => new mongoose.Types.ObjectId(id)) } } },
|
||||
{ $group: { _id: '$catalogProduct', count: { $sum: 1 } } }
|
||||
])
|
||||
: []
|
||||
const shopCountMap = new Map(shopCounts.map((c) => [String(c._id), c.count]))
|
||||
|
||||
@@ -580,7 +580,8 @@ const createShopProductComment = async (req, res, next) => {
|
||||
comment_for: 'shop_product',
|
||||
shop_product: listing._id
|
||||
},
|
||||
'comment'
|
||||
'comment',
|
||||
req.user.verify_badge
|
||||
)
|
||||
|
||||
await createCommentNotification({
|
||||
@@ -631,8 +632,8 @@ const sendProductToUsers = async (req, res, next) => {
|
||||
|
||||
const price = listing.variants?.length
|
||||
? Math.min(
|
||||
...listing.variants.map((v) => (v.discount_price != null ? v.discount_price : v.price))
|
||||
)
|
||||
...listing.variants.map((v) => (v.discount_price != null ? v.discount_price : v.price))
|
||||
)
|
||||
: null
|
||||
|
||||
const sharedProduct = {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,9 @@ const {
|
||||
} = require('../../../utils/blockVisibility')
|
||||
|
||||
const NEARBY_RADIUS_METERS = 5000
|
||||
const MIN_RADIUS_METERS = 5000
|
||||
const MAX_RADIUS_METERS = 50000
|
||||
const RADIUS_STEP_METERS = 5000
|
||||
|
||||
function haversineMeters(lat1, lon1, lat2, lon2) {
|
||||
const toRad = (deg) => (deg * Math.PI) / 180
|
||||
@@ -22,6 +25,13 @@ function parseCoord(value) {
|
||||
return Number.isFinite(n) ? n : null
|
||||
}
|
||||
|
||||
function parseRadiusMeters(value) {
|
||||
const n = parseFloat(value)
|
||||
if (!Number.isFinite(n)) return NEARBY_RADIUS_METERS
|
||||
const clamped = Math.min(Math.max(n, MIN_RADIUS_METERS), MAX_RADIUS_METERS)
|
||||
return Math.round(clamped / RADIUS_STEP_METERS) * RADIUS_STEP_METERS
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /users/nearby?lat=&lng=
|
||||
* Requires auth + show_location + valid coords.
|
||||
@@ -32,6 +42,7 @@ const getNearbyUsers = async (req, res, next) => {
|
||||
const userId = String(req.user._id)
|
||||
const lat = parseCoord(req.query.lat)
|
||||
const lng = parseCoord(req.query.lng)
|
||||
const radiusMeters = parseRadiusMeters(req.query.radius)
|
||||
|
||||
if (lat == null || lng == null) {
|
||||
return res.status(422).json({
|
||||
@@ -98,7 +109,7 @@ const getNearbyUsers = async (req, res, next) => {
|
||||
if (peerLat == null || peerLng == null) continue
|
||||
|
||||
const distanceMeters = haversineMeters(lat, lng, peerLat, peerLng)
|
||||
if (distanceMeters > NEARBY_RADIUS_METERS) continue
|
||||
if (distanceMeters > radiusMeters) continue
|
||||
|
||||
nearby.push({
|
||||
_id: uid,
|
||||
@@ -116,7 +127,7 @@ const getNearbyUsers = async (req, res, next) => {
|
||||
nearby.sort((a, b) => a.distanceMeters - b.distanceMeters)
|
||||
|
||||
res.json({
|
||||
radiusMeters: NEARBY_RADIUS_METERS,
|
||||
radiusMeters,
|
||||
users: nearby
|
||||
})
|
||||
} catch (error) {
|
||||
@@ -126,5 +137,8 @@ const getNearbyUsers = async (req, res, next) => {
|
||||
|
||||
module.exports = {
|
||||
getNearbyUsers,
|
||||
NEARBY_RADIUS_METERS
|
||||
NEARBY_RADIUS_METERS,
|
||||
MIN_RADIUS_METERS,
|
||||
MAX_RADIUS_METERS,
|
||||
RADIUS_STEP_METERS
|
||||
}
|
||||
|
||||
@@ -134,7 +134,10 @@ const getProjects = async (req, res, next) => {
|
||||
]
|
||||
}
|
||||
} else if (status_filter === 'در دست بررسی') {
|
||||
filter = { creator_id: userId, status: { $in: ['paid', 'rejected'] } }
|
||||
// pending: کاربرِ بدون تیک وریفای که پروژهش پرداخت شده ولی هنوز از پنل
|
||||
// ادمین تایید نشده — باید زیرِ همین تبِ «در دست بررسی» دیده بشه، نه
|
||||
// «پرداخت نشده» (که یعنی اصلاً پرداخت نکرده)
|
||||
filter = { creator_id: userId, status: { $in: ['paid', 'pending', 'rejected'] } }
|
||||
} else if (status_filter === 'پرداخت نشده') {
|
||||
filter = { creator_id: userId, status: 'pre_payment' }
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ const { default: mongoose } = require('mongoose')
|
||||
// دریافت لیست کامنتها با پیجینیشن
|
||||
const getComments = async (req, res, next) => {
|
||||
try {
|
||||
console.log("gg6");
|
||||
const token = req.header('Authorization').split(' ')[1]
|
||||
if (!token) return res.status(401).send('Access Denied')
|
||||
|
||||
@@ -14,7 +13,8 @@ const getComments = async (req, res, next) => {
|
||||
page = 1, limit = 10,
|
||||
startDate,
|
||||
endDate,
|
||||
status
|
||||
status,
|
||||
creator
|
||||
} = req.query
|
||||
const filter = {} // فیلتر بر اساس وضعیت کامنت
|
||||
|
||||
@@ -30,10 +30,17 @@ const getComments = async (req, res, next) => {
|
||||
filter.createdAt.$lte = new Date(endDate) // تاریخ پایان
|
||||
}
|
||||
}
|
||||
console.log("gg8");
|
||||
if (status) {
|
||||
filter.status = status
|
||||
}
|
||||
// برای تبِ «نظرات» توی جزئیاتِ کاربر (UserComments.jsx پنل) — قبلاً کلِ لیستِ
|
||||
// صفحهبندیشدهی سراسری گرفته میشد و بعدش سمتِ کلاینت فیلتر میشد، یعنی totalPages
|
||||
// و حتی خودِ نتایج بسته به اینکه کاربر توی همون صفحهی فعلی کامنتی داشته باشه یا نه
|
||||
// درست نبود. فیلترِ creator رو مستقیم به کوئری اضافه میکنیم تا هم نتایج هم
|
||||
// صفحهبندی واقعاً مالِ همین کاربر باشه
|
||||
if (creator && mongoose.Types.ObjectId.isValid(creator)) {
|
||||
filter.creator = creator
|
||||
}
|
||||
const options = {
|
||||
page: parseInt(page), // تبدیل صفحه به عدد صحیح
|
||||
limit: parseInt(limit), // تبدیل محدودیت به عدد صحیح
|
||||
|
||||
@@ -19,6 +19,7 @@ const getPosts = async (req, res, next) => {
|
||||
startDate,
|
||||
endDate,
|
||||
status,
|
||||
user_id,
|
||||
} = req.query;
|
||||
|
||||
const filter = {};
|
||||
@@ -34,6 +35,12 @@ const getPosts = async (req, res, next) => {
|
||||
if (status) {
|
||||
filter.status = status;
|
||||
}
|
||||
// برای تبِ «پستها» توی جزئیاتِ کاربر (UserPosts.jsx پنل) — قبلاً ۱۰۰۰ تا پستِ
|
||||
// سراسری میگرفت و سمتِ کلاینت فیلتر میکرد؛ اگه پستهای این کاربر بینِ ۱۰۰۰ تای
|
||||
// اخیر نمیبودن، این تب هیچی نشون نمیداد. فیلترِ user_id مستقیم به کوئری اضافه میشه
|
||||
if (user_id && mongoose.Types.ObjectId.isValid(user_id)) {
|
||||
filter.user_id = user_id;
|
||||
}
|
||||
|
||||
const options = {
|
||||
page: parseInt(page),
|
||||
|
||||
@@ -93,7 +93,7 @@ const advertisingSchema = new mongoose.Schema({
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
default: "UnderReview",
|
||||
default: 'UnderReview',
|
||||
required: false
|
||||
},
|
||||
image: {
|
||||
@@ -157,7 +157,7 @@ const advertisingSchema = new mongoose.Schema({
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
enum: ['pre_payment', 'paid', 'temp_accept', 'accepted', 'rejected', 'expired'],
|
||||
enum: ['pre_payment', 'paid', 'pending', 'temp_accept', 'accepted', 'rejected', 'expired'],
|
||||
default: 'pre_payment'
|
||||
},
|
||||
payment_status: {
|
||||
|
||||
@@ -200,7 +200,7 @@ const projectSchema = new mongoose.Schema({
|
||||
// فیلد برای وضعیت پروژه
|
||||
status: {
|
||||
type: String,
|
||||
enum: ['pre_payment', 'paid', 'temp_accept', 'accepted', 'rejected', 'done', 'cancled', 'ongoing'],
|
||||
enum: ['pre_payment', 'paid', 'pending', 'temp_accept', 'accepted', 'rejected', 'done', 'cancled', 'ongoing'],
|
||||
default: 'pre_payment'
|
||||
},
|
||||
public_status: {
|
||||
@@ -289,6 +289,14 @@ const projectSchema = new mongoose.Schema({
|
||||
|
||||
})
|
||||
|
||||
// ایندکسهای لازم برای کوئریِ لیستِ پروژهها (getProjectController.js) — بدونشون
|
||||
// روی هر درخواست یه COLLSCAN + sort-در-حافظه انجام میشد که با بزرگترشدنِ
|
||||
// کالکشن کندتر میشد؛ همون فیلترها/مرتبسازیهایی که getProjects واقعاً استفاده میکنه
|
||||
projectSchema.index({ status: 1, selected_user: 1, acceptedAt: -1, createdAt: -1 })
|
||||
projectSchema.index({ status: 1, expertise: 1 })
|
||||
projectSchema.index({ status: 1, offer_price: -1 })
|
||||
projectSchema.index({ status: 1, requested_users: -1 })
|
||||
|
||||
projectSchema.plugin(timestamp)
|
||||
// اضافه کردن پیجینیشن به مدل
|
||||
projectSchema.plugin(mongoosePaginate)
|
||||
|
||||
@@ -49,6 +49,10 @@ const requestSchema = new mongoose.Schema({
|
||||
}
|
||||
})
|
||||
|
||||
// پشتیبانِ کوئریِ enrichProjectsWithRequests (getProjectController.js) که با
|
||||
// $in روی project سرچ میکنه — بدونش هر لیستِ پروژه یه COLLSCAN روی Request میزد
|
||||
requestSchema.index({ project: 1 })
|
||||
|
||||
requestSchema.plugin(timestamp)
|
||||
|
||||
const RequestModel = mongoose.model('Request', requestSchema)
|
||||
|
||||
@@ -412,6 +412,13 @@ const userSchema = new mongoose.Schema({
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
// وقتی «ریست الگوریتم» زده میشه ست میشه — buildUserExploreSignals لایک/کامنت/آفر
|
||||
// قبل از این تاریخ رو نادیده میگیره تا کاربر واقعاً مثل تازهوارد رفتار ببینه، نه
|
||||
// فقط کش رتبهبندی پاک بشه درحالیکه سیگنالهای شخصیسازی قدیمی هنوز اثر دارن
|
||||
algorithm_reset_at: {
|
||||
type: Date,
|
||||
default: null
|
||||
},
|
||||
rate: {
|
||||
type: String,
|
||||
trim: true,
|
||||
|
||||
@@ -2,7 +2,7 @@ const express = require('express')
|
||||
const blockCheck = require('../../../middlewares/blockCheck')
|
||||
const { createAdvertisingValidationRules, createAdvertising, getAdvertisingTypes, getAdvertisingCategory, getSingleAdvertising, getAdvertisingFeatures, getAdvertisings, toggleLike, addComment, getComments, rateAdvertising, getUserAdvertisings, getAdvertisingAnalytics, editAdvertising, getUserSingleAdvertising, getUserAdvertisingProfile, updateUserAdvertisingProfile, getAdvertisingsWeb, getSingleAdvertisingWeb, getUserAdvertisingProfileWeb, recordAdvertisingView, getUserLikedAdvertisings, getUserAdvertisingComments } = require('../../../controllers/application/advertising/advertisingController')
|
||||
const auth = require('../../../middlewares/auth')
|
||||
const { initiateAdvertisingPayment, handleAdvertisingPaymentCallback, handleAdvertisingRepublishPaymentCallback, republishAdvertisingPayment, handleSuccessfulAdvertisingPayment, handleFailedAdvertisingPayment, handleFailedAdvertisingRepublishPayment, handleSuccessfulAdvertisingRepublishPayment } = require('../../../controllers/application/payment/paymentController')
|
||||
const { initiateAdvertisingPayment, handleAdvertisingPaymentCallback, handleAdvertisingRepublishPaymentCallback, republishAdvertisingPayment } = require('../../../controllers/application/payment/paymentController')
|
||||
const { initiateAdvertisingPaymentWeb, handleAdvertisingPaymentCallbackWeb, republishAdvertisingPaymentWeb, handleAdvertisingRepublishPaymentCallbackWeb } = require('../../../controllers/application/payment/paymentControllerWeb')
|
||||
const router = express.Router()
|
||||
router.get('/', [auth], getAdvertisings)
|
||||
@@ -36,13 +36,4 @@ router.get('/comments', getComments)
|
||||
router.post('/rate', [blockCheck], [auth], rateAdvertising)
|
||||
router.post('/edit/:id', [blockCheck], [auth], editAdvertising)
|
||||
|
||||
// Route for handling successful payment
|
||||
router.post('/payment-success', handleSuccessfulAdvertisingPayment)
|
||||
// Route for handling failed payment
|
||||
router.post('/payment-failed', handleFailedAdvertisingPayment)
|
||||
// Route for handling successful payment
|
||||
router.post('/republish-payment-success', handleSuccessfulAdvertisingRepublishPayment)
|
||||
// Route for handling failed payment
|
||||
router.post('/republish-payment-failed', handleFailedAdvertisingRepublishPayment)
|
||||
|
||||
module.exports = router
|
||||
|
||||
@@ -6,6 +6,7 @@ const {
|
||||
updateTrackingCode,
|
||||
getOrderById,
|
||||
confirmReceipt,
|
||||
cancelOrder,
|
||||
rateOrder,
|
||||
reportOrder,
|
||||
createReturnRequest
|
||||
@@ -19,6 +20,7 @@ router.get('/:id', getOrderById)
|
||||
router.patch('/:id/status', updateOrderStatus)
|
||||
router.patch('/:id/tracking-code', updateTrackingCode)
|
||||
router.patch('/:id/confirm-receipt', confirmReceipt)
|
||||
router.patch('/:id/cancel', cancelOrder)
|
||||
router.post('/:id/rate', rateOrder)
|
||||
router.post('/:id/report', reportOrder)
|
||||
router.post('/:id/return-request', createReturnRequest)
|
||||
|
||||
@@ -2,7 +2,7 @@ const express = require('express')
|
||||
const { createProject, createProjectValidationRules } = require('../../../controllers/application/projects/createProjectController')
|
||||
const { getProjects, getSingleProjects, getSingleProjectsWeb, getUserProjects, getUserProjectsWeb } = require('../../../controllers/application/projects/getProjectController')
|
||||
const { getProjectTypes } = require('../../../controllers/application/projects/projectTypesController')
|
||||
const { initiatePayment, handlePaymentCallback, paymentRequestStepOne, paymentRequestStepTwo, paymentRequestStepAll, paymentRequestStepOneCallback, paymentRequestStepTwoCallback, paymentRequestStepAllCallback, handleSuccessfulPayment, handleFailedPayment } = require('../../../controllers/application/payment/paymentController')
|
||||
const { initiatePayment, handlePaymentCallback, paymentRequestStepOne, paymentRequestStepTwo, paymentRequestStepAll, paymentRequestStepOneCallback, paymentRequestStepTwoCallback, paymentRequestStepAllCallback } = require('../../../controllers/application/payment/paymentController')
|
||||
const auth = require('../../../middlewares/auth')
|
||||
const { requestProjectValidationRules, requestProject, acceptProject, editRequestProject } = require('../../../controllers/application/projects/requestProjectController')
|
||||
const { editProject, doneProject, cancleProject, editProjectValidationRules, setRateProject, doneProjectWeb } = require('../../../controllers/application/projects/updateProjectController')
|
||||
@@ -47,8 +47,4 @@ router.post('/cancle', [isBasicRegister], [blockCheck], [auth], cancleProject)
|
||||
router.post('/initiate-completion-payment/web', [isBasicRegister], [blockCheck], [auth], initiateProjectCompletionPaymentWeb)
|
||||
router.get('/payment-completion-callback/web', handleProjectCompletionPaymentCallbackWeb)
|
||||
|
||||
// Route for handling successful payment
|
||||
router.post('/payment-success', handleSuccessfulPayment)
|
||||
// Route for handling failed payment
|
||||
router.post('/payment-failed', handleFailedPayment)
|
||||
module.exports = router
|
||||
|
||||
@@ -48,7 +48,10 @@ async function invalidateCachePattern (pattern) {
|
||||
for await (const key of redis.scanIterator({ MATCH: pattern, COUNT: 100 })) {
|
||||
keys.push(key)
|
||||
}
|
||||
if (keys.length) await redis.del(keys)
|
||||
// این کلاینتِ redis برای del چندکلیدی آرگومانهای جدا میخواد، نه یه آرایه —
|
||||
// پاسدادنِ مستقیمِ آرایه با خطای "arguments[1] must be of type string|Buffer"
|
||||
// silently fail میشد و کشِ باطلنشده میموند
|
||||
if (keys.length) await redis.del(...keys)
|
||||
} catch (err) {
|
||||
console.error(`[cache] pattern invalidate error for ${pattern}:`, err.message)
|
||||
}
|
||||
|
||||
@@ -2,15 +2,17 @@ const { resolveCommentModerationStatus } = require('./contentModeration')
|
||||
|
||||
const STATUS_FALLBACKS = {
|
||||
temp_accept: ['temp_accept', 'accepted', 'pending'],
|
||||
rejected: ['rejected', 'pending'],
|
||||
rejected: ['rejected', 'pending']
|
||||
}
|
||||
|
||||
/**
|
||||
* Save comment with moderation status; fall back if server schema lacks newer enum values.
|
||||
* verifyBadge: تیکِ وریفایِ کاربرِ سازنده (creator) — اگه طلایی/سبز/آبی نباشه،
|
||||
* status روی pending میمونه تا از پنل ادمین دستی تایید بشه
|
||||
*/
|
||||
async function saveCommentWithModerationStatus(Model, data, textField = 'comment') {
|
||||
async function saveCommentWithModerationStatus (Model, data, textField = 'comment', verifyBadge) {
|
||||
const text = data[textField] ?? data.text ?? ''
|
||||
const preferredStatus = resolveCommentModerationStatus(text)
|
||||
const preferredStatus = resolveCommentModerationStatus(text, verifyBadge)
|
||||
const candidates = STATUS_FALLBACKS[preferredStatus] ?? [preferredStatus]
|
||||
|
||||
let lastError = null
|
||||
@@ -34,5 +36,5 @@ async function saveCommentWithModerationStatus(Model, data, textField = 'comment
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
saveCommentWithModerationStatus,
|
||||
saveCommentWithModerationStatus
|
||||
}
|
||||
|
||||
@@ -1,63 +1,76 @@
|
||||
/**
|
||||
* Moderation helpers for comments and posts (Persian profanity + explicit cues).
|
||||
*/
|
||||
const { isVerifiedBadge } = require('./verifyBadge')
|
||||
|
||||
const PROFANITY_PATTERNS = [
|
||||
/\b(کس|کیر|کون|جنده|حروم|لاشی|بی\s*شعور|گه|گایید|گائید)\b/i,
|
||||
/\b(fuck|shit|bitch|asshole)\b/i,
|
||||
];
|
||||
/\b(fuck|shit|bitch|asshole)\b/i
|
||||
]
|
||||
|
||||
const EXPLICIT_CONTENT_PATTERNS = [
|
||||
/\b(برهنگ|برهنگی|پورن|porn|nude|naked|sex)\b/i,
|
||||
/\b(سکس|جنسی\s*صریح)\b/i,
|
||||
];
|
||||
/\b(سکس|جنسی\s*صریح)\b/i
|
||||
]
|
||||
|
||||
function normalizeText(text) {
|
||||
if (!text || typeof text !== 'string') return '';
|
||||
function normalizeText (text) {
|
||||
if (!text || typeof text !== 'string') return ''
|
||||
return text
|
||||
.replace(/[\u200c\u200f]/g, '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
.trim()
|
||||
}
|
||||
|
||||
function matchesAny(text, patterns) {
|
||||
const normalized = normalizeText(text);
|
||||
if (!normalized) return false;
|
||||
return patterns.some((pattern) => pattern.test(normalized));
|
||||
function matchesAny (text, patterns) {
|
||||
const normalized = normalizeText(text)
|
||||
if (!normalized) return false
|
||||
return patterns.some((pattern) => pattern.test(normalized))
|
||||
}
|
||||
|
||||
function containsProfanity(text) {
|
||||
return matchesAny(text, PROFANITY_PATTERNS);
|
||||
function containsProfanity (text) {
|
||||
return matchesAny(text, PROFANITY_PATTERNS)
|
||||
}
|
||||
|
||||
function containsExplicitContent(text) {
|
||||
return matchesAny(text, EXPLICIT_CONTENT_PATTERNS);
|
||||
function containsExplicitContent (text) {
|
||||
return matchesAny(text, EXPLICIT_CONTENT_PATTERNS)
|
||||
}
|
||||
|
||||
/** Comment / vitrine / academy comment status */
|
||||
function resolveCommentModerationStatus(text) {
|
||||
/**
|
||||
* Comment / vitrine / academy comment status.
|
||||
* کاربرِ بدون تیک وریفای (طلایی/سبز/آبی) نباید مستقیم توی اپ/وب نمایش داده بشه —
|
||||
* باید توی پنل ادمین دستی تایید بشه، پس status روش pending میمونه
|
||||
*/
|
||||
function resolveCommentModerationStatus (text, verifyBadge) {
|
||||
if (containsProfanity(text) || containsExplicitContent(text)) {
|
||||
return 'rejected';
|
||||
return 'rejected'
|
||||
}
|
||||
return 'temp_accept';
|
||||
if (!isVerifiedBadge(verifyBadge)) {
|
||||
return 'pending'
|
||||
}
|
||||
return 'temp_accept'
|
||||
}
|
||||
|
||||
/**
|
||||
* Post moderation:
|
||||
* - reject: profanity / explicit text in caption
|
||||
* - temp_accept: visible in app, awaits manual full approval
|
||||
* - pending: creator ندارد تیک وریفای طلایی/سبز/آبی — نیاز به تاییدِ دستیِ پنل داره
|
||||
* - temp_accept: creator وریفایشدهست — مستقیم توی اپ نمایش داده میشه
|
||||
*/
|
||||
function resolvePostModerationStatus({ caption, filePaths = [] }) {
|
||||
function resolvePostModerationStatus ({ caption, filePaths = [], verifyBadge }) {
|
||||
if (containsProfanity(caption) || containsExplicitContent(caption)) {
|
||||
return { status: 'reject', reason: 'caption_violation' };
|
||||
return { status: 'reject', reason: 'caption_violation' }
|
||||
}
|
||||
|
||||
return { status: 'temp_accept', reason: 'awaiting_review' };
|
||||
if (!isVerifiedBadge(verifyBadge)) {
|
||||
return { status: 'pending', reason: 'awaiting_admin_review' }
|
||||
}
|
||||
|
||||
return { status: 'temp_accept', reason: 'awaiting_review' }
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
containsProfanity,
|
||||
containsExplicitContent,
|
||||
resolveCommentModerationStatus,
|
||||
resolvePostModerationStatus,
|
||||
};
|
||||
resolvePostModerationStatus
|
||||
}
|
||||
|
||||
@@ -257,19 +257,26 @@ async function buildUserExploreSignals(userId) {
|
||||
}
|
||||
|
||||
const user = await UserModel.findById(userId)
|
||||
.select('expertise sub_expertise')
|
||||
.select('expertise sub_expertise algorithm_reset_at')
|
||||
.lean()
|
||||
|
||||
// بعد از «ریست الگوریتم»، لایک/کامنت/آفرِ قبل از این تاریخ نباید توی ترجیحات
|
||||
// دوبارهساختهشده اثر بذاره — وگرنه با اینکه کشِ رتبهبندی پاک شده، سیگنالهای
|
||||
// شخصیسازیِ قدیمی از نو محاسبه میشن و کاربر عملاً هیچ تغییری حس نمیکنه
|
||||
const resetAt = user?.algorithm_reset_at || null
|
||||
const createdAtFilter = resetAt ? { createdAt: { $gte: resetAt } } : {}
|
||||
|
||||
const [likes, comments, offers, interactions] = await Promise.all([
|
||||
LikeModel.find({ userId }).select('postId createdAt').lean(),
|
||||
LikeModel.find({ userId, ...createdAtFilter }).select('postId createdAt').lean(),
|
||||
CommentModel.find({
|
||||
creator: userId,
|
||||
comment_for: 'post',
|
||||
post: { $ne: null }
|
||||
post: { $ne: null },
|
||||
...createdAtFilter
|
||||
})
|
||||
.select('post user createdAt')
|
||||
.lean(),
|
||||
OfferModel.find({ sender: userId }).select('receiver createdAt').lean(),
|
||||
OfferModel.find({ sender: userId, ...createdAtFilter }).select('receiver createdAt').lean(),
|
||||
ExploreInteractionModel.find({ viewerId: userId })
|
||||
.sort({ createdAt: -1 })
|
||||
.limit(1200)
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
function computeVerifyBadge({ is_verified, license_verified, gold_badge }) {
|
||||
const VERIFIED_BADGES = ['gold', 'green', 'blue']
|
||||
|
||||
// کاربر با تیک طلایی/سبز/آبی «وریفایشده» حساب میشه — محتواش نیازی به تاییدِ
|
||||
// دستیِ پنل نداره و مستقیم توی اپ/وب نمایش داده میشه
|
||||
function isVerifiedBadge (badge) {
|
||||
return VERIFIED_BADGES.includes(badge)
|
||||
}
|
||||
|
||||
function computeVerifyBadge ({ is_verified, license_verified, gold_badge }) {
|
||||
if (gold_badge) return 'gold'
|
||||
if (license_verified) return 'green'
|
||||
if (is_verified === 'verified') return 'blue'
|
||||
@@ -6,7 +14,7 @@ function computeVerifyBadge({ is_verified, license_verified, gold_badge }) {
|
||||
return 'none'
|
||||
}
|
||||
|
||||
function syncVerifyBadge(userDoc) {
|
||||
function syncVerifyBadge (userDoc) {
|
||||
userDoc.verify_badge = computeVerifyBadge({
|
||||
is_verified: userDoc.is_verified,
|
||||
license_verified: userDoc.license_verified,
|
||||
@@ -15,4 +23,4 @@ function syncVerifyBadge(userDoc) {
|
||||
return userDoc.verify_badge
|
||||
}
|
||||
|
||||
module.exports = { computeVerifyBadge, syncVerifyBadge }
|
||||
module.exports = { computeVerifyBadge, syncVerifyBadge, isVerifiedBadge, VERIFIED_BADGES }
|
||||
|
||||
Reference in New Issue
Block a user