This commit is contained in:
payacom
2026-09-03 16:06:02 +03:30
parent 3f416a7a60
commit 90c5f94b57
31 changed files with 3968 additions and 4283 deletions

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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: 'الگوریتم بازنشانی شد' })

View File

@@ -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}+`

View File

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

View File

@@ -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') {

View File

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

View File

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

View File

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

View File

@@ -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), // تبدیل محدودیت به عدد صحیح

View File

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

View File

@@ -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: {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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