From 90c5f94b5763765dd09d1313f731024178ddcb6a Mon Sep 17 00:00:00 2001 From: payacom <59262811+payacom@users.noreply.github.com> Date: Thu, 3 Sep 2026 16:06:02 +0330 Subject: [PATCH] check --- .../application/academy/academyControllers.js | 4652 ++++++++--------- .../advertising/advertisingController.js | 14 +- .../bookings/bookingPaymentController.js | 39 +- .../financial/financialController.js | 48 +- .../application/offer/offerController.js | 15 +- .../application/orders/orderController.js | 59 + .../application/payment/paymentController.js | 559 +- .../payment/paymentControllerWeb.js | 634 +-- .../application/posts/postController.js | 333 +- .../application/profile/accountController.js | 7 + .../projects/createProjectController.js | 11 +- .../projects/getProjectController.js | 11 +- .../projects/updateProjectController.js | 7 +- .../shop-products/shopProductController.js | 13 +- .../application/users/getUserController.js | 1653 +++--- .../users/nearbyUsersController.js | 20 +- .../workroom/workroomController.js | 5 +- .../panel/comments/commentController.js | 13 +- controllers/panel/posts/postController.js | 7 + models/AdvertisingModel.js | 4 +- models/ProjectModel.js | 10 +- models/RequestModel.js | 4 + models/UserModel.js | 7 + routes/application/advertising/index.js | 11 +- routes/application/orders/index.js | 2 + routes/application/projects/index.js | 6 +- utils/cache.js | 5 +- utils/commentStatusSave.js | 10 +- utils/contentModeration.js | 63 +- utils/exploreAlgorithm.js | 15 +- utils/verifyBadge.js | 14 +- 31 files changed, 3968 insertions(+), 4283 deletions(-) diff --git a/controllers/application/academy/academyControllers.js b/controllers/application/academy/academyControllers.js index dd75168..1317150 100644 --- a/controllers/application/academy/academyControllers.js +++ b/controllers/application/academy/academyControllers.js @@ -1,158 +1,159 @@ -const jwt = require("jsonwebtoken"); -const fs = require("fs-extra"); -const path = require("path"); -const jMoment = require("moment-jalaali"); -const AcademyContentModel = require("../../../models/AcademyContentModel"); -const AcademyModel = require("../../../models/AcademyModel"); -const AcademyPaymentModel = require("../../../models/AcademyPaymentModel"); -const CourseModel = require("../../../models/CourseModel"); -const CoursePaymentModel = require("../../../models/CoursePaymentModel"); -const UserModel = require("../../../models/UserModel"); -const PaymentModel = require("../../../models/PaymentModel"); -const { default: axios } = require("axios"); -const AcademyLikeModel = require("../../../models/AcademyLikeModel"); -const CourseComment = require("../../../models/CourseComentModel"); -const IsPaymentCourse = require("../../../models/IsPaymentCourseModel"); -const Tax = require("../../../models/TaxModel"); -const AcademyCategoryModel = require('../../../models/AcademyCategoryModel'); -const { buildUnifiedInterestScore, blendFreshIntoRanked: blendInterestIntoRanked } = require('../../../utils/exploreAlgorithm'); -const { resolveCommentModerationStatus } = require('../../../utils/contentModeration'); -const { saveCommentWithModerationStatus } = require('../../../utils/commentStatusSave'); +const jwt = require('jsonwebtoken') +const fs = require('fs-extra') +const path = require('path') +const jMoment = require('moment-jalaali') +const AcademyContentModel = require('../../../models/AcademyContentModel') +const AcademyModel = require('../../../models/AcademyModel') +const AcademyPaymentModel = require('../../../models/AcademyPaymentModel') +const CourseModel = require('../../../models/CourseModel') +const CoursePaymentModel = require('../../../models/CoursePaymentModel') +const UserModel = require('../../../models/UserModel') +const PaymentModel = require('../../../models/PaymentModel') +const { default: axios } = require('axios') +const AcademyLikeModel = require('../../../models/AcademyLikeModel') +const CourseComment = require('../../../models/CourseComentModel') +const IsPaymentCourse = require('../../../models/IsPaymentCourseModel') +const Tax = require('../../../models/TaxModel') +const AcademyCategoryModel = require('../../../models/AcademyCategoryModel') +const { buildUnifiedInterestScore, blendFreshIntoRanked: blendInterestIntoRanked } = require('../../../utils/exploreAlgorithm') +const { resolveCommentModerationStatus } = require('../../../utils/contentModeration') +const { saveCommentWithModerationStatus } = require('../../../utils/commentStatusSave') +const { isVerifiedBadge } = require('../../../utils/verifyBadge') const { coursePublicStatusFilter, academyContentPublicStatusFilter, commentStatusFilter, - PUBLIC_COMMENT_STATUSES, -} = require('../../../utils/moderationStatus'); + PUBLIC_COMMENT_STATUSES +} = require('../../../utils/moderationStatus') const { createLikeNotification, resolveCourseOwnerId -} = require('../../../utils/likeNotification'); +} = require('../../../utils/likeNotification') const { createCommentNotification, - createRatingNotification, -} = require('../../../utils/commentNotification'); + createRatingNotification +} = require('../../../utils/commentNotification') const { - createCoursePurchaseNotification, -} = require('../../../utils/academyNotification'); -const AdvertisingProfileModel = require('../../../models/AdvertisingProfile'); -const WalletTransactionModel = require('../../../models/WalletTransactionModel'); + createCoursePurchaseNotification +} = require('../../../utils/academyNotification') +const AdvertisingProfileModel = require('../../../models/AdvertisingProfile') +const WalletTransactionModel = require('../../../models/WalletTransactionModel') -const ACADEMY_COMMISSION_RATE = 0.15; +const ACADEMY_COMMISSION_RATE = 0.15 const onServerRestart = async () => { const CoursePayment = await CoursePaymentModel.findOne({ - cuorse_type: "pro", - }); + cuorse_type: 'pro' + }) if (!CoursePayment) { const newCoursePayment = new CoursePaymentModel({ - price: "60000", - cuorse_type: "pro", - }); - await newCoursePayment.save(); + price: '60000', + cuorse_type: 'pro' + }) + await newCoursePayment.save() - return console.log("اشتراک پرو ساخته شد"); + return console.log('اشتراک پرو ساخته شد') } - console.log("اشتراک پرو موجود هست"); + console.log('اشتراک پرو موجود هست') const tax = await Tax.findOne({ - type: "course", - }); + type: 'course' + }) if (!tax) { const newTax = new Tax({ tax: 8, - type: "course", - }); - await newTax.save(); + type: 'course' + }) + await newTax.save() - return console.log("تکس ساخته شد"); + return console.log('تکس ساخته شد') } - console.log(" تکس موجود هست"); -}; -onServerRestart(); + console.log(' تکس موجود هست') +} +onServerRestart() const findAcademy = async (req, res, next) => { try { - const token = req.header("Authorization").split(" ")[1]; - if (!token) return res.status(401).send("Access Denied"); - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - const userId = decodedToken.id; + const 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 user = await UserModel.findById(userId); + const user = await UserModel.findById(userId) if (!user) { return res.status(422).json({ error: true, - message: "کاربر یافت نشد", - }); + message: 'کاربر یافت نشد' + }) } - const academy = await AcademyModel.findOne({ userId: userId }); + const academy = await AcademyModel.findOne({ userId }) if (!academy) { const newAcademy = new AcademyModel({ - userId: userId, - }); - await newAcademy.save(); + userId + }) + await newAcademy.save() return res.status(200).json({ - academy: newAcademy, - }); + academy: newAcademy + }) } const advertisingProfile = await AdvertisingProfileModel.findOne({ - user: userId, - }).select("contactInfo"); + user: userId + }).select('contactInfo') res.status(200).json({ academy: { ...academy.toObject(), - contactInfo: advertisingProfile?.contactInfo || null, - }, - }); + contactInfo: advertisingProfile?.contactInfo || null + } + }) } catch (error) { // در صورت بروز خطا، ارسال پیام خطا به کاربر - console.error("Error:", error); - res.status(500).json({ message: "err in creat" }); + console.error('Error:', error) + res.status(500).json({ message: 'err in creat' }) } -}; +} const findAcademyById = async (req, res, next) => { try { - const { _id } = req.body; + const { _id } = req.body - const academy = await AcademyModel.findById(_id); + const academy = await AcademyModel.findById(_id) if (!academy) { return res.status(404).json({ success: false, - message: "اکادمی موجود نیست", - }); + message: 'اکادمی موجود نیست' + }) } const owner = await UserModel.findById(academy.userId).select( - "user_name first_name last_name is_verified verify_badge is_Register profile_image user_score rate" - ); + 'user_name first_name last_name is_verified verify_badge is_Register profile_image user_score rate' + ) const packagesCount = await CourseModel.countDocuments({ academyId: academy._id, - status: coursePublicStatusFilter(), - }); + status: coursePublicStatusFilter() + }) const academyCourses = await CourseModel.find({ academyId: academy._id, - status: coursePublicStatusFilter(), - }).select("_id number_of_course_content"); + status: coursePublicStatusFilter() + }).select('_id number_of_course_content') - const courseIds = academyCourses.map((course) => course._id); + const courseIds = academyCourses.map((course) => course._id) const soldCount = courseIds.length ? await IsPaymentCourse.countDocuments({ course_id: { $in: courseIds } }) - : 0; + : 0 const totalVideos = academyCourses.reduce( (sum, course) => sum + (parseInt(course.number_of_course_content, 10) || 0), 0 - ); + ) const advertisingProfile = await AdvertisingProfileModel.findOne({ - user: academy.userId, - }).select("contactInfo"); + user: academy.userId + }).select('contactInfo') res.status(200).json({ academy: { @@ -160,86 +161,86 @@ const findAcademyById = async (req, res, next) => { stats: { packagesCount, soldCount, - totalVideos, + totalVideos }, owner, - contactInfo: advertisingProfile?.contactInfo || null, - }, - }); + contactInfo: advertisingProfile?.contactInfo || null + } + }) } catch (error) { - console.error("Error:", error); - res.status(500).json({ message: "err in creat" }); + console.error('Error:', error) + res.status(500).json({ message: 'err in creat' }) } -}; +} const academyProfile = async (req, res, next) => { try { - const token = req.header("Authorization").split(" ")[1]; - if (!token) return res.status(401).send("Access Denied"); - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - const userId = decodedToken.id; + const 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 user = await UserModel.findById(userId); + const user = await UserModel.findById(userId) if (!user) { return res.status(422).json({ error: true, - message: "کاربر یافت نشد", - }); + message: 'کاربر یافت نشد' + }) } - const academy = await AcademyModel.findOne({ userId: userId }); + const academy = await AcademyModel.findOne({ userId }) if (!academy) { return res.status(200).json({ - message: "اکادمی پیدا نشد", - }); + message: 'اکادمی پیدا نشد' + }) } - const { profile_image } = req.files; - const { name, sheba, bio, tag } = req.body; + const { profile_image } = req.files + const { name, sheba, bio, tag } = req.body if (!profile_image) { return res.status(422).json({ error: true, - message: "اطلاعات ارسالی اشتباه است", - }); + message: 'اطلاعات ارسالی اشتباه است' + }) } - let shebaDigits = String(sheba || "") - .replace(/\s/g, "") - .toUpperCase(); - if (shebaDigits.startsWith("IR")) { - shebaDigits = shebaDigits.slice(2); + let shebaDigits = String(sheba || '') + .replace(/\s/g, '') + .toUpperCase() + if (shebaDigits.startsWith('IR')) { + shebaDigits = shebaDigits.slice(2) } - shebaDigits = shebaDigits.replace(/\D/g, ""); + shebaDigits = shebaDigits.replace(/\D/g, '') if (!/^\d{24}$/.test(shebaDigits)) { return res.status(422).json({ error: true, - message: "شماره شبا باید ۲۴ رقم باشد", - }); + message: 'شماره شبا باید ۲۴ رقم باشد' + }) } - const uploadDir = path.join(__dirname, "../../../../storage/profiles"); + const uploadDir = path.join(__dirname, '../../../../storage/profiles') if (!fs.existsSync(uploadDir)) { - fs.mkdirSync(uploadDir, { recursive: true }); + fs.mkdirSync(uploadDir, { recursive: true }) } const uniqueFileName = `${name}-${Date.now()}${path.extname( profile_image.name - )}`; - const filePath = path.join(uploadDir, uniqueFileName); + )}` + const filePath = path.join(uploadDir, uniqueFileName) - await fs.move(profile_image.path, filePath); + await fs.move(profile_image.path, filePath) - academy.academy_image = `/profiles/${uniqueFileName}`; - academy.academy_name = name; - academy.sheba = `IR${shebaDigits}`; - academy.bio = bio; - academy.tag = tag; - await academy.save(); + academy.academy_image = `/profiles/${uniqueFileName}` + academy.academy_name = name + academy.sheba = `IR${shebaDigits}` + academy.bio = bio + academy.tag = tag + await academy.save() const contactPayloadPresent = req.body.contact_mobile !== undefined || req.body.contact_telegram !== undefined || req.body.contact_whatsapp !== undefined || - req.body.contact_instagram !== undefined; + req.body.contact_instagram !== undefined if (contactPayloadPresent) { const contactInfo = @@ -247,75 +248,75 @@ const academyProfile = async (req, res, next) => { mobile: null, telegramLink: null, whatsappNumber: null, - instagramLink: null, - }; + instagramLink: null + } let advertisingProfile = await AdvertisingProfileModel.findOne({ - user: userId, - }); + user: userId + }) if (!advertisingProfile) { advertisingProfile = new AdvertisingProfileModel({ user: userId, - contactInfo, - }); - await advertisingProfile.save(); + contactInfo + }) + await advertisingProfile.save() } else { - advertisingProfile.contactInfo = contactInfo; - await advertisingProfile.save(); + advertisingProfile.contactInfo = contactInfo + await advertisingProfile.save() } } res.status(200).json({ - message: "ثبت شد", - }); + message: 'ثبت شد' + }) } catch (error) { // در صورت بروز خطا، ارسال پیام خطا به کاربر - console.error("Error:", error); - res.status(500).json({ message: "err in update" }); + console.error('Error:', error) + res.status(500).json({ message: 'err in update' }) } -}; +} const parseCourseContactInfo = (body) => { - const mobile = body.contact_mobile || body["contactInfo[mobile]"] || ""; + const mobile = body.contact_mobile || body['contactInfo[mobile]'] || '' const telegramLink = - body.contact_telegram || body["contactInfo[telegramLink]"] || ""; + body.contact_telegram || body['contactInfo[telegramLink]'] || '' const whatsappNumber = - body.contact_whatsapp || body["contactInfo[whatsappNumber]"] || ""; + body.contact_whatsapp || body['contactInfo[whatsappNumber]'] || '' const instagramLink = - body.contact_instagram || body["contactInfo[instagramLink]"] || ""; + body.contact_instagram || body['contactInfo[instagramLink]'] || '' const hasAny = [mobile, telegramLink, whatsappNumber, instagramLink].some( Boolean - ); - if (!hasAny) return undefined; + ) + if (!hasAny) return undefined return { mobile: mobile || null, telegramLink: telegramLink || null, whatsappNumber: whatsappNumber || null, - instagramLink: instagramLink || null, - }; -}; + instagramLink: instagramLink || null + } +} const creatCourse = async (req, res, next) => { try { - const token = req.header("Authorization").split(" ")[1]; - if (!token) return res.status(401).send("Access Denied"); - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - const userId = decodedToken.id; + const 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 user = await UserModel.findById(userId); + const user = await UserModel.findById(userId) if (!user) { return res.status(422).json({ error: true, - message: "کاربر یافت نشد", - }); + message: 'کاربر یافت نشد' + }) } - const academy = await AcademyModel.findOne({ userId: userId }); + const academy = await AcademyModel.findOne({ userId }) if (!academy) { return res.status(200).json({ - message: "اکادمی پیدا نشد", - }); + message: 'اکادمی پیدا نشد' + }) } const { price, @@ -326,40 +327,40 @@ const creatCourse = async (req, res, next) => { course_time, number_of_course_content, teacher_name, - is_free, - } = req.body; + is_free + } = req.body - const isFreePackage = is_free === "true" || is_free === true; - const contactInfo = parseCourseContactInfo(req.body); + const isFreePackage = is_free === 'true' || is_free === true + const contactInfo = parseCourseContactInfo(req.body) - const { course_image } = req.files; + const { course_image } = req.files if (!course_image) { return res.status(422).json({ error: true, - message: "اطلاعات ارسالی اشتباه است", - }); + message: 'اطلاعات ارسالی اشتباه است' + }) } - const uploadDir = path.join(__dirname, "../../../../storage/profiles"); + const uploadDir = path.join(__dirname, '../../../../storage/profiles') if (!fs.existsSync(uploadDir)) { - fs.mkdirSync(uploadDir, { recursive: true }); + fs.mkdirSync(uploadDir, { recursive: true }) } const uniqueFileName = `${cuorse_name}-${Date.now()}${path.extname( course_image.name - )}`; - const filePath = path.join(uploadDir, uniqueFileName); + )}` + const filePath = path.join(uploadDir, uniqueFileName) - await fs.move(course_image.path, filePath); + await fs.move(course_image.path, filePath) // ✅ ایجاد رکورد جدید const newAcademy = new CourseModel({ course_image: `/profiles/${uniqueFileName}`, - price: isFreePackage ? "0" : price, + price: isFreePackage ? '0' : price, cuorse_name, category, - offer: offer || "0", + offer: offer || '0', teacher_name, academyId: academy._id, caption, @@ -367,28 +368,28 @@ const creatCourse = async (req, res, next) => { number_of_course_content, is_free: isFreePackage, ...(contactInfo ? { contactInfo } : {}), - status: 'temp_accept', - }); + status: isVerifiedBadge(user.verify_badge) ? 'temp_accept' : 'pending' + }) - await newAcademy.save(); + await newAcademy.save() res.status(200).json({ - message: "ثبت شد", - }); + message: 'ثبت شد' + }) } catch (error) { // در صورت بروز خطا، ارسال پیام خطا به کاربر - console.error("Error:", error); - res.status(500).json({ message: "err in update" }); + console.error('Error:', error) + res.status(500).json({ message: 'err in update' }) } -}; +} const updateCourse = async (req, res, next) => { try { - const token = req.header("Authorization")?.split(" ")[1]; - if (!token) return res.status(401).send("Access Denied"); + 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 decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id const { price, @@ -400,71 +401,70 @@ const updateCourse = async (req, res, next) => { caption, course_time, number_of_course_content, - is_free, - } = req.body; + is_free + } = req.body const isFreePackage = - is_free === "true" || is_free === true + is_free === 'true' || is_free === true ? true - : is_free === "false" || is_free === false + : is_free === 'false' || is_free === false ? false - : undefined; + : undefined // اعتبارسنجی courseId if (!courseId) { return res.status(422).json({ error: true, - message: "آیدی دوره الزامی است", - }); + message: 'آیدی دوره الزامی است' + }) } - const user = await UserModel.findById(userId); + const user = await UserModel.findById(userId) if (!user) { return res.status(422).json({ error: true, - message: "کاربر یافت نشد", - }); + message: 'کاربر یافت نشد' + }) } - const course = await CourseModel.findById(courseId); + const course = await CourseModel.findById(courseId) if (!course) { return res.status(422).json({ error: true, - message: "پکیجی یافت نشد", - }); + message: 'پکیجی یافت نشد' + }) } // ========== آپلود عکس جدید (اختیاری) ========== - const { course_image } = req.files || {}; + const { course_image } = req.files || {} if (course_image) { // فقط اگه عکس جدید آپلود شده بود، آپلود کن - const uploadDir = path.join(__dirname, "../../../../storage/profiles"); + const uploadDir = path.join(__dirname, '../../../../storage/profiles') if (!fs.existsSync(uploadDir)) { - fs.mkdirSync(uploadDir, { recursive: true }); + fs.mkdirSync(uploadDir, { recursive: true }) } // استفاده از id به جای نام دوره برای نام فایل const safeFileName = `course-${courseId}-${Date.now()}${path.extname( course_image.name - )}`; - const filePath = path.join(uploadDir, safeFileName); + )}` + const filePath = path.join(uploadDir, safeFileName) - await fs.move(course_image.path, filePath); - course.course_image = `/profiles/${safeFileName}`; + await fs.move(course_image.path, filePath) + course.course_image = `/profiles/${safeFileName}` } // ========== به روز رسانی فیلدها (فقط فیلدهایی که ارسال شده) ========== - if (price !== undefined) course.price = isFreePackage ? "0" : price; - if (cuorse_name !== undefined) course.cuorse_name = cuorse_name; - if (category !== undefined) course.category = category; - if (offer !== undefined) course.offer = offer || "0"; - if (caption !== undefined) course.caption = caption; - if (course_time !== undefined) course.course_time = course_time; - if (number_of_course_content !== undefined) - course.number_of_course_content = number_of_course_content; - if (teacher_name !== undefined) course.teacher_name = teacher_name; - if (isFreePackage !== undefined) course.is_free = isFreePackage; + if (price !== undefined) course.price = isFreePackage ? '0' : price + if (cuorse_name !== undefined) course.cuorse_name = cuorse_name + if (category !== undefined) course.category = category + if (offer !== undefined) course.offer = offer || '0' + if (caption !== undefined) course.caption = caption + if (course_time !== undefined) course.course_time = course_time + if (number_of_course_content !== undefined) { course.number_of_course_content = number_of_course_content } + if (teacher_name !== undefined) course.teacher_name = teacher_name + if (isFreePackage !== undefined) course.is_free = isFreePackage if ( req.body.contact_mobile !== undefined || req.body.contact_telegram !== undefined || @@ -475,112 +475,112 @@ const updateCourse = async (req, res, next) => { mobile: req.body.contact_mobile || null, telegramLink: req.body.contact_telegram || null, whatsappNumber: req.body.contact_whatsapp || null, - instagramLink: req.body.contact_instagram || null, - }; + instagramLink: req.body.contact_instagram || null + } } - await course.save(); + await course.save() res.status(200).json({ success: true, - message: "دوره با موفقیت به روز رسانی شد", + message: 'دوره با موفقیت به روز رسانی شد', data: { - course: course, - }, - }); + course + } + }) } catch (error) { - console.error("Error in updateCourse:", error); + console.error('Error in updateCourse:', error) res.status(500).json({ success: false, - message: "خطا در به روز رسانی دوره", - error: error.message, - }); + message: 'خطا در به روز رسانی دوره', + error: error.message + }) } -}; +} const deleteCourse = async (req, res, next) => { try { - const token = req.header("Authorization").split(" ")[1]; - if (!token) return res.status(401).send("Access Denied"); - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - const userId = decodedToken.id; - const { courseId } = 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 { courseId } = req.body - const user = await UserModel.findById(userId); + const user = await UserModel.findById(userId) if (!user) { return res.status(422).json({ error: true, - message: "کاربر یافت نشد", - }); + message: 'کاربر یافت نشد' + }) } - const course = await CourseModel.findById(courseId); + const course = await CourseModel.findById(courseId) if (!course) { return res.status(422).json({ error: true, - message: "پکیجی یافت نشد", - }); + message: 'پکیجی یافت نشد' + }) } - course.status = "reject"; - await course.save(); + course.status = 'reject' + await course.save() res.status(200).json({ - message: "ثبت شد", - }); + message: 'ثبت شد' + }) } catch (error) { // در صورت بروز خطا، ارسال پیام خطا به کاربر - console.error("Error:", error); - res.status(500).json({ message: "err in update" }); + console.error('Error:', error) + res.status(500).json({ message: 'err in update' }) } -}; +} const getCourse = async (req, res, next) => { try { - const token = req.header("Authorization")?.split(" ")[1]; - if (!token) return res.status(401).send("Access Denied"); + 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 decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id // گرفتن پارامترهای صفحه‌بندی - const page = parseInt(req.query.page) || 1; - const limit = parseInt(req.query.limit) || 10; - const skip = (page - 1) * limit; + const page = parseInt(req.query.page) || 1 + const limit = parseInt(req.query.limit) || 10 + const skip = (page - 1) * limit - const user = await UserModel.findById(userId); + const user = await UserModel.findById(userId) if (!user) { return res.status(422).json({ error: true, - message: "کاربر یافت نشد", - }); + message: 'کاربر یافت نشد' + }) } - const academy = await AcademyModel.findOne({ userId: userId }); + const academy = await AcademyModel.findOne({ userId }) if (!academy) { return res.status(404).json({ success: false, - message: "آکادمی پیدا نشد", - }); + message: 'آکادمی پیدا نشد' + }) } // ✅ شرط جستجو - فقط دوره‌های تایید شده const query = { academyId: academy._id, - status: coursePublicStatusFilter(), - }; + status: coursePublicStatusFilter() + } // دریافت تعداد کل دوره‌های تایید شده - const totalCourses = await CourseModel.countDocuments(query); + const totalCourses = await CourseModel.countDocuments(query) // دریافت دوره‌های تایید شده با صفحه‌بندی const courses = await CourseModel.find(query) .sort({ createdAt: -1 }) .skip(skip) - .limit(limit); + .limit(limit) // ✅ اگر دوره‌ای وجود نداشت، خطا نده، بلکه آرایه خالی برگردون if (!courses || courses.length === 0) { return res.status(200).json({ success: true, - message: "هیچ دوره تایید شده‌ای یافت نشد", + message: 'هیچ دوره تایید شده‌ای یافت نشد', data: { courses: [], pagination: { @@ -591,124 +591,124 @@ const getCourse = async (req, res, next) => { hasNextPage: false, hasPrevPage: false, nextPage: null, - prevPage: null, - }, - }, - }); + prevPage: null + } + } + }) } // محاسبات صفحه‌بندی - const totalPages = Math.ceil(totalCourses / limit); - const hasNextPage = page < totalPages; - const hasPrevPage = page > 1; + const totalPages = Math.ceil(totalCourses / limit) + const hasNextPage = page < totalPages + const hasPrevPage = page > 1 res.status(200).json({ success: true, - message: "لیست پکیج‌ها با موفقیت دریافت شد", + message: 'لیست پکیج‌ها با موفقیت دریافت شد', data: { - courses: courses, + courses, pagination: { currentPage: page, - totalPages: totalPages, + totalPages, totalItems: totalCourses, itemsPerPage: limit, - hasNextPage: hasNextPage, - hasPrevPage: hasPrevPage, + hasNextPage, + hasPrevPage, nextPage: hasNextPage ? page + 1 : null, - prevPage: hasPrevPage ? page - 1 : null, - }, - }, - }); + prevPage: hasPrevPage ? page - 1 : null + } + } + }) } catch (error) { - console.error("Error:", error); + console.error('Error:', error) res.status(500).json({ success: false, - message: "خطا در دریافت پکیج‌ها", - error: error.message, - }); + message: 'خطا در دریافت پکیج‌ها', + error: error.message + }) } -}; +} const getCourses = async (req, res, next) => { try { // ========== 3. دریافت پارامترهای صفحه‌بندی ========== - const page = parseInt(req.query.page) || 1; - const limit = parseInt(req.query.limit) || 10; - const skip = (page - 1) * limit; + const page = parseInt(req.query.page) || 1 + const limit = parseInt(req.query.limit) || 10 + const skip = (page - 1) * limit // ========== 4. دریافت پارامترهای جستجو ========== - const search = req.query.search || ""; - const category = req.query.category || ""; - const minPrice = req.query.minPrice ? parseInt(req.query.minPrice) : 0; + const search = req.query.search || '' + const category = req.query.category || '' + const minPrice = req.query.minPrice ? parseInt(req.query.minPrice) : 0 const maxPrice = req.query.maxPrice ? parseInt(req.query.maxPrice) - : 100000000; - const hasOffer = req.query.hasOffer === "true"; // تخفیف دار - const teacherName = req.query.teacherName || ""; - const type = req.query.type || ""; // normal, pro, legend - const isFree = req.query.isFree === "true"; - const status = req.query.status || "all"; // pending, accept, reject - const sortBy = req.query.sortBy || "createdAt"; - const sortOrder = req.query.sortOrder === "asc" ? 1 : -1; - const _id = req.query._id || ""; + : 100000000 + const hasOffer = req.query.hasOffer === 'true' // تخفیف دار + const teacherName = req.query.teacherName || '' + const type = req.query.type || '' // normal, pro, legend + const isFree = req.query.isFree === 'true' + const status = req.query.status || 'all' // pending, accept, reject + const sortBy = req.query.sortBy || 'createdAt' + const sortOrder = req.query.sortOrder === 'asc' ? 1 : -1 + const _id = req.query._id || '' // ========== 5. ساخت شرط جستجو (Query) ========== - let query = {}; + const query = {} // جستجو در نام دوره و کپشن if (search) { query.$or = [ - { cuorse_name: { $regex: search, $options: "i" } }, - { caption: { $regex: search, $options: "i" } }, - { teacher_name: { $regex: search, $options: "i" } }, - ]; + { cuorse_name: { $regex: search, $options: 'i' } }, + { caption: { $regex: search, $options: 'i' } }, + { teacher_name: { $regex: search, $options: 'i' } } + ] } // فیلتر بر اساس دسته بندی if (category) { - query.category = category; + query.category = category } if (_id) { - query._id = _id; + query._id = _id } // فیلتر بر اساس محدوده قیمت (تبدیل به عدد برای مقایسه) if (minPrice > 0 || maxPrice < 100000000) { query.price = { $gte: minPrice.toString(), - $lte: maxPrice.toString(), - }; + $lte: maxPrice.toString() + } } // فیلتر بر اساس تخفیف دار (offer وجود داشته باشد و از 0 بیشتر باشد) if (hasOffer) { - query.offer = { $exists: true, $ne: "0", $ne: "" }; + query.offer = { $exists: true, $ne: '0', $ne: '' } } // فیلتر بر اساس نام مدرس if (teacherName) { - query.teacher_name = { $regex: teacherName, $options: "i" }; + query.teacher_name = { $regex: teacherName, $options: 'i' } } // فیلتر بر اساس نوع دوره if (type) { - query.type = type; + query.type = type } if (isFree) { - query.is_free = true; + query.is_free = true } // فیلتر بر اساس وضعیت — در اپ، temp_accept هم نمایش داده می‌شود - if (status === "accept") { - query.status = coursePublicStatusFilter(); - } else if (status !== "all") { - query.status = status; + if (status === 'accept') { + query.status = coursePublicStatusFilter() + } else if (status !== 'all') { + query.status = status } else { - query.status = coursePublicStatusFilter(); + query.status = coursePublicStatusFilter() } // ========== 6. دریافت آمار و اطلاعات ========== // تعداد کل پکیج‌ها (بدون صفحه‌بندی) - const totalCourses = await CourseModel.countDocuments(query); + const totalCourses = await CourseModel.countDocuments(query) // مجموع قیمت‌ها (تبدیل به عدد برای محاسبه) const priceStats = await CourseModel.aggregate([ @@ -716,40 +716,40 @@ const getCourses = async (req, res, next) => { { $group: { _id: null, - total: { $sum: { $toDouble: "$price" } }, - avg: { $avg: { $toDouble: "$price" } }, - min: { $min: { $toDouble: "$price" } }, - max: { $max: { $toDouble: "$price" } }, - }, - }, - ]); + total: { $sum: { $toDouble: '$price' } }, + avg: { $avg: { $toDouble: '$price' } }, + min: { $min: { $toDouble: '$price' } }, + max: { $max: { $toDouble: '$price' } } + } + } + ]) // تعداد دوره‌های با تخفیف const offeredCourses = await CourseModel.countDocuments({ ...query, - offer: { $exists: true, $ne: "0", $ne: "" }, - }); + offer: { $exists: true, $ne: '0', $ne: '' } + }) // تعداد دوره‌ها بر اساس نوع const typeStats = await CourseModel.aggregate([ { $match: query }, { $group: { - _id: "$type", - count: { $sum: 1 }, - }, - }, - ]); + _id: '$type', + count: { $sum: 1 } + } + } + ]) // ========== 7. دریافت پکیج‌ها با صفحه‌بندی ========== const courses = await CourseModel.find(query) - .populate("user_id", "name email") // اطلاعات کاربر سازنده - .populate("academyId", "academy_name academy_image") // برای نمایش عکس/نام آموزشگاه توی لیست - .populate("likes", "name") // اطلاعات لایک‌ها + .populate('user_id', 'name email') // اطلاعات کاربر سازنده + .populate('academyId', 'academy_name academy_image') // برای نمایش عکس/نام آموزشگاه توی لیست + .populate('likes', 'name') // اطلاعات لایک‌ها .sort({ [sortBy]: sortOrder }) .skip(skip) .limit(limit) - .lean(); + .lean() // پردازش داده‌ها برای خروجی let processedCourses = courses.map((course) => ({ @@ -772,38 +772,38 @@ const getCourses = async (req, res, next) => { : course.offer ? (parseInt(course.price) * (100 - parseInt(course.offer))) / 100 : parseInt(course.price), - likesCount: course.likes?.length || 0, - })); + likesCount: course.likes?.length || 0 + })) // بلند کردن دوره‌های مرتبط با علایق کاربر داخل نتایج پیش‌فرض (بدون مرتب‌سازی دستی کاربر) - const isDefaultBrowsePath = !req.query.sortBy && !category && !search && page === 1; + const isDefaultBrowsePath = !req.query.sortBy && !category && !search && page === 1 if (isDefaultBrowsePath) { try { - const token = req.header("Authorization")?.split(" ")[1]; - const viewerId = token ? jwt.verify(token, process.env.APP_SECRET)?.id : null; + const token = req.header('Authorization')?.split(' ')[1] + const viewerId = token ? jwt.verify(token, process.env.APP_SECRET)?.id : null if (viewerId) { - const interestWeights = await buildUnifiedInterestScore(viewerId); - const tags = Object.keys(interestWeights).filter((tag) => interestWeights[tag] > 0); + const interestWeights = await buildUnifiedInterestScore(viewerId) + const tags = Object.keys(interestWeights).filter((tag) => interestWeights[tag] > 0) if (tags.length) { const interestCategories = await AcademyCategoryModel.find({ - relatedExpertise: { $in: tags }, + relatedExpertise: { $in: tags } }) - .select("title") - .lean(); - const categoryTitles = interestCategories.map((c) => c.title); + .select('title') + .lean() + const categoryTitles = interestCategories.map((c) => c.title) if (categoryTitles.length) { - const existingIds = processedCourses.map((c) => c._id); + const existingIds = processedCourses.map((c) => c._id) const interestCourses = await CourseModel.find({ ...query, category: { $in: categoryTitles }, - _id: { $nin: existingIds }, + _id: { $nin: existingIds } }) - .populate("user_id", "name email") - .populate("academyId", "academy_name academy_image") - .populate("likes", "name") + .populate('user_id', 'name email') + .populate('academyId', 'academy_name academy_image') + .populate('likes', 'name') .sort({ createdAt: -1 }) .limit(5) - .lean(); + .lean() const processedInterestCourses = interestCourses.map((course) => ({ ...course, academy_name: course.academyId?.academy_name || null, @@ -819,13 +819,13 @@ const getCourses = async (req, res, next) => { : course.offer ? (parseInt(course.price) * (100 - parseInt(course.offer))) / 100 : parseInt(course.price), - likesCount: course.likes?.length || 0, - })); + likesCount: course.likes?.length || 0 + })) processedCourses = blendInterestIntoRanked( processedCourses, [...processedCourses, ...processedInterestCourses], 0.25 - ).slice(0, limit); + ).slice(0, limit) } } } @@ -835,18 +835,18 @@ const getCourses = async (req, res, next) => { } // ========== 8. محاسبات صفحه‌بندی ========== - const totalPages = Math.ceil(totalCourses / limit); - const hasNextPage = page < totalPages; - const hasPrevPage = page > 1; + const totalPages = Math.ceil(totalCourses / limit) + const hasNextPage = page < totalPages + const hasPrevPage = page > 1 // محاسبه محدوده نمایش - const startItem = totalCourses === 0 ? 0 : (page - 1) * limit + 1; - const endItem = Math.min(page * limit, totalCourses); + const startItem = totalCourses === 0 ? 0 : (page - 1) * limit + 1 + const endItem = Math.min(page * limit, totalCourses) // ========== 9. ارسال پاسخ ========== res.status(200).json({ success: true, - message: "لیست پکیج‌ها با موفقیت دریافت شد", + message: 'لیست پکیج‌ها با موفقیت دریافت شد', data: { courses: processedCourses, stats: { @@ -856,472 +856,478 @@ const getCourses = async (req, res, next) => { minPrice: priceStats[0]?.min || 0, maxPrice: priceStats[0]?.max || 0, totalPrice: priceStats[0]?.total || 0, - typeStats: typeStats, + typeStats }, pagination: { currentPage: page, - totalPages: totalPages, + totalPages, totalItems: totalCourses, itemsPerPage: limit, - hasNextPage: hasNextPage, - hasPrevPage: hasPrevPage, + hasNextPage, + hasPrevPage, nextPage: hasNextPage ? page + 1 : null, prevPage: hasPrevPage ? page - 1 : null, - startItem: startItem, - endItem: endItem, + startItem, + endItem }, filters: { - search: search, - category: category, - minPrice: minPrice, - maxPrice: maxPrice, - hasOffer: hasOffer, - teacherName: teacherName, - type: type, - sortBy: sortBy, - sortOrder: sortOrder === 1 ? "asc" : "desc", - status: status, - }, - }, - }); + search, + category, + minPrice, + maxPrice, + hasOffer, + teacherName, + type, + sortBy, + sortOrder: sortOrder === 1 ? 'asc' : 'desc', + status + } + } + }) } catch (error) { - console.error("Error in getCourses:", error); + console.error('Error in getCourses:', error) res.status(500).json({ success: false, - message: "خطا در دریافت پکیج‌ها", - error: error.message, - }); + message: 'خطا در دریافت پکیج‌ها', + error: error.message + }) } -}; +} const proPayment = async (req, res, next) => { try { - const token = req.header("Authorization")?.split(" ")[1]; - if (!token) return res.status(401).send("Access Denied"); + 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 { courseId } = req.body; + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + const { courseId } = req.body // ========== 1. بررسی وجود CoursePayment ========== const coursePayment = await CoursePaymentModel.findOne({ - cuorse_type: "pro", - }); + cuorse_type: 'pro' + }) if (!coursePayment) { return res.status(404).json({ error: true, - message: "قیمت‌گذاری برای نوع پرو یافت نشد", - }); + message: 'قیمت‌گذاری برای نوع پرو یافت نشد' + }) } - const price = coursePayment.price; + const price = coursePayment.price // ========== 2. اعتبارسنجی قیمت ========== if (!price || isNaN(Number(price)) || Number(price) <= 0) { return res.status(422).json({ error: true, - message: "مبلغ نامعتبر است", - }); + message: 'مبلغ نامعتبر است' + }) } // ========== 3. بررسی وجود دوره ========== - const course = await CourseModel.findById(courseId); + const course = await CourseModel.findById(courseId) if (!course) { return res.status(404).json({ error: true, - message: "پکیجی یافت نشد", - }); + message: 'پکیجی یافت نشد' + }) } // ========== 4. محاسبه مبلغ ========== - const amount = Number(price) * 10; + const amount = Number(price) * 10 // مبلغ باید حداقل 1000 تومان باشد (طبق قوانین زرین‌پال) if (amount < 1000) { return res.status(422).json({ error: true, - message: "مبلغ باید حداقل 1000 تومان باشد", - }); + message: 'مبلغ باید حداقل 1000 تومان باشد' + }) } - const callbackUrl = `https://api.modstagram.com/api/v1/academy/academy/course/pro?courseId=${courseId}&userId=${userId}`; + const callbackUrl = `https://api.modstagram.com/api/v1/academy/academy/course/pro?courseId=${courseId}&userId=${userId}` // ========== 5. درخواست به زرین‌پال ========== const zarinpalRes = await axios.post( - "https://api.zarinpal.com/pg/v4/payment/request.json", + 'https://api.zarinpal.com/pg/v4/payment/request.json', { merchant_id: process.env.ZARINPAL_MERCHANT_ID || - "c7c41e8a-918f-4741-bcd5-58f3bc51db73", + 'c7c41e8a-918f-4741-bcd5-58f3bc51db73', amount: Math.round(amount), // حتماً عدد صحیح باشد description: `خرید اشتراک پرو برای ${course.cuorse_name}`, callback_url: callbackUrl, metadata: { user_id: userId, - course_id: courseId, - }, + course_id: courseId + } }, { - headers: { "Content-Type": "application/json" }, - timeout: 10000, // 10 ثانیه تایم‌اوت + headers: { 'Content-Type': 'application/json' }, + timeout: 10000 // 10 ثانیه تایم‌اوت } - ); + ) - const result = zarinpalRes.data; + const result = zarinpalRes.data // ========== 6. بررسی پاسخ زرین‌پال ========== if (result.data && result.data.code === 100) { // ذخیره اطلاعات پرداخت در دیتابیس (اختیاری) await PaymentModel.create({ authority: result.data.authority, - amount: amount, + amount, user_id: userId, course_id: courseId, - status: "pending", - type: "pro", - createdAt: new Date(), - }); + status: 'pending', + type: 'pro', + createdAt: new Date() + }) return res.status(200).json({ success: true, authority: result.data.authority, - paymentUrl: `https://www.zarinpal.com/pg/StartPay/${result.data.authority}`, - }); + paymentUrl: `https://www.zarinpal.com/pg/StartPay/${result.data.authority}` + }) } // خطای زرین‌پال - console.error("ZarinPal error:", result); + console.error('ZarinPal error:', result) return res.status(422).json({ error: true, - message: result.errors?.message || "خطا در ارتباط با درگاه پرداخت", - details: result.errors, - }); + message: result.errors?.message || 'خطا در ارتباط با درگاه پرداخت', + details: result.errors + }) } catch (error) { console.error( - "Error in proPayment:", + 'Error in proPayment:', error.response?.data || error.message - ); + ) res.status(500).json({ error: true, - message: "خطا در پردازش پرداخت", - details: error.response?.data || error.message, - }); + message: 'خطا در پردازش پرداخت', + details: error.response?.data || error.message + }) } -}; +} const handleCoursePaymentCallback = 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') try { - const { Authority, Status, userId, courseId } = req.query; + const { Authority, Status, userId, courseId } = req.query - if (Status !== "OK") { - return res.redirect(`${process.env.APP_SITE}/settings/academy/course?payment=failed`); + if (Status !== 'OK') { + return res.redirect(`${process.env.APP_SITE}/settings/academy/course?payment=failed`) } // ========== 1. پیدا کردن قیمت ========== const coursePayment = await CoursePaymentModel.findOne({ - cuorse_type: "pro", - }); + cuorse_type: 'pro' + }) if (!coursePayment) { return res.redirect( `${process.env.APP_SITE}/settings/academy/course?payment=failed&message=price_not_found` - ); + ) } - const price = coursePayment.price; - const amount = Number(price) * 10; + const price = coursePayment.price + const amount = Number(price) * 10 // ========== 2. تایید پرداخت با زرین‌پال ========== const verify = await axios.post( - "https://api.zarinpal.com/pg/v4/payment/verify.json", + 'https://api.zarinpal.com/pg/v4/payment/verify.json', { merchant_id: process.env.ZARINPAL_MERCHANT_ID || - "c7c41e8a-918f-4741-bcd5-58f3bc51db73", + 'c7c41e8a-918f-4741-bcd5-58f3bc51db73', authority: Authority, - amount: Math.round(amount), + amount: Math.round(amount) }, { - headers: { "Content-Type": "application/json" }, - timeout: 10000, + headers: { 'Content-Type': 'application/json' }, + timeout: 10000 } - ); + ) - const data = verify.data.data; + const data = verify.data.data // ========== 3. بررسی نتیجه تایید ========== if (data.code === 100) { // ثبت پرداخت موفق await PaymentModel.create({ amount: amount / 10, - status: "successful", + status: 'successful', authority: Authority, ref_id: data.ref_id, user_id: userId, course_id: courseId, - type: "course", - verifiedAt: new Date(), - }); + type: 'course', + verifiedAt: new Date() + }) // به روز رسانی نوع دوره به pro - const course = await CourseModel.findById(courseId); + const course = await CourseModel.findById(courseId) if (course) { - course.type = "pro"; - await course.save(); + course.type = 'pro' + await course.save() } // به روز رسانی وضعیت کاربر (اشتراک پرو) await UserModel.findByIdAndUpdate(userId, { $set: { - "proSubscription.active": true, - "proSubscription.courseId": courseId, - "proSubscription.startDate": new Date(), - "proSubscription.purchaseDate": new Date(), - }, - }); + 'proSubscription.active': true, + 'proSubscription.courseId': courseId, + 'proSubscription.startDate': new Date(), + 'proSubscription.purchaseDate': new Date() + } + }) return res.redirect( `${process.env.APP_SITE}/settings/academy/course?payment=success` - ); + ) } // پرداخت ناموفق return res.redirect( `${process.env.APP_SITE}/settings/academy/course?payment=failed&code=${data.code}` - ); + ) } catch (err) { - console.error("Error in handleCoursePaymentCallback:", err); + console.error('Error in handleCoursePaymentCallback:', err) return res.redirect( `${process.env.APP_SITE}/settings/academy/course?payment=failed&message=verification_error` - ); + ) } -}; +} const creatCourseVideo = async (req, res, next) => { - console.log("=== New request received ==="); - console.log("Headers:", req.headers); - console.log("Body:", req.body); - console.log("File:", req.file); + console.log('=== New request received ===') + console.log('Headers:', req.headers) + console.log('Body:', req.body) + console.log('File:', req.file) try { - const token = req.header("Authorization")?.split(" ")[1]; - if (!token) return res.status(401).send("Access Denied"); - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - const userId = decodedToken.id; + const 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 { courseId, is_free, file_name } = req.body; - const videoFile = req.file; + const { courseId, is_free, file_name } = req.body + const videoFile = req.file if (!videoFile) { return res.status(422).json({ error: true, - message: "فایل ویدیو ارسال نشده است", - }); + message: 'فایل ویدیو ارسال نشده است' + }) } if (!courseId) { return res.status(422).json({ error: true, - message: "شناسه دوره الزامی است", - }); + message: 'شناسه دوره الزامی است' + }) } // مسیر ذخیره‌سازی نهایی (مشابه سیستم پست‌ها) const finalVideoDir = path.join( __dirname, - "../../../storage/courses/videos" - ); + '../../../storage/courses/videos' + ) const safeFileName = `course-${courseId}-${Date.now()}${path.extname( videoFile.originalname - )}`; - const filePath = path.join(finalVideoDir, safeFileName); + )}` + const filePath = path.join(finalVideoDir, safeFileName) // انتقال فایل از محل موقت به محل نهایی - await fs.rename(videoFile.path, filePath); + await fs.rename(videoFile.path, filePath) + + const uploaderUser = await UserModel.findById(userId).select('verify_badge').lean() // ایجاد رکورد جدید در دیتابیس const newAcademyContent = new AcademyContentModel({ course_video: `/storage/courses/videos/${safeFileName}`, - type: "video", - is_free: is_free === "true" || is_free === true, + type: 'video', + is_free: is_free === 'true' || is_free === true, courseId, file_name: file_name || videoFile.originalname, - }); + status: isVerifiedBadge(uploaderUser?.verify_badge) ? 'temp_accept' : 'pending' + }) - await newAcademyContent.save(); + await newAcademyContent.save() - const course = await CourseModel.findById(courseId); + const course = await CourseModel.findById(courseId) if (!course) { return res.status(404).json({ success: false, - message: "پکیج پیدا نشد", - }); + message: 'پکیج پیدا نشد' + }) } course.number_of_course_content = `${ Number(course.number_of_course_content) + 1 - }`; - await course.save(); + }` + await course.save() res.status(200).json({ success: true, - message: "ویدیو با موفقیت ثبت شد", - data: newAcademyContent, - }); + message: 'ویدیو با موفقیت ثبت شد', + data: newAcademyContent + }) } catch (error) { - console.error("Error in creatCourseVideo:", error); + console.error('Error in creatCourseVideo:', error) res.status(500).json({ success: false, - message: "خطا در ثبت ویدیو", - error: error.message, - }); + message: 'خطا در ثبت ویدیو', + error: error.message + }) } -}; +} const creatCourseVideoBase64 = async (req, res, next) => { - console.log("=== Base64 Video Upload ==="); - console.log("Body:", Object.keys(req.body)); + console.log('=== Base64 Video Upload ===') + console.log('Body:', Object.keys(req.body)) try { - const token = req.header("Authorization")?.split(" ")[1]; - if (!token) return res.status(401).send("Access Denied"); - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - const userId = decodedToken.id; + const 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 { courseId, is_free, file_name, video_base64, mime_type } = req.body; + const { courseId, is_free, file_name, video_base64, mime_type } = req.body // اعتبارسنجی if (!video_base64) { return res.status(422).json({ error: true, - message: "فایل ویدیو ارسال نشده است", - }); + message: 'فایل ویدیو ارسال نشده است' + }) } if (!courseId) { return res.status(422).json({ error: true, - message: "شناسه دوره الزامی است", - }); + message: 'شناسه دوره الزامی است' + }) } // تبدیل Base64 به بافر - const base64Data = video_base64.split(",")[1] || video_base64; - const buffer = Buffer.from(base64Data, "base64"); + const base64Data = video_base64.split(',')[1] || video_base64 + const buffer = Buffer.from(base64Data, 'base64') - console.log("Buffer size:", buffer.length, "bytes"); + console.log('Buffer size:', buffer.length, 'bytes') // ایجاد دایرکتوری اگر وجود ندارد const finalVideoDir = path.join( process.cwd(), - "storage", - "courses", - "videos" - ); + 'storage', + 'courses', + 'videos' + ) if (!fs.existsSync(finalVideoDir)) { - fs.mkdirSync(finalVideoDir, { recursive: true }); + fs.mkdirSync(finalVideoDir, { recursive: true }) } // تعیین پسوند فایل از mime_type - let extension = ".mp4"; - if (mime_type === "video/mp4") extension = ".mp4"; - else if (mime_type === "video/quicktime") extension = ".mov"; - else if (mime_type === "video/x-msvideo") extension = ".avi"; - else if (mime_type === "video/webm") extension = ".webm"; + let extension = '.mp4' + if (mime_type === 'video/mp4') extension = '.mp4' + else if (mime_type === 'video/quicktime') extension = '.mov' + else if (mime_type === 'video/x-msvideo') extension = '.avi' + else if (mime_type === 'video/webm') extension = '.webm' - const safeFileName = `course-${courseId}-${Date.now()}${extension}`; - const filePath = path.join(finalVideoDir, safeFileName); + const safeFileName = `course-${courseId}-${Date.now()}${extension}` + const filePath = path.join(finalVideoDir, safeFileName) // ذخیره فایل - await fs.writeFileSync(filePath, buffer); + await fs.writeFileSync(filePath, buffer) - console.log("File saved:", filePath); + console.log('File saved:', filePath) + + const uploaderUser = await UserModel.findById(userId).select('verify_badge').lean() // ایجاد رکورد در دیتابیس const newAcademyContent = new AcademyContentModel({ course_video: `/storage/courses/videos/${safeFileName}`, - type: "video", - is_free: is_free === "true" || is_free === true, + type: 'video', + is_free: is_free === 'true' || is_free === true, courseId, - file_name: file_name, - }); + file_name, + status: isVerifiedBadge(uploaderUser?.verify_badge) ? 'temp_accept' : 'pending' + }) - await newAcademyContent.save(); + await newAcademyContent.save() // به روز رسانی تعداد محتواهای دوره - const course = await CourseModel.findById(courseId); + const course = await CourseModel.findById(courseId) if (course) { course.number_of_course_content = `${ Number(course.number_of_course_content) + 1 - }`; - await course.save(); + }` + await course.save() } res.status(200).json({ success: true, - message: "ویدیو با موفقیت ثبت شد", - data: newAcademyContent, - }); + message: 'ویدیو با موفقیت ثبت شد', + data: newAcademyContent + }) } catch (error) { - console.error("Error in creatCourseVideoBase64:", error); + console.error('Error in creatCourseVideoBase64:', error) res.status(500).json({ success: false, - message: "خطا در ثبت ویدیو", - error: error.message, - }); + message: 'خطا در ثبت ویدیو', + error: error.message + }) } -}; +} const getCourseContent = async (req, res, next) => { try { // ✅ اصلاح: دریافت courseId از query یا params - const courseId = req.query.courseId || req.params.courseId; + const courseId = req.query.courseId || req.params.courseId // اعتبارسنجی courseId if (!courseId) { return res.status(422).json({ error: true, - message: "شناسه دوره الزامی است", - }); + message: 'شناسه دوره الزامی است' + }) } // گرفتن پارامترهای صفحه‌بندی - const page = parseInt(req.query.page) || 1; - const limit = parseInt(req.query.limit) || 10; - const skip = (page - 1) * limit; + const page = parseInt(req.query.page) || 1 + const limit = parseInt(req.query.limit) || 10 + const skip = (page - 1) * limit - const academy = await CourseModel.findById(courseId); + const academy = await CourseModel.findById(courseId) if (!academy) { return res.status(404).json({ success: false, - message: "پکیج پیدا نشد", - }); + message: 'پکیج پیدا نشد' + }) } // ✅ شرط جستجو - محتوای تایید موقت و تایید کامل const query = { courseId: academy._id, - status: academyContentPublicStatusFilter(), - }; + status: academyContentPublicStatusFilter() + } // دریافت تعداد کل دوره‌های تایید شده - const totalCourses = await AcademyContentModel.countDocuments(query); + const totalCourses = await AcademyContentModel.countDocuments(query) // دریافت دوره‌های تایید شده با صفحه‌بندی const courses = await AcademyContentModel.find(query) .skip(skip) - .limit(limit); + .limit(limit) // ✅ اگر دوره‌ای وجود نداشت، خطا نده، بلکه آرایه خالی برگردون if (!courses || courses.length === 0) { return res.status(200).json({ success: true, - message: "هیچ محتوایی یافت نشد", + message: 'هیچ محتوایی یافت نشد', data: { courses: [], pagination: { @@ -1330,97 +1336,97 @@ const getCourseContent = async (req, res, next) => { totalItems: 0, itemsPerPage: limit, hasNextPage: false, - hasPrevPage: false, - }, - }, - }); + hasPrevPage: false + } + } + }) } // محاسبات صفحه‌بندی - const totalPages = Math.ceil(totalCourses / limit); - const hasNextPage = page < totalPages; - const hasPrevPage = page > 1; + const totalPages = Math.ceil(totalCourses / limit) + const hasNextPage = page < totalPages + const hasPrevPage = page > 1 res.status(200).json({ success: true, - message: "لیست محتواها با موفقیت دریافت شد", + message: 'لیست محتواها با موفقیت دریافت شد', data: { - courses: courses, + courses, pagination: { currentPage: page, - totalPages: totalPages, + totalPages, totalItems: totalCourses, itemsPerPage: limit, - hasNextPage: hasNextPage, - hasPrevPage: hasPrevPage, + hasNextPage, + hasPrevPage, nextPage: hasNextPage ? page + 1 : null, - prevPage: hasPrevPage ? page - 1 : null, - }, - }, - }); + prevPage: hasPrevPage ? page - 1 : null + } + } + }) } catch (error) { - console.error("Error in getCourseContent:", error); + console.error('Error in getCourseContent:', error) res.status(500).json({ success: false, - message: "خطا در دریافت محتواها", - error: error.message, - }); + message: 'خطا در دریافت محتواها', + error: error.message + }) } -}; +} // لایک کردن محتوا const likeCourseContent = async (req, res, next) => { try { - const token = req.header("Authorization")?.split(" ")[1]; - if (!token) return res.status(401).send("Access Denied"); + 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 decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id if (!userId) { return res.status(422).json({ error: true, - message: "برای لایک کردن وارد حساب کاربری خود شوید", - }); + message: 'برای لایک کردن وارد حساب کاربری خود شوید' + }) } - const { courseId } = req.body; + const { courseId } = req.body // بررسی وجود دوره - const course = await CourseModel.findById(courseId); + const course = await CourseModel.findById(courseId) if (!course) { return res.status(404).json({ success: false, - message: "پکیج پیدا نشد", - }); + message: 'پکیج پیدا نشد' + }) } // بررسی اینکه آیا قبلاً لایک کرده است const existingLike = await AcademyLikeModel.findOne({ user_id: userId, - course_id: courseId, - }); + course_id: courseId + }) if (existingLike) { return res.status(400).json({ success: false, - message: "شما قبلاً این دوره را لایک کرده‌اید", - }); + message: 'شما قبلاً این دوره را لایک کرده‌اید' + }) } // افزایش تعداد لایک‌های دوره - course.likes = (course.likes || 0) + 1; - await course.save(); + course.likes = (course.likes || 0) + 1 + await course.save() // ایجاد رکورد لایک await AcademyLikeModel.create({ user_id: userId, course_id: courseId, - createdAt: new Date(), - }); + createdAt: new Date() + }) - const courseOwnerId = await resolveCourseOwnerId(course); + const courseOwnerId = await resolveCourseOwnerId(course) if (!courseOwnerId) { - console.warn("likeCourseContent: course owner not found for", courseId); + console.warn('likeCourseContent: course owner not found for', courseId) } await createLikeNotification({ @@ -1429,160 +1435,160 @@ const likeCourseContent = async (req, res, next) => { likerId: userId, entityId: course._id, type: 'academy_like' - }); + }) res.status(200).json({ success: true, - message: "دوره با موفقیت لایک شد", + message: 'دوره با موفقیت لایک شد', isLiked: true, - likesCount: course.likes, - }); + likesCount: course.likes + }) } catch (error) { - console.error("Error in likeCourseContent:", error); + console.error('Error in likeCourseContent:', error) res.status(500).json({ success: false, - message: "خطا در لایک کردن محتوا", - error: error.message, - }); + message: 'خطا در لایک کردن محتوا', + error: error.message + }) } -}; +} // دیسلایک کردن محتوا (حذف لایک) const dontLikeCourseContent = async (req, res, next) => { try { - const token = req.header("Authorization")?.split(" ")[1]; - if (!token) return res.status(401).send("Access Denied"); + 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 decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id - const { courseId } = req.body; + const { courseId } = req.body // بررسی وجود دوره - const course = await CourseModel.findById(courseId); + const course = await CourseModel.findById(courseId) if (!course) { return res.status(404).json({ success: false, - message: "پکیج پیدا نشد", - }); + message: 'پکیج پیدا نشد' + }) } // پیدا کردن و حذف لایک const deletedLike = await AcademyLikeModel.findOneAndDelete({ user_id: userId, - course_id: courseId, - }); + course_id: courseId + }) if (!deletedLike) { return res.status(404).json({ success: false, - message: "شما این دوره را لایک نکرده‌اید", - }); + message: 'شما این دوره را لایک نکرده‌اید' + }) } // کاهش تعداد لایک‌های دوره - course.likes = Math.max((course.likes || 0) - 1, 0); // جلوگیری از منفی شدن - await course.save(); + course.likes = Math.max((course.likes || 0) - 1, 0) // جلوگیری از منفی شدن + await course.save() res.status(200).json({ success: true, - message: "لایک دوره با موفقیت حذف شد", + message: 'لایک دوره با موفقیت حذف شد', isLiked: false, - likesCount: course.likes, - }); + likesCount: course.likes + }) } catch (error) { - console.error("Error in dontLikeCourseContent:", error); + console.error('Error in dontLikeCourseContent:', error) res.status(500).json({ success: false, - message: "خطا در حذف لایک محتوا", - error: error.message, - }); + message: 'خطا در حذف لایک محتوا', + error: error.message + }) } -}; +} // بررسی وضعیت لایک (آیا کاربر لایک کرده است یا نه) const isLikeCourseContent = async (req, res, next) => { try { - const token = req.header("Authorization")?.split(" ")[1]; - if (!token) return res.status(401).send("Access Denied"); + 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 decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id - const { courseId } = req.body; + const { courseId } = req.body // پیدا کردن لایک const academyLike = await AcademyLikeModel.findOne({ user_id: userId, - course_id: courseId, - }); + course_id: courseId + }) res.status(200).json({ success: true, - isLiked: !!academyLike, // تبدیل به boolean - }); + isLiked: !!academyLike // تبدیل به boolean + }) } catch (error) { - console.error("Error in isLikeCourseContent:", error); + console.error('Error in isLikeCourseContent:', error) res.status(500).json({ success: false, - message: "خطا در بررسی وضعیت لایک", - error: error.message, - }); + message: 'خطا در بررسی وضعیت لایک', + error: error.message + }) } -}; +} // دریافت تعداد لایک‌های یک دوره const getCourseLikesCount = async (req, res, next) => { try { - const { courseId } = req.params; + const { courseId } = req.params - const course = await CourseModel.findById(courseId); + const course = await CourseModel.findById(courseId) if (!course) { return res.status(404).json({ success: false, - message: "دوره پیدا نشد", - }); + message: 'دوره پیدا نشد' + }) } const likesCount = await AcademyLikeModel.countDocuments({ - course_id: courseId, - }); + course_id: courseId + }) res.status(200).json({ success: true, - likesCount: likesCount, - courseLikes: course.likes || 0, - }); + likesCount, + courseLikes: course.likes || 0 + }) } catch (error) { - console.error("Error in getCourseLikesCount:", error); + console.error('Error in getCourseLikesCount:', error) res.status(500).json({ success: false, - message: "خطا در دریافت تعداد لایک‌ها", - error: error.message, - }); + message: 'خطا در دریافت تعداد لایک‌ها', + error: error.message + }) } -}; +} // دریافت همه دوره‌هایی که کاربر لایک کرده است const getUserLikedCourses = async (req, res, next) => { try { - const token = req.header("Authorization")?.split(" ")[1]; - if (!token) return res.status(401).send("Access Denied"); + 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 decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id - const page = parseInt(req.query.page) || 1; - const limit = parseInt(req.query.limit) || 10; - const skip = (page - 1) * limit; + const page = parseInt(req.query.page) || 1 + const limit = parseInt(req.query.limit) || 10 + const skip = (page - 1) * limit const likes = await AcademyLikeModel.find({ user_id: userId }) - .populate("course_id") + .populate('course_id') .sort({ createdAt: -1 }) .skip(skip) - .limit(limit); + .limit(limit) - const total = await AcademyLikeModel.countDocuments({ user_id: userId }); + const total = await AcademyLikeModel.countDocuments({ user_id: userId }) res.status(200).json({ success: true, @@ -1591,89 +1597,90 @@ const getUserLikedCourses = async (req, res, next) => { currentPage: page, totalPages: Math.ceil(total / limit), totalItems: total, - itemsPerPage: limit, - }, - }); + itemsPerPage: limit + } + }) } catch (error) { - console.error("Error in getUserLikedCourses:", error); + console.error('Error in getUserLikedCourses:', error) res.status(500).json({ success: false, - message: "خطا در دریافت دوره‌های لایک شده", - error: error.message, - }); + message: 'خطا در دریافت دوره‌های لایک شده', + error: error.message + }) } -}; +} // ==================== ساخت کامنت جدید ==================== const createComment = async (req, res) => { try { - const token = req.header("Authorization")?.split(" ")[1]; - if (!token) - return res.status(401).json({ error: true, message: "Access Denied" }); + const token = req.header('Authorization')?.split(' ')[1] + if (!token) { return res.status(401).json({ error: true, message: 'Access Denied' }) } - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - const userId = decodedToken.id; + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id - const { course_id, comment, rate } = req.body; + const { course_id, comment, rate } = req.body // اعتبارسنجی if (!course_id || !comment) { return res.status(422).json({ error: true, - message: "شناسه دوره و متن کامنت الزامی است", - }); + message: 'شناسه دوره و متن کامنت الزامی است' + }) } // بررسی وجود دوره - const course = await CourseModel.findById(course_id); + const course = await CourseModel.findById(course_id) if (!course) { return res.status(404).json({ error: true, - message: "دوره پیدا نشد", - }); + message: 'دوره پیدا نشد' + }) } - const parsedRate = Number(rate) || 0; + const parsedRate = Number(rate) || 0 const existingRatedComment = await CourseComment.findOne({ user_id: userId, course_id, - rate: { $gt: 0 }, - }); + rate: { $gt: 0 } + }) if (existingRatedComment && parsedRate > 0) { return res.status(400).json({ error: true, - message: "شما قبلاً به این دوره امتیاز داده‌اید.", - }); + message: 'شما قبلاً به این دوره امتیاز داده‌اید.' + }) } - const finalRate = existingRatedComment ? 0 : parsedRate; + const finalRate = existingRatedComment ? 0 : parsedRate + + const commentUser = await UserModel.findById(userId) + .select('first_name last_name user_name profile_image is_verified verify_badge') + .lean() // ایجاد کامنت جدید const newComment = await saveCommentWithModerationStatus( CourseComment, { user_id: userId, - course_id: course_id, - comment: comment, - rate: finalRate, + course_id, + comment, + rate: finalRate }, - 'comment' - ); + 'comment', + commentUser?.verify_badge + ) // populate کردن اطلاعات کاربر - const commentDoc = await CourseComment.findById(newComment._id).lean(); - const commentUser = await UserModel.findById(userId) - .select("first_name last_name user_name profile_image is_verified verify_badge") - .lean(); + const commentDoc = await CourseComment.findById(newComment._id).lean() const populatedComment = { ...commentDoc, - user_id: commentUser, - }; + user_id: commentUser + } - const courseOwnerId = await resolveCourseOwnerId(course); + const courseOwnerId = await resolveCourseOwnerId(course) if (!courseOwnerId) { - console.warn("createComment: course owner not found for", course_id); + console.warn('createComment: course owner not found for', course_id) } await createCommentNotification({ @@ -1682,7 +1689,7 @@ const createComment = async (req, res) => { commenterId: userId, entityId: course._id, type: 'academy_comment' - }); + }) if (finalRate > 0) { await createRatingNotification({ @@ -1690,391 +1697,388 @@ const createComment = async (req, res) => { ownerId: courseOwnerId, raterId: userId, entityId: course._id, - type: 'academy_rating', - }); + type: 'academy_rating' + }) } res.status(201).json({ success: true, - message: "کامنت با موفقیت ثبت شد", - data: populatedComment, - }); + message: 'کامنت با موفقیت ثبت شد', + data: populatedComment + }) } catch (error) { - console.error("Error in createComment:", error); - if (error?.name === "ValidationError") { + console.error('Error in createComment:', error) + if (error?.name === 'ValidationError') { return res.status(500).json({ success: false, - message: "خطا در ثبت کامنت. مدل کامنت روی سرور به‌روز نیست.", - }); + message: 'خطا در ثبت کامنت. مدل کامنت روی سرور به‌روز نیست.' + }) } res.status(500).json({ success: false, - message: "خطا در ثبت کامنت", - error: error.message, - }); + message: 'خطا در ثبت کامنت', + error: error.message + }) } -}; +} // ==================== دریافت تعداد کل کامنت‌های یک دوره ==================== const getTotalCommentsCount = async (req, res) => { try { - const { courseId } = req.params; - const { status } = req.query; // اختیاری: فقط کامنت‌های با وضعیت خاص + const { courseId } = req.params + const { status } = req.query // اختیاری: فقط کامنت‌های با وضعیت خاص - const query = { course_id: courseId }; + const query = { course_id: courseId } if (status) { - query.status = status; + query.status = status } - const totalComments = await CourseComment.countDocuments(query); + const totalComments = await CourseComment.countDocuments(query) // محاسبه میانگین امتیازات const averageRate = await CourseComment.aggregate([ - { $match: { course_id: courseId, status: "accepted" } }, - { $group: { _id: null, avgRate: { $avg: "$rate" } } }, - ]); + { $match: { course_id: courseId, status: 'accepted' } }, + { $group: { _id: null, avgRate: { $avg: '$rate' } } } + ]) res.status(200).json({ success: true, data: { - totalComments: totalComments, + totalComments, averageRate: averageRate[0]?.avgRate || 0, - courseId: courseId, - }, - }); + courseId + } + }) } catch (error) { - console.error("Error in getTotalCommentsCount:", error); + console.error('Error in getTotalCommentsCount:', error) res.status(500).json({ success: false, - message: "خطا در دریافت تعداد کامنت‌ها", - error: error.message, - }); + message: 'خطا در دریافت تعداد کامنت‌ها', + error: error.message + }) } -}; +} // ==================== دریافت کامنت‌ها با فیلترهای مختلف ==================== const getCourseComments = async (req, res) => { try { - const { courseId } = req.params; + const { courseId } = req.params const { page = 1, limit = 10, - status = "accepted", // 'accepted', 'pending', 'rejected', 'all' + status = 'accepted', // 'accepted', 'pending', 'rejected', 'all' minRate, maxRate, - sortBy = "createdAt", - sortOrder = "desc", - userId, // اختیاری: کامنت‌های یک کاربر خاص - } = req.query; + sortBy = 'createdAt', + sortOrder = 'desc', + userId // اختیاری: کامنت‌های یک کاربر خاص + } = req.query // ساخت شرط جستجو - const query = { course_id: courseId }; + const query = { course_id: courseId } // فیلتر بر اساس وضعیت - if (status === "accepted") { - query.status = commentStatusFilter(); - } else if (status !== "all") { - query.status = status; + if (status === 'accepted') { + query.status = commentStatusFilter() + } else if (status !== 'all') { + query.status = status } else { - query.status = commentStatusFilter(); + query.status = commentStatusFilter() } // فیلتر بر اساس امتیاز if (minRate || maxRate) { - query.rate = {}; - if (minRate) query.rate.$gte = parseInt(minRate); - if (maxRate) query.rate.$lte = parseInt(maxRate); + query.rate = {} + if (minRate) query.rate.$gte = parseInt(minRate) + if (maxRate) query.rate.$lte = parseInt(maxRate) } // فیلتر بر اساس کاربر خاص if (userId) { - query.user_id = userId; + query.user_id = userId } // تنظیمات مرتب‌سازی - const sortOptions = {}; - sortOptions[sortBy] = sortOrder === "desc" ? -1 : 1; + const sortOptions = {} + sortOptions[sortBy] = sortOrder === 'desc' ? -1 : 1 // دریافت کامنت‌ها با pagination - const skip = (parseInt(page) - 1) * parseInt(limit); + const skip = (parseInt(page) - 1) * parseInt(limit) const [comments, totalComments] = await Promise.all([ CourseComment.find(query) .sort(sortOptions) .skip(skip) .limit(parseInt(limit)) - .populate("user_id", "name username profileImage"), - CourseComment.countDocuments(query), - ]); + .populate('user_id', 'name username profileImage'), + CourseComment.countDocuments(query) + ]) // محاسبه آمار کامنت‌ها const stats = await CourseComment.aggregate([ - { $match: { course_id: courseId, status: "accepted" } }, + { $match: { course_id: courseId, status: 'accepted' } }, { $group: { _id: null, totalAccepted: { $sum: 1 }, - averageRate: { $avg: "$rate" }, + averageRate: { $avg: '$rate' }, rateDistribution: { - $push: "$rate", - }, - }, - }, - ]); + $push: '$rate' + } + } + } + ]) - const totalPages = Math.ceil(totalComments / parseInt(limit)); + const totalPages = Math.ceil(totalComments / parseInt(limit)) res.status(200).json({ success: true, - message: "لیست کامنت‌ها با موفقیت دریافت شد", + message: 'لیست کامنت‌ها با موفقیت دریافت شد', data: { - comments: comments, + comments, pagination: { currentPage: parseInt(page), - totalPages: totalPages, + totalPages, totalItems: totalComments, itemsPerPage: parseInt(limit), hasNextPage: parseInt(page) < totalPages, - hasPrevPage: parseInt(page) > 1, + hasPrevPage: parseInt(page) > 1 }, stats: { - totalComments: totalComments, + totalComments, acceptedComments: stats[0]?.totalAccepted || 0, averageRate: Math.round((stats[0]?.averageRate || 0) * 10) / 10, - rateDistribution: stats[0]?.rateDistribution || [], - }, - }, - }); + rateDistribution: stats[0]?.rateDistribution || [] + } + } + }) } catch (error) { - console.error("Error in getCourseComments:", error); + console.error('Error in getCourseComments:', error) res.status(500).json({ success: false, - message: "خطا در دریافت کامنت‌ها", - error: error.message, - }); + message: 'خطا در دریافت کامنت‌ها', + error: error.message + }) } -}; +} // ==================== ویرایش وضعیت کامنت (برای همه قابل استفاده است) ==================== const updateCommentStatus = async (req, res) => { try { - const token = req.header("Authorization")?.split(" ")[1]; - if (!token) - return res.status(401).json({ error: true, message: "Access Denied" }); + const token = req.header('Authorization')?.split(' ')[1] + if (!token) { return res.status(401).json({ error: true, message: 'Access Denied' }) } - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - const userId = decodedToken.id; + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id - const { commentId } = req.params; - const { status } = req.body; // 'pending', 'accepted', 'rejected' + const { commentId } = req.params + const { status } = req.body // 'pending', 'accepted', 'rejected' // اعتبارسنجی وضعیت - if (!["pending", "accepted", "rejected"].includes(status)) { + if (!['pending', 'accepted', 'rejected'].includes(status)) { return res.status(422).json({ error: true, message: - "وضعیت نامعتبر است. وضعیت باید یکی از این موارد باشد: pending, accepted, rejected", - }); + 'وضعیت نامعتبر است. وضعیت باید یکی از این موارد باشد: pending, accepted, rejected' + }) } // پیدا کردن کامنت - const comment = await CourseComment.findById(commentId); + const comment = await CourseComment.findById(commentId) if (!comment) { return res.status(404).json({ error: true, - message: "کامنت پیدا نشد", - }); + message: 'کامنت پیدا نشد' + }) } // بررسی دسترسی: یا خود صاحب کامنت است یا هر کاربر دیگری (طبق درخواست شما که همه بتوانند) // اگر می‌خواهید فقط ادمین بتواند وضعیت را تغییر دهد، این بخش را تغییر دهید - const user = await UserModel.findById(userId); - const isOwner = comment.user_id.toString() === userId; - const isAdmin = user?.role === "admin"; + const user = await UserModel.findById(userId) + const isOwner = comment.user_id.toString() === userId + const isAdmin = user?.role === 'admin' if (!isOwner && !isAdmin) { return res.status(403).json({ error: true, - message: "شما اجازه تغییر وضعیت این کامنت را ندارید", - }); + message: 'شما اجازه تغییر وضعیت این کامنت را ندارید' + }) } // به روز رسانی وضعیت - comment.status = status; - await comment.save(); + comment.status = status + await comment.save() // اگر کامنت تایید شد و امتیاز داشت، میانگین امتیاز دوره را به روز کن - if (status === "accepted" && comment.rate > 0) { + if (status === 'accepted' && comment.rate > 0) { const averageRate = await CourseComment.aggregate([ - { $match: { course_id: comment.course_id, status: "accepted" } }, - { $group: { _id: null, avgRate: { $avg: "$rate" } } }, - ]); + { $match: { course_id: comment.course_id, status: 'accepted' } }, + { $group: { _id: null, avgRate: { $avg: '$rate' } } } + ]) await CourseModel.findByIdAndUpdate(comment.course_id, { - averageRate: averageRate[0]?.avgRate || 0, - }); + averageRate: averageRate[0]?.avgRate || 0 + }) } res.status(200).json({ success: true, message: `وضعیت کامنت با موفقیت به ${ - status === "accepted" - ? "تایید شده" - : status === "rejected" - ? "رد شده" - : "در انتظار" + status === 'accepted' + ? 'تایید شده' + : status === 'rejected' + ? 'رد شده' + : 'در انتظار' } تغییر یافت`, - data: comment, - }); + data: comment + }) } catch (error) { - console.error("Error in updateCommentStatus:", error); + console.error('Error in updateCommentStatus:', error) res.status(500).json({ success: false, - message: "خطا در تغییر وضعیت کامنت", - error: error.message, - }); + message: 'خطا در تغییر وضعیت کامنت', + error: error.message + }) } -}; +} // ==================== دریافت کامنت‌های یک کاربر خاص ==================== const getUserComments = async (req, res) => { try { - const token = req.header("Authorization")?.split(" ")[1]; - if (!token) - return res.status(401).json({ error: true, message: "Access Denied" }); + const token = req.header('Authorization')?.split(' ')[1] + if (!token) { return res.status(401).json({ error: true, message: 'Access Denied' }) } - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - const userId = decodedToken.id; + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id - const { page = 1, limit = 10, status = "all", courseId } = req.query; + const { page = 1, limit = 10, status = 'all', courseId } = req.query - const query = { user_id: userId }; + const query = { user_id: userId } - if (status !== "all") { - query.status = status; + if (status !== 'all') { + query.status = status } if (courseId) { - query.course_id = courseId; + query.course_id = courseId } - const skip = (parseInt(page) - 1) * parseInt(limit); + const skip = (parseInt(page) - 1) * parseInt(limit) const [comments, totalComments] = await Promise.all([ CourseComment.find(query) .sort({ createdAt: -1 }) .skip(skip) .limit(parseInt(limit)) - .populate("course_id", "cuorse_name packageImage"), - CourseComment.countDocuments(query), - ]); + .populate('course_id', 'cuorse_name packageImage'), + CourseComment.countDocuments(query) + ]) - const totalPages = Math.ceil(totalComments / parseInt(limit)); + const totalPages = Math.ceil(totalComments / parseInt(limit)) res.status(200).json({ success: true, data: { - comments: comments, + comments, pagination: { currentPage: parseInt(page), - totalPages: totalPages, + totalPages, totalItems: totalComments, - itemsPerPage: parseInt(limit), - }, - }, - }); + itemsPerPage: parseInt(limit) + } + } + }) } catch (error) { - console.error("Error in getUserComments:", error); + console.error('Error in getUserComments:', error) res.status(500).json({ success: false, - message: "خطا در دریافت کامنت‌های کاربر", - error: error.message, - }); + message: 'خطا در دریافت کامنت‌های کاربر', + error: error.message + }) } -}; +} // ==================== حذف کامنت (اختیاری) ==================== const deleteComment = async (req, res) => { try { - const token = req.header("Authorization")?.split(" ")[1]; - if (!token) - return res.status(401).json({ error: true, message: "Access Denied" }); + const token = req.header('Authorization')?.split(' ')[1] + if (!token) { return res.status(401).json({ error: true, message: 'Access Denied' }) } - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - const userId = decodedToken.id; + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id - const { commentId } = req.params; + const { commentId } = req.params - const comment = await CourseComment.findById(commentId); + const comment = await CourseComment.findById(commentId) if (!comment) { return res.status(404).json({ error: true, - message: "کامنت پیدا نشد", - }); + message: 'کامنت پیدا نشد' + }) } // بررسی دسترسی: یا خود صاحب کامنت است یا ادمین - const user = await UserModel.findById(userId); - const isOwner = comment.user_id.toString() === userId; - const isAdmin = user?.role === "admin"; + const user = await UserModel.findById(userId) + const isOwner = comment.user_id.toString() === userId + const isAdmin = user?.role === 'admin' if (!isOwner && !isAdmin) { return res.status(403).json({ error: true, - message: "شما اجازه حذف این کامنت را ندارید", - }); + message: 'شما اجازه حذف این کامنت را ندارید' + }) } - await comment.deleteOne(); + await comment.deleteOne() res.status(200).json({ success: true, - message: "کامنت با موفقیت حذف شد", - }); + message: 'کامنت با موفقیت حذف شد' + }) } catch (error) { - console.error("Error in deleteComment:", error); + console.error('Error in deleteComment:', error) res.status(500).json({ success: false, - message: "خطا در حذف کامنت", - error: error.message, - }); + message: 'خطا در حذف کامنت', + error: error.message + }) } -}; +} // academyController.js const coursePayment = async (req, res, next) => { try { - const token = req.header("Authorization")?.split(" ")[1]; + const token = req.header('Authorization')?.split(' ')[1] if (!token) { return res.status(401).json({ error: true, - message: "لطفا وارد حساب کاربری خود شوید", - }); + message: 'لطفا وارد حساب کاربری خود شوید' + }) } - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - const userId = decodedToken.id; - const { courseId } = req.body; + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + const { courseId } = req.body // ========== 1. بررسی وجود دوره ========== - const course = await CourseModel.findById(courseId); + const course = await CourseModel.findById(courseId) if (!course) { return res.status(404).json({ error: true, - message: "دوره مورد نظر یافت نشد", - }); + message: 'دوره مورد نظر یافت نشد' + }) } // ========== 2. بررسی خرید قبلی ========== const existingPurchase = await IsPaymentCourse.findOne({ user_id: userId, - course_id: course._id, - }); + course_id: course._id + }) if (existingPurchase) { return res.status(400).json({ error: true, - message: "شما قبلاً این دوره را خریداری کرده‌اید", - }); + message: 'شما قبلاً این دوره را خریداری کرده‌اید' + }) } if (course.is_free) { @@ -2082,69 +2086,69 @@ const coursePayment = async (req, res, next) => { user_id: userId, course_id: course._id, price_paid: 0, - purchased_at: new Date(), - }); + purchased_at: new Date() + }) - const courseOwnerId = await resolveCourseOwnerId(course); + const courseOwnerId = await resolveCourseOwnerId(course) await createCoursePurchaseNotification({ ownerId: courseOwnerId, buyerId: userId, - courseId: course._id, - }); + courseId: course._id + }) return res.status(200).json({ success: true, free: true, - message: "پکیج رایگان با موفقیت ثبت شد", - }); + message: 'پکیج رایگان با موفقیت ثبت شد' + }) } // ========== 3. دریافت نرخ مالیات ========== - let taxRate = 9; // پیش‌فرض 9 درصد - const tax = await Tax.findOne({ type: "course" }); + let taxRate = 9 // پیش‌فرض 9 درصد + const tax = await Tax.findOne({ type: 'course' }) if (tax && tax.tax) { - taxRate = Number(tax.tax); + taxRate = Number(tax.tax) } // ========== 4. محاسبه قیمت ========== - const offerNum = Number(course.offer) || 0; - const priceNum = Number(course.price); + const offerNum = Number(course.offer) || 0 + const priceNum = Number(course.price) // قیمت پس از تخفیف - const discountedPrice = (priceNum / 100) * (100 - offerNum); + const discountedPrice = (priceNum / 100) * (100 - offerNum) // محاسبه مالیات - const taxAmount = discountedPrice * (taxRate / 100); + const taxAmount = discountedPrice * (taxRate / 100) // قیمت نهایی با مالیات (تومان) - const finalPrice = discountedPrice; - const finalTaxPrice = (discountedPrice / 100) * (100 - taxRate); + const finalPrice = discountedPrice + const finalTaxPrice = (discountedPrice / 100) * (100 - taxRate) // ========== 5. اعتبارسنجی قیمت ========== if (!finalPrice || isNaN(finalPrice) || finalPrice <= 0) { return res.status(422).json({ error: true, - message: "مبلغ نامعتبر است", - }); + message: 'مبلغ نامعتبر است' + }) } // تبدیل به ریال (ضرب در 10 چون هر تومان = 10 ریال) - const amountInRials = Math.round(finalPrice * 10); + const amountInRials = Math.round(finalPrice * 10) // مبلغ باید حداقل 1000 تومان باشد (10000 ریال) if (amountInRials < 10000) { return res.status(422).json({ error: true, - message: "مبلغ باید حداقل 1000 تومان باشد", - }); + message: 'مبلغ باید حداقل 1000 تومان باشد' + }) } // ========== 6. ساخت کال‌بک URL ========== - const callbackUrl = `https://api.modstagram.com/api/v1/academy/academy/course/payment/verify?courseId=${courseId}&userId=${userId}`; + const callbackUrl = `https://api.modstagram.com/api/v1/academy/academy/course/payment/verify?courseId=${courseId}&userId=${userId}` // ========== 7. درخواست به زرین‌پال ========== const zarinpalRes = await axios.post( - "https://api.zarinpal.com/pg/v4/payment/request.json", + 'https://api.zarinpal.com/pg/v4/payment/request.json', { merchant_id: process.env.ZARINPAL_MERCHANT_ID, amount: amountInRials, @@ -2154,16 +2158,16 @@ const coursePayment = async (req, res, next) => { user_id: userId, course_id: courseId, price: finalPrice, - tax_rate: taxRate, - }, + tax_rate: taxRate + } }, { - headers: { "Content-Type": "application/json" }, - timeout: 90000, + headers: { 'Content-Type': 'application/json' }, + timeout: 90000 } - ); + ) - const result = zarinpalRes.data; + const result = zarinpalRes.data // ========== 8. بررسی پاسخ زرین‌پال ========== if (result.data && result.data.code === 100) { @@ -2172,120 +2176,120 @@ const coursePayment = async (req, res, next) => { price: finalPrice, discountAmount: priceNum - discountedPrice, taxAmount: finalTaxPrice, - taxRate: taxRate, + taxRate, course_name: course.cuorse_name, course_id: course._id, academy_id: course.academyId, user_id: userId, payment_authority: result.data.authority, - status: "pending", - }); + status: 'pending' + }) return res.status(200).json({ success: true, authority: result.data.authority, - paymentUrl: `https://www.zarinpal.com/pg/StartPay/${result.data.authority}`, - }); + paymentUrl: `https://www.zarinpal.com/pg/StartPay/${result.data.authority}` + }) } // خطای زرین‌پال - console.error("ZarinPal error:", result); + console.error('ZarinPal error:', result) return res.status(422).json({ error: true, - message: result.errors?.message || "خطا در ارتباط با درگاه پرداخت", - details: result.errors, - }); + message: result.errors?.message || 'خطا در ارتباط با درگاه پرداخت', + details: result.errors + }) } catch (error) { console.error( - "Error in coursePayment:", + 'Error in coursePayment:', error.response?.data || error.message - ); + ) res.status(500).json({ error: true, - message: "خطا در پردازش پرداخت", - details: error.response?.data || error.message, - }); + message: 'خطا در پردازش پرداخت', + details: error.response?.data || error.message + }) } -}; +} const CoursePaymentCallback = async (req, res) => { try { - const { Authority, Status, courseId, userId } = req.query; + const { Authority, Status, courseId, userId } = req.query // ========== 1. بررسی وضعیت پرداخت ========== - if (Status !== "OK") { + if (Status !== 'OK') { return res.redirect( `${process.env.APP_SITE}/settings/academy/course?payment=failed&message=payment_canceled` - ); + ) } // ========== 2. پیدا کردن دوره ========== - const course = await CourseModel.findById(courseId); + const course = await CourseModel.findById(courseId) if (!course) { return res.redirect( `${process.env.APP_SITE}/settings/academy/course?payment=failed&message=course_not_found` - ); + ) } // ========== 3. پیدا کردن رکورد پرداخت ========== const paymentRecord = await AcademyPaymentModel.findOne({ payment_authority: Authority, course_id: courseId, - user_id: userId, - }); + user_id: userId + }) if (!paymentRecord) { return res.redirect( `${process.env.APP_SITE}/settings/academy/course?payment=failed&message=payment_not_found` - ); + ) } // ========== 4. محاسبه مبلغ ========== - const amountInRials = Math.round(paymentRecord.price * 10); + const amountInRials = Math.round(paymentRecord.price * 10) // ========== 5. تایید پرداخت با زرین‌پال ========== const verify = await axios.post( - "https://api.zarinpal.com/pg/v4/payment/verify.json", + 'https://api.zarinpal.com/pg/v4/payment/verify.json', { merchant_id: process.env.ZARINPAL_MERCHANT_ID, authority: Authority, - amount: amountInRials, + amount: amountInRials }, { - headers: { "Content-Type": "application/json" }, - timeout: 10000, + headers: { 'Content-Type': 'application/json' }, + timeout: 10000 } - ); + ) - const data = verify.data.data; + const data = verify.data.data // ========== 6. بررسی نتیجه تایید ========== if (data.code === 100) { // به‌روزرسانی رکورد پرداخت await AcademyPaymentModel.findByIdAndUpdate(paymentRecord._id, { - status: "success", - payment_ref_id: data.ref_id, - }); + status: 'success', + payment_ref_id: data.ref_id + }) // ثبت خرید کاربر await IsPaymentCourse.create({ user_id: userId, course_id: course._id, price_paid: paymentRecord.price, - purchased_at: new Date(), - }); + purchased_at: new Date() + }) - const courseOwnerId = await resolveCourseOwnerId(course); + const courseOwnerId = await resolveCourseOwnerId(course) await createCoursePurchaseNotification({ ownerId: courseOwnerId, buyerId: userId, - courseId: course._id, - }); + courseId: course._id + }) if (courseOwnerId) { const sellerNetAmount = Math.round( paymentRecord.price * (1 - ACADEMY_COMMISSION_RATE) - ); + ) if (sellerNetAmount > 0) { await WalletTransactionModel.create({ user: courseOwnerId, @@ -2295,113 +2299,111 @@ const CoursePaymentCallback = async (req, res) => { referenceType: 'payment', referenceId: paymentRecord._id, description: `درآمد فروش پکیج آموزشی «${course.title || ''}» (پس از کسر ۱۵٪ کارمزد)` - }); + }) } } // هدایت به صفحه موفقیت return res.redirect( `${process.env.APP_SITE}/settings/academy/my-courses?payment=success` - ); + ) } // پرداخت ناموفق await AcademyPaymentModel.findByIdAndUpdate(paymentRecord._id, { - status: "failed", - }); + status: 'failed' + }) return res.redirect( `${process.env.APP_SITE}/settings/academy/course?payment=failed&code=${data.code}` - ); + ) } catch (err) { - console.error("Error in CoursePaymentCallback:", err); + console.error('Error in CoursePaymentCallback:', err) return res.redirect( `${process.env.APP_SITE}/settings/academy/course?payment=failed&message=verification_error` - ); + ) } -}; +} const getUserPurchasedCourses = async (req, res) => { try { - const token = req.header("Authorization")?.split(" ")[1]; - if (!token) - return res.status(401).json({ error: true, message: "Access Denied" }); + const token = req.header('Authorization')?.split(' ')[1] + if (!token) { return res.status(401).json({ error: true, message: 'Access Denied' }) } - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - const user_id = decodedToken.id; + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const user_id = decodedToken.id - const { page = 1, limit = 10 } = req.query; + const { page = 1, limit = 10 } = req.query // پیدا کردن تمام رکوردهای پرداخت برای این کاربر - const payments = await IsPaymentCourse.find({ user_id: user_id }) + const payments = await IsPaymentCourse.find({ user_id }) .sort({ createdAt: -1 }) // جدیدترین اول .skip((page - 1) * limit) - .limit(parseInt(limit)); + .limit(parseInt(limit)) // گرفتن اطلاعات کامل دوره‌ها - const courseIds = payments.map((payment) => payment.course_id); + const courseIds = payments.map((payment) => payment.course_id) const courses = await CourseModel.find({ _id: { $in: courseIds }, - status: coursePublicStatusFilter(), // فقط دوره‌های تایید شده - }); + status: coursePublicStatusFilter() // فقط دوره‌های تایید شده + }) // محاسبه تعداد کل - const total = await IsPaymentCourse.countDocuments({ user_id: user_id }); + const total = await IsPaymentCourse.countDocuments({ user_id }) res.status(200).json({ success: true, data: { - courses: courses, + courses, pagination: { total, page: parseInt(page), pages: Math.ceil(total / limit), - limit: parseInt(limit), - }, - }, - }); + limit: parseInt(limit) + } + } + }) } catch (error) { - console.error("Error in getUserPurchasedCourses:", error); + console.error('Error in getUserPurchasedCourses:', error) res.status(500).json({ success: false, - message: "خطا در دریافت دوره‌های خریداری شده", - error: error.message, - }); + message: 'خطا در دریافت دوره‌های خریداری شده', + error: error.message + }) } -}; +} // API با populate (روش بهتر) const getUserPurchasedCoursesWithPopulate = async (req, res) => { try { - const token = req.header("Authorization")?.split(" ")[1]; - if (!token) - return res.status(401).json({ error: true, message: "Access Denied" }); + const token = req.header('Authorization')?.split(' ')[1] + if (!token) { return res.status(401).json({ error: true, message: 'Access Denied' }) } - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - const user_id = decodedToken.id; + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const user_id = decodedToken.id - const { page = 1, limit = 10 } = req.query; + const { page = 1, limit = 10 } = req.query const options = { page: parseInt(page), limit: parseInt(limit), sort: { createdAt: -1 }, populate: { - path: "course_id", - model: "Course", - match: { status: coursePublicStatusFilter() }, - }, - }; + path: 'course_id', + model: 'Course', + match: { status: coursePublicStatusFilter() } + } + } const result = await IsPaymentCourse.paginate( - { user_id: user_id }, + { user_id }, options - ); + ) // فیلتر کردن دوره‌هایی که populate شده‌اند و وجود دارند const validCourses = result.docs .filter((doc) => doc.course_id !== null) - .map((doc) => doc.course_id); + .map((doc) => doc.course_id) res.status(200).json({ success: true, @@ -2413,70 +2415,70 @@ const getUserPurchasedCoursesWithPopulate = async (req, res) => { totalPages: result.totalPages, limit: result.limit, hasNextPage: result.hasNextPage, - hasPrevPage: result.hasPrevPage, - }, - }, - }); + hasPrevPage: result.hasPrevPage + } + } + }) } catch (error) { - console.error("Error:", error); + console.error('Error:', error) res.status(500).json({ success: false, - message: "خطا در دریافت دوره‌های خریداری شده", - }); + message: 'خطا در دریافت دوره‌های خریداری شده' + }) } -}; +} const getAllAcademyPayments = async (req, res) => { try { // 1. بررسی توکن - const token = req.header("Authorization")?.split(" ")[1]; + const token = req.header('Authorization')?.split(' ')[1] if (!token) { return res.status(401).json({ success: false, - message: "دسترسی غیرمجاز", - }); + message: 'دسترسی غیرمجاز' + }) } // 2. verify توکن - let decodedToken; + let decodedToken try { - decodedToken = jwt.verify(token, process.env.APP_SECRET); + decodedToken = jwt.verify(token, process.env.APP_SECRET) } catch (jwtError) { return res.status(401).json({ success: false, - message: "توکن نامعتبر است", - }); + message: 'توکن نامعتبر است' + }) } - const userId = decodedToken.id; + const userId = decodedToken.id // 3. پیدا کردن آکادمی - const academy = await AcademyModel.findOne({ userId: userId }); + const academy = await AcademyModel.findOne({ userId }) if (!academy) { return res.status(404).json({ success: false, - message: "آکادمی برای این کاربر یافت نشد", - }); + message: 'آکادمی برای این کاربر یافت نشد' + }) } - const academy_id = academy._id; - const { page = 1, limit = 10, status } = req.query; + const academy_id = academy._id + const { page = 1, limit = 10, status } = req.query // 4. ساخت فیلتر - let filter = { academy_id: academy_id }; + const filter = { academy_id } if ( status && - ["pending", "success", "failed", "settled"].includes(status) + ['pending', 'success', 'failed', 'settled'].includes(status) ) { - filter.status = status; + filter.status = status } // 5. تنظیمات صفحه‌بندی - const pageNum = parseInt(page); - const limitNum = parseInt(limit); - const skip = (pageNum - 1) * limitNum; + const pageNum = parseInt(page) + const limitNum = parseInt(limit) + const skip = (pageNum - 1) * limitNum const options = { page: pageNum, @@ -2484,34 +2486,34 @@ const getAllAcademyPayments = async (req, res) => { sort: { createdAt: -1 }, populate: [ { - path: "course_id", - model: "Course", - select: "cuorse_name price course_image category teacher_name", + path: 'course_id', + model: 'Course', + select: 'cuorse_name price course_image category teacher_name' }, { - path: "user_id", - model: "User", - select: "user_name profile_image email phone", - }, - ], - }; + path: 'user_id', + model: 'User', + select: 'user_name profile_image email phone' + } + ] + } // 6. دریافت پرداخت‌ها - const result = await AcademyPaymentModel.paginate(filter, options); + const result = await AcademyPaymentModel.paginate(filter, options) // 7. محاسبه آمار دقیق‌تر - let totalAmount = 0; - let pendingCount = 0; - let acceptedCount = 0; + let totalAmount = 0 + let pendingCount = 0 + let acceptedCount = 0 if (result.docs && Array.isArray(result.docs)) { totalAmount = result.docs.reduce((sum, p) => { - const price = parseInt(p.taxAmount) || 0; - return sum + price; - }, 0); + const price = parseInt(p.taxAmount) || 0 + return sum + price + }, 0) - pendingCount = result.docs.filter((p) => p.status === "pending").length; - acceptedCount = result.docs.filter((p) => p.status === "accept").length; + pendingCount = result.docs.filter((p) => p.status === 'pending').length + acceptedCount = result.docs.filter((p) => p.status === 'accept').length } // 8. پاسخ نهایی @@ -2522,7 +2524,7 @@ const getAllAcademyPayments = async (req, res) => { _id: academy._id, academy_name: academy.academy_name, academy_image: academy.academy_image, - bio: academy.bio, + bio: academy.bio }, payments: result.docs || [], pagination: { @@ -2531,202 +2533,201 @@ const getAllAcademyPayments = async (req, res) => { totalPages: result.totalPages || 1, limit: result.limit || limitNum, hasNextPage: result.hasNextPage || false, - hasPrevPage: result.hasPrevPage || false, + hasPrevPage: result.hasPrevPage || false }, stats: { - totalAmount: totalAmount, - pendingCount: pendingCount, - acceptedCount: acceptedCount, - }, - }, - }); + totalAmount, + pendingCount, + acceptedCount + } + } + }) } catch (error) { - console.error("Error in getAllAcademyPayments:", error); + console.error('Error in getAllAcademyPayments:', error) res.status(500).json({ success: false, - message: "خطا در دریافت لیست پرداخت‌ها", - error: process.env.NODE_ENV === "development" ? error.message : undefined, - }); + message: 'خطا در دریافت لیست پرداخت‌ها', + error: process.env.NODE_ENV === 'development' ? error.message : undefined + }) } -}; +} -const ANALYTICS_RANGE_DAYS = { "7d": 7, "30d": 30, "90d": 90, all: null }; +const ANALYTICS_RANGE_DAYS = { '7d': 7, '30d': 30, '90d': 90, all: null } /** Revenue-over-time + best-selling courses for the seller's Academy — backs the "Academy" analytics tab. */ const getAcademyAnalytics = async (req, res) => { try { - const token = req.header("Authorization")?.split(" ")[1]; - if (!token) return res.status(401).json({ success: false, message: "دسترسی غیرمجاز" }); + const token = req.header('Authorization')?.split(' ')[1] + if (!token) return res.status(401).json({ success: false, message: 'دسترسی غیرمجاز' }) - let decodedToken; + let decodedToken try { - decodedToken = jwt.verify(token, process.env.APP_SECRET); + decodedToken = jwt.verify(token, process.env.APP_SECRET) } catch { - return res.status(401).json({ success: false, message: "توکن نامعتبر است" }); + return res.status(401).json({ success: false, message: 'توکن نامعتبر است' }) } - const academy = await AcademyModel.findOne({ userId: decodedToken.id }); + const academy = await AcademyModel.findOne({ userId: decodedToken.id }) if (!academy) { return res.json({ - range: req.query.range || "30d", + range: req.query.range || '30d', series: [], totals: { totalRevenue: 0, totalCount: 0 }, topItems: [], - rating: { average: academy?.rate || 0, count: academy?.number_of_rate || 0 }, - }); + rating: { average: academy?.rate || 0, count: academy?.number_of_rate || 0 } + }) } - const range = ANALYTICS_RANGE_DAYS[req.query.range] !== undefined ? req.query.range : "30d"; - const days = ANALYTICS_RANGE_DAYS[range]; - const since = days ? new Date(Date.now() - days * 24 * 60 * 60 * 1000) : new Date(0); + const range = ANALYTICS_RANGE_DAYS[req.query.range] !== undefined ? req.query.range : '30d' + const days = ANALYTICS_RANGE_DAYS[range] + const since = days ? new Date(Date.now() - days * 24 * 60 * 60 * 1000) : new Date(0) const match = { academy_id: academy._id, - status: { $in: ["success", "settled"] }, - createdAt: { $gte: since }, - }; + status: { $in: ['success', 'settled'] }, + createdAt: { $gte: since } + } const [series, totalsAgg, bestCoursesAgg] = await Promise.all([ AcademyPaymentModel.aggregate([ { $match: match }, { $group: { - _id: { $dateToString: { format: "%Y-%m-%d", date: "$createdAt" } }, - revenue: { $sum: "$price" }, - count: { $sum: 1 }, - }, + _id: { $dateToString: { format: '%Y-%m-%d', date: '$createdAt' } }, + revenue: { $sum: '$price' }, + count: { $sum: 1 } + } }, - { $sort: { _id: 1 } }, + { $sort: { _id: 1 } } ]), AcademyPaymentModel.aggregate([ { $match: match }, - { $group: { _id: null, totalRevenue: { $sum: "$price" }, totalCount: { $sum: 1 } } }, + { $group: { _id: null, totalRevenue: { $sum: '$price' }, totalCount: { $sum: 1 } } } ]), AcademyPaymentModel.aggregate([ { $match: match }, - { $group: { _id: "$course_id", revenue: { $sum: "$price" }, count: { $sum: 1 } } }, + { $group: { _id: '$course_id', revenue: { $sum: '$price' }, count: { $sum: 1 } } }, { $sort: { revenue: -1 } }, - { $limit: 5 }, - ]), - ]); + { $limit: 5 } + ]) + ]) - const courseIds = bestCoursesAgg.map((row) => row._id).filter(Boolean); + const courseIds = bestCoursesAgg.map((row) => row._id).filter(Boolean) const courses = courseIds.length - ? await CourseModel.find({ _id: { $in: courseIds } }).select("cuorse_name course_image") - : []; - const courseMap = new Map(courses.map((c) => [String(c._id), c])); + ? await CourseModel.find({ _id: { $in: courseIds } }).select('cuorse_name course_image') + : [] + const courseMap = new Map(courses.map((c) => [String(c._id), c])) const topItems = bestCoursesAgg .filter((row) => row._id && courseMap.has(String(row._id))) .map((row) => { - const course = courseMap.get(String(row._id)); + const course = courseMap.get(String(row._id)) return { id: String(row._id), title: course.cuorse_name, image: course.course_image || null, - metric: row.revenue, - }; - }); + metric: row.revenue + } + }) return res.json({ range, series, totals: totalsAgg[0] || { totalRevenue: 0, totalCount: 0 }, topItems, - rating: { average: academy.rate || 0, count: academy.number_of_rate || 0 }, - }); + rating: { average: academy.rate || 0, count: academy.number_of_rate || 0 } + }) } catch (error) { - console.error("Error in getAcademyAnalytics:", error); - res.status(500).json({ success: false, message: "خطا در دریافت آنالیتیکس آموزشگاه" }); + console.error('Error in getAcademyAnalytics:', error) + res.status(500).json({ success: false, message: 'خطا در دریافت آنالیتیکس آموزشگاه' }) } -}; +} const checkCoursePurchase = async (req, res) => { try { - const token = req.header("Authorization")?.split(" ")[1]; - if (!token) - return res.status(401).json({ error: true, message: "Access Denied" }); + const token = req.header('Authorization')?.split(' ')[1] + if (!token) { return res.status(401).json({ error: true, message: 'Access Denied' }) } - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - const userId = decodedToken.id; - const { courseId } = req.params; + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + const { courseId } = req.params // بررسی وجود کاربر و دوره if (!userId || !courseId) { return res.status(400).json({ success: false, - message: "userId و courseId الزامی هستند", - }); + message: 'userId و courseId الزامی هستند' + }) } // جستجو در دیتابیس const purchase = await IsPaymentCourse.findOne({ user_id: userId, - course_id: courseId, - }); + course_id: courseId + }) // نتیجه if (purchase) { return res.status(200).json({ success: true, isPurchased: true, - message: "این دوره قبلاً خریداری شده است", - purchaseData: purchase, - }); + message: 'این دوره قبلاً خریداری شده است', + purchaseData: purchase + }) } else { return res.status(200).json({ success: true, isPurchased: false, - message: "این دوره خریداری نشده است", - }); + message: 'این دوره خریداری نشده است' + }) } } catch (error) { - console.error("Error in checkCoursePurchase:", error); + console.error('Error in checkCoursePurchase:', error) res.status(500).json({ success: false, - message: "خطا در بررسی خرید دوره", - error: error.message, - }); + message: 'خطا در بررسی خرید دوره', + error: error.message + }) } -}; +} const getAcademyCourse = async (req, res, next) => { try { - const { Id } = req.params; + const { Id } = req.params // گرفتن پارامترهای صفحه‌بندی - const page = parseInt(req.query.page) || 1; - const limit = parseInt(req.query.limit) || 10; - const skip = (page - 1) * limit; + const page = parseInt(req.query.page) || 1 + const limit = parseInt(req.query.limit) || 10 + const skip = (page - 1) * limit - const academy = await AcademyModel.findById(Id); + const academy = await AcademyModel.findById(Id) if (!academy) { return res.status(404).json({ success: false, - message: "آکادمی پیدا نشد", - }); + message: 'آکادمی پیدا نشد' + }) } // ✅ شرط جستجو - فقط دوره‌های تایید شده const query = { academyId: academy._id, - status: coursePublicStatusFilter(), - }; + status: coursePublicStatusFilter() + } // دریافت تعداد کل دوره‌های تایید شده - const totalCourses = await CourseModel.countDocuments(query); + const totalCourses = await CourseModel.countDocuments(query) // دریافت دوره‌های تایید شده با صفحه‌بندی const courses = await CourseModel.find(query) .sort({ createdAt: -1 }) .skip(skip) - .limit(limit); + .limit(limit) // ✅ اگر دوره‌ای وجود نداشت، خطا نده، بلکه آرایه خالی برگردون if (!courses || courses.length === 0) { return res.status(200).json({ success: true, - message: "هیچ دوره تایید شده‌ای یافت نشد", + message: 'هیچ دوره تایید شده‌ای یافت نشد', data: { courses: [], pagination: { @@ -2737,68 +2738,68 @@ const getAcademyCourse = async (req, res, next) => { hasNextPage: false, hasPrevPage: false, nextPage: null, - prevPage: null, - }, - }, - }); + prevPage: null + } + } + }) } // محاسبات صفحه‌بندی - const totalPages = Math.ceil(totalCourses / limit); - const hasNextPage = page < totalPages; - const hasPrevPage = page > 1; + const totalPages = Math.ceil(totalCourses / limit) + const hasNextPage = page < totalPages + const hasPrevPage = page > 1 res.status(200).json({ success: true, - message: "لیست پکیج‌ها با موفقیت دریافت شد", + message: 'لیست پکیج‌ها با موفقیت دریافت شد', data: { - courses: courses, + courses, pagination: { currentPage: page, - totalPages: totalPages, + totalPages, totalItems: totalCourses, itemsPerPage: limit, - hasNextPage: hasNextPage, - hasPrevPage: hasPrevPage, + hasNextPage, + hasPrevPage, nextPage: hasNextPage ? page + 1 : null, - prevPage: hasPrevPage ? page - 1 : null, - }, - }, - }); + prevPage: hasPrevPage ? page - 1 : null + } + } + }) } catch (error) { - console.error("Error:", error); + console.error('Error:', error) res.status(500).json({ success: false, - message: "خطا در دریافت پکیج‌ها", - error: error.message, - }); + message: 'خطا در دریافت پکیج‌ها', + error: error.message + }) } -}; +} // ==================== مدیریت پرداخت‌ها ==================== const getAllPayments = async (req, res) => { try { - const { page = 1, limit = 20, status, startDate, endDate } = req.query; - const skip = (parseInt(page) - 1) * parseInt(limit); + const { page = 1, limit = 20, status, startDate, endDate } = req.query + const skip = (parseInt(page) - 1) * parseInt(limit) - let query = {}; - if (status) query.status = status; + const query = {} + if (status) query.status = status if (startDate && endDate) { query.createdAt = { $gte: new Date(startDate), - $lte: new Date(endDate), - }; + $lte: new Date(endDate) + } } const payments = await AcademyPaymentModel.find(query) - .populate("user_id", "user_name profile_image email") - .populate("course_id", "cuorse_name price course_image") - .populate("academy_id", "academy_name academy_image sheba") + .populate('user_id', 'user_name profile_image email') + .populate('course_id', 'cuorse_name price course_image') + .populate('academy_id', 'academy_name academy_image sheba') .sort({ createdAt: -1 }) .skip(skip) - .limit(parseInt(limit)); + .limit(parseInt(limit)) - const total = await AcademyPaymentModel.countDocuments(query); + const total = await AcademyPaymentModel.countDocuments(query) // آمار کلی پرداخت‌ها const stats = await AcademyPaymentModel.aggregate([ @@ -2806,20 +2807,20 @@ const getAllPayments = async (req, res) => { { $group: { _id: null, - totalAmount: { $sum: { $toDouble: "$price" } }, + totalAmount: { $sum: { $toDouble: '$price' } }, totalCount: { $sum: 1 }, pendingCount: { - $sum: { $cond: [{ $eq: ["$status", "pending"] }, 1, 0] }, + $sum: { $cond: [{ $eq: ['$status', 'pending'] }, 1, 0] } }, settledCount: { - $sum: { $cond: [{ $eq: ["$status", "settled"] }, 1, 0] }, + $sum: { $cond: [{ $eq: ['$status', 'settled'] }, 1, 0] } }, successCount: { - $sum: { $cond: [{ $eq: ["$status", "success"] }, 1, 0] }, - }, - }, - }, - ]); + $sum: { $cond: [{ $eq: ['$status', 'success'] }, 1, 0] } + } + } + } + ]) res.status(200).json({ success: true, @@ -2830,79 +2831,79 @@ const getAllPayments = async (req, res) => { totalCount: 0, pendingCount: 0, settledCount: 0, - successCount: 0, + successCount: 0 }, pagination: { currentPage: parseInt(page), totalPages: Math.ceil(total / parseInt(limit)), totalItems: total, - itemsPerPage: parseInt(limit), - }, - }, - }); + itemsPerPage: parseInt(limit) + } + } + }) } catch (error) { - console.error(error); + console.error(error) res .status(500) - .json({ success: false, message: "خطا در دریافت پرداخت‌ها" }); + .json({ success: false, message: 'خطا در دریافت پرداخت‌ها' }) } -}; +} const updatePaymentStatus = async (req, res) => { try { - const { paymentId, status } = req.body; + const { paymentId, status } = req.body const payment = await AcademyPaymentModel.findByIdAndUpdate( paymentId, { status, updatedAt: new Date() }, { new: true } - ); + ) if (!payment) { return res .status(404) - .json({ success: false, message: "پرداخت یافت نشد" }); + .json({ success: false, message: 'پرداخت یافت نشد' }) } res.status(200).json({ success: true, - message: "وضعیت پرداخت با موفقیت تغییر کرد", - data: payment, - }); + message: 'وضعیت پرداخت با موفقیت تغییر کرد', + data: payment + }) } catch (error) { - console.error(error); + console.error(error) res .status(500) - .json({ success: false, message: "خطا در تغییر وضعیت پرداخت" }); + .json({ success: false, message: 'خطا در تغییر وضعیت پرداخت' }) } -}; +} // ==================== مدیریت دوره‌ها ==================== const getAllCourses = async (req, res) => { try { - const { page = 1, limit = 20, status, type, category, search } = req.query; - const skip = (parseInt(page) - 1) * parseInt(limit); + const { page = 1, limit = 20, status, type, category, search } = req.query + const skip = (parseInt(page) - 1) * parseInt(limit) - let query = {}; - if (status) query.status = status; - if (type) query.type = type; - if (category) query.category = category; + const query = {} + if (status) query.status = status + if (type) query.type = type + if (category) query.category = category if (search) { query.$or = [ - { cuorse_name: { $regex: search, $options: "i" } }, - { caption: { $regex: search, $options: "i" } }, - { teacher_name: { $regex: search, $options: "i" } }, - ]; + { cuorse_name: { $regex: search, $options: 'i' } }, + { caption: { $regex: search, $options: 'i' } }, + { teacher_name: { $regex: search, $options: 'i' } } + ] } const courses = await CourseModel.find(query) - .populate("academyId", "academy_name academy_image") - .populate("user_id", "user_name profile_image") + .populate('academyId', 'academy_name academy_image') + .populate('user_id', 'user_name profile_image') .sort({ createdAt: -1 }) .skip(skip) - .limit(parseInt(limit)); + .limit(parseInt(limit)) - const total = await CourseModel.countDocuments(query); + const total = await CourseModel.countDocuments(query) // آمار دوره‌ها const stats = await CourseModel.aggregate([ @@ -2911,23 +2912,23 @@ const getAllCourses = async (req, res) => { $group: { _id: null, totalCourses: { $sum: 1 }, - totalPrice: { $sum: { $toDouble: "$price" } }, + totalPrice: { $sum: { $toDouble: '$price' } }, normalCount: { - $sum: { $cond: [{ $eq: ["$type", "normal"] }, 1, 0] }, + $sum: { $cond: [{ $eq: ['$type', 'normal'] }, 1, 0] } }, - proCount: { $sum: { $cond: [{ $eq: ["$type", "pro"] }, 1, 0] } }, + proCount: { $sum: { $cond: [{ $eq: ['$type', 'pro'] }, 1, 0] } }, legendCount: { - $sum: { $cond: [{ $eq: ["$type", "legend"] }, 1, 0] }, + $sum: { $cond: [{ $eq: ['$type', 'legend'] }, 1, 0] } }, pendingCount: { - $sum: { $cond: [{ $eq: ["$status", "pending"] }, 1, 0] }, + $sum: { $cond: [{ $eq: ['$status', 'pending'] }, 1, 0] } }, acceptCount: { - $sum: { $cond: [{ $eq: ["$status", "accept"] }, 1, 0] }, - }, - }, - }, - ]); + $sum: { $cond: [{ $eq: ['$status', 'accept'] }, 1, 0] } + } + } + } + ]) res.status(200).json({ success: true, @@ -2938,90 +2939,90 @@ const getAllCourses = async (req, res) => { currentPage: parseInt(page), totalPages: Math.ceil(total / parseInt(limit)), totalItems: total, - itemsPerPage: parseInt(limit), - }, - }, - }); + itemsPerPage: parseInt(limit) + } + } + }) } catch (error) { - console.error(error); - res.status(500).json({ success: false, message: "خطا در دریافت دوره‌ها" }); + console.error(error) + res.status(500).json({ success: false, message: 'خطا در دریافت دوره‌ها' }) } -}; +} const updateCourseStatus = async (req, res) => { try { - const { courseId, status, type } = req.body; + const { courseId, status, type } = req.body - console.log(courseId, status, type); + console.log(courseId, status, type) - const course = await CourseModel.findById(courseId); + const course = await CourseModel.findById(courseId) if (!course) { - return res.status(404).json({ success: false, message: "دوره یافت نشد" }); + return res.status(404).json({ success: false, message: 'دوره یافت نشد' }) } - if (status) course.status = status; - if (type) course.type = type; - await course.save(); + if (status) course.status = status + if (type) course.type = type + await course.save() res.status(200).json({ success: true, - message: "دوره با موفقیت به‌روزرسانی شد", + message: 'دوره با موفقیت به‌روزرسانی شد', data: course, - courseId: courseId, - status: status, - type: type, - }); + courseId, + status, + type + }) } catch (error) { - console.error(error); + console.error(error) res .status(500) - .json({ success: false, message: "خطا در به‌روزرسانی دوره" }); + .json({ success: false, message: 'خطا در به‌روزرسانی دوره' }) } -}; +} const admindeleteCourse = async (req, res) => { try { - const { courseId } = req.params; + const { courseId } = req.params - const course = await CourseModel.findByIdAndDelete(courseId); + const course = await CourseModel.findByIdAndDelete(courseId) if (!course) { - return res.status(404).json({ success: false, message: "دوره یافت نشد" }); + return res.status(404).json({ success: false, message: 'دوره یافت نشد' }) } // حذف تمام پرداخت‌های مرتبط با این دوره - await AcademyPaymentModel.deleteMany({ course_id: courseId }); + await AcademyPaymentModel.deleteMany({ course_id: courseId }) res.status(200).json({ success: true, - message: "دوره با موفقیت حذف شد", - }); + message: 'دوره با موفقیت حذف شد' + }) } catch (error) { - console.error(error); - res.status(500).json({ success: false, message: "خطا در حذف دوره" }); + console.error(error) + res.status(500).json({ success: false, message: 'خطا در حذف دوره' }) } -}; +} // ==================== مدیریت دوره‌های خریداری شده ==================== const getAllPurchasedCourses = async (req, res) => { try { - const { page = 1, limit = 20, userId, courseId, status } = req.query; - const skip = (parseInt(page) - 1) * parseInt(limit); + const { page = 1, limit = 20, userId, courseId, status } = req.query + const skip = (parseInt(page) - 1) * parseInt(limit) - let query = {}; - if (userId) query.user_id = userId; - if (courseId) query.course_id = courseId; - if (status) query.status = status; + const query = {} + if (userId) query.user_id = userId + if (courseId) query.course_id = courseId + if (status) query.status = status const purchases = await IsPaymentCourse.find(query) - .populate("user_id", "user_name profile_image email phone") - .populate("course_id", "cuorse_name price course_image teacher_name") + .populate('user_id', 'user_name profile_image email phone') + .populate('course_id', 'cuorse_name price course_image teacher_name') .sort({ createdAt: -1 }) .skip(skip) - .limit(parseInt(limit)); + .limit(parseInt(limit)) - const total = await IsPaymentCourse.countDocuments(query); + const total = await IsPaymentCourse.countDocuments(query) // آمار خریدها const stats = await IsPaymentCourse.aggregate([ @@ -3030,10 +3031,10 @@ const getAllPurchasedCourses = async (req, res) => { $group: { _id: null, totalPurchases: { $sum: 1 }, - uniqueUsers: { $addToSet: "$user_id" }, - }, - }, - ]); + uniqueUsers: { $addToSet: '$user_id' } + } + } + ]) res.status(200).json({ success: true, @@ -3041,42 +3042,42 @@ const getAllPurchasedCourses = async (req, res) => { purchases, stats: { totalPurchases: stats[0]?.totalPurchases || 0, - uniqueUsers: stats[0]?.uniqueUsers?.length || 0, + uniqueUsers: stats[0]?.uniqueUsers?.length || 0 }, pagination: { currentPage: parseInt(page), totalPages: Math.ceil(total / parseInt(limit)), totalItems: total, - itemsPerPage: parseInt(limit), - }, - }, - }); + itemsPerPage: parseInt(limit) + } + } + }) } catch (error) { - console.error(error); + console.error(error) res .status(500) - .json({ success: false, message: "خطا در دریافت دوره‌های خریداری شده" }); + .json({ success: false, message: 'خطا در دریافت دوره‌های خریداری شده' }) } -}; +} // ==================== مدیریت آکادمی‌ها ==================== const getAllAcademies = async (req, res) => { try { - const { page = 1, limit = 20, search } = req.query; - const skip = (parseInt(page) - 1) * parseInt(limit); + const { page = 1, limit = 20, search } = req.query + const skip = (parseInt(page) - 1) * parseInt(limit) - let query = {}; + const query = {} if (search) { - query.academy_name = { $regex: search, $options: "i" }; + query.academy_name = { $regex: search, $options: 'i' } } const academies = await AcademyModel.find(query) - .populate("userId", "user_name profile_image email") + .populate('userId', 'user_name profile_image email') .sort({ createdAt: -1 }) .skip(skip) - .limit(parseInt(limit)); + .limit(parseInt(limit)) - const total = await AcademyModel.countDocuments(query); + const total = await AcademyModel.countDocuments(query) // آمار آکادمی‌ها const stats = await AcademyModel.aggregate([ @@ -3084,11 +3085,11 @@ const getAllAcademies = async (req, res) => { $group: { _id: null, totalAcademies: { $sum: 1 }, - totalRate: { $sum: "$rate" }, - avgRate: { $avg: "$rate" }, - }, - }, - ]); + totalRate: { $sum: '$rate' }, + avgRate: { $avg: '$rate' } + } + } + ]) res.status(200).json({ success: true, @@ -3099,56 +3100,56 @@ const getAllAcademies = async (req, res) => { currentPage: parseInt(page), totalPages: Math.ceil(total / parseInt(limit)), totalItems: total, - itemsPerPage: parseInt(limit), - }, - }, - }); + itemsPerPage: parseInt(limit) + } + } + }) } catch (error) { - console.error(error); + console.error(error) res .status(500) - .json({ success: false, message: "خطا در دریافت آکادمی‌ها" }); + .json({ success: false, message: 'خطا در دریافت آکادمی‌ها' }) } -}; +} const updateAcademyStatus = async (req, res) => { try { - const { academyId, status, rate, sheba } = req.body; + const { academyId, status, rate, sheba } = req.body - const updateData = {}; - if (status) updateData.status = status; - if (rate) updateData.rate = rate; - if (sheba) updateData.sheba = sheba; + const updateData = {} + if (status) updateData.status = status + if (rate) updateData.rate = rate + if (sheba) updateData.sheba = sheba const academy = await AcademyModel.findByIdAndUpdate( academyId, updateData, { new: true } - ); + ) if (!academy) { return res .status(404) - .json({ success: false, message: "آکادمی یافت نشد" }); + .json({ success: false, message: 'آکادمی یافت نشد' }) } res.status(200).json({ success: true, - message: "آکادمی با موفقیت به‌روزرسانی شد", - data: academy, - }); + message: 'آکادمی با موفقیت به‌روزرسانی شد', + data: academy + }) } catch (error) { - console.error(error); + console.error(error) res .status(500) - .json({ success: false, message: "خطا در به‌روزرسانی آکادمی" }); + .json({ success: false, message: 'خطا در به‌روزرسانی آکادمی' }) } -}; +} // ==================== مدیریت تنظیمات (تکس و قیمت پرو) ==================== const getSettings = async (req, res) => { try { - let settings = await SettingModel.findOne(); + let settings = await SettingModel.findOne() if (!settings) { settings = await SettingModel.create({ @@ -3156,19 +3157,19 @@ const getSettings = async (req, res) => { proPrice: 99000, legendPrice: 199000, minWithdrawAmount: 50000, - maxWithdrawAmount: 10000000, - }); + maxWithdrawAmount: 10000000 + }) } res.status(200).json({ success: true, - data: settings, - }); + data: settings + }) } catch (error) { - console.error(error); - res.status(500).json({ success: false, message: "خطا در دریافت تنظیمات" }); + console.error(error) + res.status(500).json({ success: false, message: 'خطا در دریافت تنظیمات' }) } -}; +} const updateSettings = async (req, res) => { try { @@ -3177,8 +3178,8 @@ const updateSettings = async (req, res) => { proPrice, legendPrice, minWithdrawAmount, - maxWithdrawAmount, - } = req.body; + maxWithdrawAmount + } = req.body const settings = await SettingModel.findOneAndUpdate( {}, @@ -3188,109 +3189,109 @@ const updateSettings = async (req, res) => { legendPrice, minWithdrawAmount, maxWithdrawAmount, - updatedAt: new Date(), + updatedAt: new Date() }, { new: true, upsert: true } - ); + ) res.status(200).json({ success: true, - message: "تنظیمات با موفقیت به‌روزرسانی شد", - data: settings, - }); + message: 'تنظیمات با موفقیت به‌روزرسانی شد', + data: settings + }) } catch (error) { - console.error(error); + console.error(error) res .status(500) - .json({ success: false, message: "خطا در به‌روزرسانی تنظیمات" }); + .json({ success: false, message: 'خطا در به‌روزرسانی تنظیمات' }) } -}; +} const getCoursePrices = async (req, res) => { try { - const proPrice = await CoursePaymentModel.findOne({ cuorse_type: "pro" }); + const proPrice = await CoursePaymentModel.findOne({ cuorse_type: 'pro' }) const legendPrice = await CoursePaymentModel.findOne({ - cuorse_type: "legend", - }); + cuorse_type: 'legend' + }) const normalPrice = await CoursePaymentModel.findOne({ - cuorse_type: "normal", - }); + cuorse_type: 'normal' + }) res.status(200).json({ success: true, data: { pro: proPrice ? { price: proPrice.price, _id: proPrice._id } - : { price: "60000" }, + : { price: '60000' }, legend: legendPrice ? { price: legendPrice.price, _id: legendPrice._id } - : { price: "199000" }, + : { price: '199000' }, normal: normalPrice ? { price: normalPrice.price, _id: normalPrice._id } - : { price: "0" }, - }, - }); + : { price: '0' } + } + }) } catch (error) { - console.error(error); + console.error(error) res .status(500) - .json({ success: false, message: "خطا در دریافت قیمت دوره‌ها" }); + .json({ success: false, message: 'خطا در دریافت قیمت دوره‌ها' }) } -}; +} const updateCoursePrice = async (req, res) => { try { - const { courseType, price } = req.body; + const { courseType, price } = req.body if (!courseType || !price) { return res .status(400) - .json({ success: false, message: "نوع دوره و قیمت الزامی است" }); + .json({ success: false, message: 'نوع دوره و قیمت الزامی است' }) } let coursePayment = await CoursePaymentModel.findOne({ - cuorse_type: courseType, - }); + cuorse_type: courseType + }) if (coursePayment) { - coursePayment.price = price.toString(); - await coursePayment.save(); + coursePayment.price = price.toString() + await coursePayment.save() } else { coursePayment = new CoursePaymentModel({ price: price.toString(), - cuorse_type: courseType, - }); - await coursePayment.save(); + cuorse_type: courseType + }) + await coursePayment.save() } res.status(200).json({ success: true, message: `قیمت دوره ${ - courseType === "pro" - ? "پرو" - : courseType === "legend" - ? "لجند" - : "معمولی" + courseType === 'pro' + ? 'پرو' + : courseType === 'legend' + ? 'لجند' + : 'معمولی' } با موفقیت به‌روزرسانی شد`, - data: coursePayment, - }); + data: coursePayment + }) } catch (error) { - console.error(error); + console.error(error) res .status(500) - .json({ success: false, message: "خطا در به‌روزرسانی قیمت دوره" }); + .json({ success: false, message: 'خطا در به‌روزرسانی قیمت دوره' }) } -}; +} const getTax = async (req, res) => { try { - let tax = await Tax.findOne({ type: "course" }); + let tax = await Tax.findOne({ type: 'course' }) if (!tax) { - tax = new Tax({ tax: 8, type: "course" }); - await tax.save(); + tax = new Tax({ tax: 8, type: 'course' }) + await tax.save() } res.status(200).json({ @@ -3298,59 +3299,59 @@ const getTax = async (req, res) => { data: { tax: tax.tax, _id: tax._id, - type: tax.type, - }, - }); + type: tax.type + } + }) } catch (error) { - console.error(error); - res.status(500).json({ success: false, message: "خطا در دریافت مالیات" }); + console.error(error) + res.status(500).json({ success: false, message: 'خطا در دریافت مالیات' }) } -}; +} const updateTax = async (req, res) => { try { - const { tax } = req.body; + const { tax } = req.body if (tax === undefined || tax === null) { return res .status(400) - .json({ success: false, message: "مقدار مالیات الزامی است" }); + .json({ success: false, message: 'مقدار مالیات الزامی است' }) } - let taxRecord = await Tax.findOne({ type: "course" }); + let taxRecord = await Tax.findOne({ type: 'course' }) if (taxRecord) { - taxRecord.tax = tax; - await taxRecord.save(); + taxRecord.tax = tax + await taxRecord.save() } else { - taxRecord = new Tax({ tax: tax, type: "course" }); - await taxRecord.save(); + taxRecord = new Tax({ tax, type: 'course' }) + await taxRecord.save() } res.status(200).json({ success: true, - message: "مالیات با موفقیت به‌روزرسانی شد", - data: taxRecord, - }); + message: 'مالیات با موفقیت به‌روزرسانی شد', + data: taxRecord + }) } catch (error) { - console.error(error); + console.error(error) res .status(500) - .json({ success: false, message: "خطا در به‌روزرسانی مالیات" }); + .json({ success: false, message: 'خطا در به‌روزرسانی مالیات' }) } -}; +} const getAllSettings = async (req, res) => { try { - const proPrice = await CoursePaymentModel.findOne({ cuorse_type: "pro" }); + const proPrice = await CoursePaymentModel.findOne({ cuorse_type: 'pro' }) const legendPrice = await CoursePaymentModel.findOne({ - cuorse_type: "legend", - }); + cuorse_type: 'legend' + }) - let tax = await Tax.findOne({ type: "course" }); + let tax = await Tax.findOne({ type: 'course' }) if (!tax) { - tax = new Tax({ tax: 8, type: "course" }); - await tax.save(); + tax = new Tax({ tax: 8, type: 'course' }) + await tax.save() } res.status(200).json({ @@ -3358,73 +3359,73 @@ const getAllSettings = async (req, res) => { data: { tax: tax.tax, prices: { - pro: proPrice ? proPrice.price : "60000", - legend: legendPrice ? legendPrice.price : "199000", - }, - }, - }); + pro: proPrice ? proPrice.price : '60000', + legend: legendPrice ? legendPrice.price : '199000' + } + } + }) } catch (error) { - console.error(error); - res.status(500).json({ success: false, message: "خطا در دریافت تنظیمات" }); + console.error(error) + res.status(500).json({ success: false, message: 'خطا در دریافت تنظیمات' }) } -}; +} const updateAllSettings = async (req, res) => { try { - const { tax, proPrice, legendPrice } = req.body; + const { tax, proPrice, legendPrice } = req.body if (tax !== undefined) { - let taxRecord = await Tax.findOne({ type: "course" }); + let taxRecord = await Tax.findOne({ type: 'course' }) if (taxRecord) { - taxRecord.tax = tax; - await taxRecord.save(); + taxRecord.tax = tax + await taxRecord.save() } else { - taxRecord = new Tax({ tax: tax, type: "course" }); - await taxRecord.save(); + taxRecord = new Tax({ tax, type: 'course' }) + await taxRecord.save() } } if (proPrice !== undefined) { - let proRecord = await CoursePaymentModel.findOne({ cuorse_type: "pro" }); + let proRecord = await CoursePaymentModel.findOne({ cuorse_type: 'pro' }) if (proRecord) { - proRecord.price = proPrice.toString(); - await proRecord.save(); + proRecord.price = proPrice.toString() + await proRecord.save() } else { proRecord = new CoursePaymentModel({ price: proPrice.toString(), - cuorse_type: "pro", - }); - await proRecord.save(); + cuorse_type: 'pro' + }) + await proRecord.save() } } if (legendPrice !== undefined) { let legendRecord = await CoursePaymentModel.findOne({ - cuorse_type: "legend", - }); + cuorse_type: 'legend' + }) if (legendRecord) { - legendRecord.price = legendPrice.toString(); - await legendRecord.save(); + legendRecord.price = legendPrice.toString() + await legendRecord.save() } else { legendRecord = new CoursePaymentModel({ price: legendPrice.toString(), - cuorse_type: "legend", - }); - await legendRecord.save(); + cuorse_type: 'legend' + }) + await legendRecord.save() } } res.status(200).json({ success: true, - message: "تنظیمات با موفقیت به‌روزرسانی شد", - }); + message: 'تنظیمات با موفقیت به‌روزرسانی شد' + }) } catch (error) { - console.error(error); + console.error(error) res .status(500) - .json({ success: false, message: "خطا در به‌روزرسانی تنظیمات" }); + .json({ success: false, message: 'خطا در به‌روزرسانی تنظیمات' }) } -}; +} const getPaymentStats = async (req, res) => { try { @@ -3432,85 +3433,85 @@ const getPaymentStats = async (req, res) => { { $group: { _id: null, - totalAmount: { $sum: { $toDouble: "$price" } }, + totalAmount: { $sum: { $toDouble: '$price' } }, totalCount: { $sum: 1 }, pendingCount: { - $sum: { $cond: [{ $eq: ["$status", "pending"] }, 1, 0] }, + $sum: { $cond: [{ $eq: ['$status', 'pending'] }, 1, 0] } }, settledCount: { - $sum: { $cond: [{ $eq: ["$status", "settled"] }, 1, 0] }, + $sum: { $cond: [{ $eq: ['$status', 'settled'] }, 1, 0] } }, successCount: { - $sum: { $cond: [{ $eq: ["$status", "success"] }, 1, 0] }, - }, - }, - }, - ]); + $sum: { $cond: [{ $eq: ['$status', 'success'] }, 1, 0] } + } + } + } + ]) const result = stats[0] || { totalAmount: 0, totalCount: 0, pendingCount: 0, settledCount: 0, - successCount: 0, - }; + successCount: 0 + } result.averageAmount = result.totalCount > 0 ? Math.round(result.totalAmount / result.totalCount) - : 0; + : 0 res.status(200).json({ success: true, - data: result, - }); + data: result + }) } catch (error) { - console.error("Error in getPaymentStats:", error); + console.error('Error in getPaymentStats:', error) res.status(500).json({ success: false, - message: "خطا در دریافت آمار پرداخت‌ها", - error: process.env.NODE_ENV === "development" ? error.message : undefined, - }); + message: 'خطا در دریافت آمار پرداخت‌ها', + error: process.env.NODE_ENV === 'development' ? error.message : undefined + }) } -}; +} const getCourseById = async (req, res) => { try { - const { id } = req.params; + const { id } = req.params const course = await CourseModel.findById(id) - .populate("academyId", "academy_name academy_image") - .populate("user_id", "user_name profile_image"); + .populate('academyId', 'academy_name academy_image') + .populate('user_id', 'user_name profile_image') if (!course) { - return res.status(404).json({ success: false, message: "دوره یافت نشد" }); + return res.status(404).json({ success: false, message: 'دوره یافت نشد' }) } res.status(200).json({ success: true, - data: course, - }); + data: course + }) } catch (error) { - console.error(error); - res.status(500).json({ success: false, message: "خطا در دریافت دوره" }); + console.error(error) + res.status(500).json({ success: false, message: 'خطا در دریافت دوره' }) } -}; +} const createCourse = async (req, res) => { try { - const courseData = req.body; - const newCourse = new CourseModel(courseData); - await newCourse.save(); + const courseData = req.body + const newCourse = new CourseModel(courseData) + await newCourse.save() res.status(201).json({ success: true, - message: "دوره با موفقیت ایجاد شد", - data: newCourse, - }); + message: 'دوره با موفقیت ایجاد شد', + data: newCourse + }) } catch (error) { - console.error(error); - res.status(500).json({ success: false, message: "خطا در ایجاد دوره" }); + console.error(error) + res.status(500).json({ success: false, message: 'خطا در ایجاد دوره' }) } -}; +} const getCourseStats = async (req, res) => { try { @@ -3520,36 +3521,36 @@ const getCourseStats = async (req, res) => { _id: null, totalCourses: { $sum: 1 }, normalCount: { - $sum: { $cond: [{ $eq: ["$type", "normal"] }, 1, 0] }, + $sum: { $cond: [{ $eq: ['$type', 'normal'] }, 1, 0] } }, - proCount: { $sum: { $cond: [{ $eq: ["$type", "pro"] }, 1, 0] } }, + proCount: { $sum: { $cond: [{ $eq: ['$type', 'pro'] }, 1, 0] } }, legendCount: { - $sum: { $cond: [{ $eq: ["$type", "legend"] }, 1, 0] }, + $sum: { $cond: [{ $eq: ['$type', 'legend'] }, 1, 0] } }, pendingCount: { - $sum: { $cond: [{ $eq: ["$status", "pending"] }, 1, 0] }, + $sum: { $cond: [{ $eq: ['$status', 'pending'] }, 1, 0] } }, acceptCount: { - $sum: { $cond: [{ $eq: ["$status", "accept"] }, 1, 0] }, + $sum: { $cond: [{ $eq: ['$status', 'accept'] }, 1, 0] } }, rejectCount: { - $sum: { $cond: [{ $eq: ["$status", "reject"] }, 1, 0] }, - }, - }, - }, - ]); + $sum: { $cond: [{ $eq: ['$status', 'reject'] }, 1, 0] } + } + } + } + ]) res.status(200).json({ success: true, - data: stats[0] || {}, - }); + data: stats[0] || {} + }) } catch (error) { - console.error(error); + console.error(error) res .status(500) - .json({ success: false, message: "خطا در دریافت آمار دوره‌ها" }); + .json({ success: false, message: 'خطا در دریافت آمار دوره‌ها' }) } -}; +} const getPurchasedStats = async (req, res) => { try { @@ -3558,117 +3559,117 @@ const getPurchasedStats = async (req, res) => { $group: { _id: null, totalPurchases: { $sum: 1 }, - uniqueUsers: { $addToSet: "$user_id" }, - }, - }, - ]); + uniqueUsers: { $addToSet: '$user_id' } + } + } + ]) res.status(200).json({ success: true, data: { totalPurchases: stats[0]?.totalPurchases || 0, - uniqueUsers: stats[0]?.uniqueUsers?.length || 0, - }, - }); + uniqueUsers: stats[0]?.uniqueUsers?.length || 0 + } + }) } catch (error) { - console.error(error); + console.error(error) res .status(500) - .json({ success: false, message: "خطا در دریافت آمار خریدها" }); + .json({ success: false, message: 'خطا در دریافت آمار خریدها' }) } -}; +} const getAcademyById = async (req, res) => { try { - const { id } = req.params; + const { id } = req.params const academy = await AcademyModel.findById(id).populate( - "userId", - "user_name profile_image email" - ); + 'userId', + 'user_name profile_image email' + ) if (!academy) { return res .status(404) - .json({ success: false, message: "آکادمی یافت نشد" }); + .json({ success: false, message: 'آکادمی یافت نشد' }) } res.status(200).json({ success: true, - data: academy, - }); + data: academy + }) } catch (error) { - console.error(error); - res.status(500).json({ success: false, message: "خطا در دریافت آکادمی" }); + console.error(error) + res.status(500).json({ success: false, message: 'خطا در دریافت آکادمی' }) } -}; +} const createAcademy = async (req, res) => { try { - const academyData = req.body; - const newAcademy = new AcademyModel(academyData); - await newAcademy.save(); + const academyData = req.body + const newAcademy = new AcademyModel(academyData) + await newAcademy.save() res.status(201).json({ success: true, - message: "آکادمی با موفقیت ایجاد شد", - data: newAcademy, - }); + message: 'آکادمی با موفقیت ایجاد شد', + data: newAcademy + }) } catch (error) { - console.error(error); - res.status(500).json({ success: false, message: "خطا در ایجاد آکادمی" }); + console.error(error) + res.status(500).json({ success: false, message: 'خطا در ایجاد آکادمی' }) } -}; +} const updateAcademy = async (req, res) => { try { - const { id } = req.params; - const updateData = req.body; + const { id } = req.params + const updateData = req.body const academy = await AcademyModel.findByIdAndUpdate(id, updateData, { - new: true, - }); + new: true + }) if (!academy) { return res .status(404) - .json({ success: false, message: "آکادمی یافت نشد" }); + .json({ success: false, message: 'آکادمی یافت نشد' }) } res.status(200).json({ success: true, - message: "آکادمی با موفقیت به‌روزرسانی شد", - data: academy, - }); + message: 'آکادمی با موفقیت به‌روزرسانی شد', + data: academy + }) } catch (error) { - console.error(error); + console.error(error) res .status(500) - .json({ success: false, message: "خطا در به‌روزرسانی آکادمی" }); + .json({ success: false, message: 'خطا در به‌روزرسانی آکادمی' }) } -}; +} const deleteAcademy = async (req, res) => { try { - const { id } = req.params; - const academy = await AcademyModel.findByIdAndDelete(id); + const { id } = req.params + const academy = await AcademyModel.findByIdAndDelete(id) if (!academy) { return res .status(404) - .json({ success: false, message: "آکادمی یافت نشد" }); + .json({ success: false, message: 'آکادمی یافت نشد' }) } - await CourseModel.deleteMany({ academyId: id }); + await CourseModel.deleteMany({ academyId: id }) res.status(200).json({ success: true, - message: "آکادمی با موفقیت حذف شد", - }); + message: 'آکادمی با موفقیت حذف شد' + }) } catch (error) { - console.error(error); - res.status(500).json({ success: false, message: "خطا در حذف آکادمی" }); + console.error(error) + res.status(500).json({ success: false, message: 'خطا در حذف آکادمی' }) } -}; +} const getAcademyStats = async (req, res) => { try { @@ -3677,100 +3678,100 @@ const getAcademyStats = async (req, res) => { $group: { _id: null, totalAcademies: { $sum: 1 }, - totalRate: { $sum: "$rate" }, - avgRate: { $avg: "$rate" }, - }, - }, - ]); + totalRate: { $sum: '$rate' }, + avgRate: { $avg: '$rate' } + } + } + ]) res.status(200).json({ success: true, data: stats[0] || { totalAcademies: 0, totalRate: 0, - avgRate: 0, - }, - }); + avgRate: 0 + } + }) } catch (error) { - console.error(error); + console.error(error) res .status(500) - .json({ success: false, message: "خطا در دریافت آمار آکادمی‌ها" }); + .json({ success: false, message: 'خطا در دریافت آمار آکادمی‌ها' }) } -}; +} const deleteCourseVideo = async (req, res, next) => { try { - const token = req.header("Authorization")?.split(" ")[1]; - if (!token) return res.status(401).send("Access Denied"); - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - const userId = decodedToken.id; + const 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 { courseContentId, courseId } = req.body; - const courseContent = await AcademyContentModel.findById(courseContentId); + const { courseContentId, courseId } = req.body + const courseContent = await AcademyContentModel.findById(courseContentId) if (!courseContent) { return res.status(404).json({ error: true, - message: "ویدعو یافت نشد", - }); + message: 'ویدعو یافت نشد' + }) } - courseContent.status = "reject"; + courseContent.status = 'reject' - await courseContent.save(); + await courseContent.save() - const course = await CourseModel.findById(courseId); + const course = await CourseModel.findById(courseId) if (course) { course.number_of_course_content = `${ Number(course.number_of_course_content) - 1 - }`; - await course.save(); + }` + await course.save() } res.status(200).json({ success: true, - message: "ویدیو با موفقیت پاک شد", - }); + message: 'ویدیو با موفقیت پاک شد' + }) } catch (error) { - console.error("Error:", error); + console.error('Error:', error) res.status(500).json({ success: false, - message: "خطا در حذف ویدیو", - error: error.message, - }); + message: 'خطا در حذف ویدیو', + error: error.message + }) } -}; +} const getChunkDir = (uploadKey) => { - return path.join(process.cwd(), "storage", "temp-chunks", uploadKey); -}; + return path.join(process.cwd(), 'storage', 'temp-chunks', uploadKey) +} setInterval(async () => { - const tempDir = path.join(process.cwd(), "storage", "temp-chunks"); + const tempDir = path.join(process.cwd(), 'storage', 'temp-chunks') if (await fs.pathExists(tempDir)) { - const dirs = await fs.readdir(tempDir); - const now = Date.now(); + const dirs = await fs.readdir(tempDir) + const now = Date.now() for (const dir of dirs) { - const dirPath = path.join(tempDir, dir); - const stat = await fs.stat(dirPath); + const dirPath = path.join(tempDir, dir) + const stat = await fs.stat(dirPath) if (now - stat.mtimeMs > 30 * 60 * 1000) { // 30 دقیقه - await fs.remove(dirPath); - console.log(`Cleaned up old upload: ${dir}`); + await fs.remove(dirPath) + console.log(`Cleaned up old upload: ${dir}`) } } } -}, 60 * 60 * 1000); +}, 60 * 60 * 1000) const creatCourseVideoChunk = async (req, res, next) => { - console.log("=== Chunk Video Upload ==="); - console.log("Chunk index:", req.body.chunkIndex); + console.log('=== Chunk Video Upload ===') + console.log('Chunk index:', req.body.chunkIndex) try { - const token = req.header("Authorization")?.split(" ")[1]; - if (!token) return res.status(401).send("Access Denied"); - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - const userId = decodedToken.id; + const 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 { courseId, @@ -3781,963 +3782,963 @@ const creatCourseVideoChunk = async (req, res, next) => { totalChunks, originalFileName, fileSize, - mime_type, - } = req.body; + mime_type + } = req.body if (!chunk) { return res.status(422).json({ error: true, - message: "تکه فایل ارسال نشده است", - }); + message: 'تکه فایل ارسال نشده است' + }) } if (!courseId) { return res.status(422).json({ error: true, - message: "شناسه دوره الزامی است", - }); + message: 'شناسه دوره الزامی است' + }) } - const base64Data = chunk.split(",")[1] || chunk; - const buffer = Buffer.from(base64Data, "base64"); + const base64Data = chunk.split(',')[1] || chunk + const buffer = Buffer.from(base64Data, 'base64') - const uploadKey = `${courseId}_${originalFileName}`; - const chunkDir = getChunkDir(uploadKey); + const uploadKey = `${courseId}_${originalFileName}` + const chunkDir = getChunkDir(uploadKey) - await fs.ensureDir(chunkDir); + await fs.ensureDir(chunkDir) - const metaPath = path.join(chunkDir, "meta.json"); - if (chunkIndex === "0" || chunkIndex === 0) { + const metaPath = path.join(chunkDir, 'meta.json') + if (chunkIndex === '0' || chunkIndex === 0) { const metaData = { totalChunks: parseInt(totalChunks), - file_name: file_name, - originalFileName: originalFileName, - fileSize: fileSize, - mime_type: mime_type, - courseId: courseId, - is_free: is_free, - createdAt: Date.now(), - }; - await fs.writeJson(metaPath, metaData); + file_name, + originalFileName, + fileSize, + mime_type, + courseId, + is_free, + createdAt: Date.now() + } + await fs.writeJson(metaPath, metaData) } if (!(await fs.pathExists(metaPath))) { return res.status(404).json({ error: true, - message: "جلسه آپلود یافت نشد", - }); + message: 'جلسه آپلود یافت نشد' + }) } - const chunkPath = path.join(chunkDir, `chunk_${chunkIndex}`); - await fs.writeFile(chunkPath, buffer); + const chunkPath = path.join(chunkDir, `chunk_${chunkIndex}`) + await fs.writeFile(chunkPath, buffer) console.log( `✅ Chunk ${parseInt(chunkIndex) + 1}/${totalChunks} saved to disk (${ buffer.length } bytes)` - ); + ) res.status(200).json({ success: true, message: `تکه ${ parseInt(chunkIndex) + 1 } از ${totalChunks} با موفقیت دریافت شد`, - chunkIndex: chunkIndex, - totalChunks: totalChunks, - }); + chunkIndex, + totalChunks + }) } catch (error) { - console.error("Error in creatCourseVideoChunk:", error); + console.error('Error in creatCourseVideoChunk:', error) res.status(500).json({ success: false, - message: "خطا در دریافت تکه ویدیو", - error: error.message, - }); + message: 'خطا در دریافت تکه ویدیو', + error: error.message + }) } -}; +} const mergeVideoChunks = async (req, res, next) => { - console.log("=== Merge Video Chunks ==="); + console.log('=== Merge Video Chunks ===') try { - const token = req.header("Authorization")?.split(" ")[1]; - if (!token) return res.status(401).send("Access Denied"); - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - const userId = decodedToken.id; + const 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 { courseId, originalFileName, totalChunks } = req.body; + const { courseId, originalFileName, totalChunks } = req.body - const uploadKey = `${courseId}_${originalFileName}`; - const chunkDir = getChunkDir(uploadKey); - const metaPath = path.join(chunkDir, "meta.json"); + const uploadKey = `${courseId}_${originalFileName}` + const chunkDir = getChunkDir(uploadKey) + const metaPath = path.join(chunkDir, 'meta.json') if (!(await fs.pathExists(chunkDir)) || !(await fs.pathExists(metaPath))) { return res.status(404).json({ error: true, - message: "داده‌های آپلود یافت نشد", - }); + message: 'داده‌های آپلود یافت نشد' + }) } - const metaData = await fs.readJson(metaPath); + const metaData = await fs.readJson(metaPath) - const existingChunks = await fs.readdir(chunkDir); - const chunkFiles = existingChunks.filter((f) => f.startsWith("chunk_")); + const existingChunks = await fs.readdir(chunkDir) + const chunkFiles = existingChunks.filter((f) => f.startsWith('chunk_')) if (chunkFiles.length !== parseInt(totalChunks)) { return res.status(422).json({ error: true, - message: `تعداد تکه‌ها کامل نیست. دریافت شده: ${chunkFiles.length}، مورد نیاز: ${totalChunks}`, - }); + message: `تعداد تکه‌ها کامل نیست. دریافت شده: ${chunkFiles.length}، مورد نیاز: ${totalChunks}` + }) } - const chunks = []; + const chunks = [] for (let i = 0; i < parseInt(totalChunks); i++) { - const chunkPath = path.join(chunkDir, `chunk_${i}`); + const chunkPath = path.join(chunkDir, `chunk_${i}`) if (!(await fs.pathExists(chunkPath))) { return res.status(422).json({ error: true, - message: `تکه ${i + 1} پیدا نشد`, - }); + message: `تکه ${i + 1} پیدا نشد` + }) } - const chunkBuffer = await fs.readFile(chunkPath); - chunks.push(chunkBuffer); + const chunkBuffer = await fs.readFile(chunkPath) + chunks.push(chunkBuffer) } - const completeBuffer = Buffer.concat(chunks); - console.log("Original file size:", completeBuffer.length, "bytes"); + const completeBuffer = Buffer.concat(chunks) + console.log('Original file size:', completeBuffer.length, 'bytes') const finalVideoDir = path.join( process.cwd(), - "storage", - "courses", - "videos" - ); - await fs.ensureDir(finalVideoDir); + 'storage', + 'courses', + 'videos' + ) + await fs.ensureDir(finalVideoDir) - let extension = ".mp4"; - if (metaData.mime_type === "video/mp4") extension = ".mp4"; - else if (metaData.mime_type === "video/quicktime") extension = ".mov"; - else if (metaData.mime_type === "video/x-msvideo") extension = ".avi"; - else if (metaData.mime_type === "video/webm") extension = ".webm"; + let extension = '.mp4' + if (metaData.mime_type === 'video/mp4') extension = '.mp4' + else if (metaData.mime_type === 'video/quicktime') extension = '.mov' + else if (metaData.mime_type === 'video/x-msvideo') extension = '.avi' + else if (metaData.mime_type === 'video/webm') extension = '.webm' - const safeFileName = `course-${courseId}-${Date.now()}${extension}`; - const filePath = path.join(finalVideoDir, safeFileName); + const safeFileName = `course-${courseId}-${Date.now()}${extension}` + const filePath = path.join(finalVideoDir, safeFileName) - await fs.writeFile(filePath, completeBuffer); - console.log("Original file saved:", filePath); + await fs.writeFile(filePath, completeBuffer) + console.log('Original file saved:', filePath) + + const uploaderUser = await UserModel.findById(userId).select('verify_badge').lean() const newAcademyContent = new AcademyContentModel({ course_video: `/storage/courses/videos/${safeFileName}`, - type: "video", - is_free: metaData.is_free === "true" || metaData.is_free === true, - courseId: courseId, + type: 'video', + is_free: metaData.is_free === 'true' || metaData.is_free === true, + courseId, file_name: metaData.file_name || metaData.originalFileName, - }); + status: isVerifiedBadge(uploaderUser?.verify_badge) ? 'temp_accept' : 'pending' + }) - await newAcademyContent.save(); + await newAcademyContent.save() - const course = await CourseModel.findById(courseId); + const course = await CourseModel.findById(courseId) if (course) { course.number_of_course_content = `${ Number(course.number_of_course_content) + 1 - }`; - await course.save(); + }` + await course.save() } - await fs.remove(chunkDir); + await fs.remove(chunkDir) res.status(200).json({ success: true, - message: "ویدیو با موفقیت ثبت شد (در حال پردازش...)", - data: newAcademyContent, - }); + message: 'ویدیو با موفقیت ثبت شد (در حال پردازش...)', + data: newAcademyContent + }) if (completeBuffer.length > 20 * 1024 * 1024) { // اجرای غیرهمزمان بدون await - compressVideoInBackground(filePath, safeFileName, newAcademyContent._id); + compressVideoInBackground(filePath, safeFileName, newAcademyContent._id) } } catch (error) { - console.error("Error in mergeVideoChunks:", error); + console.error('Error in mergeVideoChunks:', error) res.status(500).json({ success: false, - message: "خطا در ترکیب ویدیو", - error: error.message, - }); + message: 'خطا در ترکیب ویدیو', + error: error.message + }) } -}; +} const compressVideoInBackground = async (filePath, fileName, contentId) => { - console.log(`Starting background compression for ${fileName}`); + console.log(`Starting background compression for ${fileName}`) - const dir = path.dirname(filePath); - const tempPath = path.join(dir, `temp-${fileName}`); - const { exec } = require("child_process"); - const util = require("util"); - const execPromise = util.promisify(exec); + const dir = path.dirname(filePath) + const tempPath = path.join(dir, `temp-${fileName}`) + const { exec } = require('child_process') + const util = require('util') + const execPromise = util.promisify(exec) try { await execPromise( `ffmpeg -i "${filePath}" -c:v libx264 -crf 28 -preset veryfast -c:a aac -b:a 128k -movflags +faststart "${tempPath}" -y` - ); + ) - const compressedStat = await fs.stat(tempPath); - const originalStat = await fs.stat(filePath); + const compressedStat = await fs.stat(tempPath) + const originalStat = await fs.stat(filePath) if (compressedStat.size < originalStat.size) { - await fs.rename(tempPath, filePath); + await fs.rename(tempPath, filePath) console.log( `✅ Background compression done: ${( originalStat.size / 1024 / 1024 ).toFixed(2)}MB → ${(compressedStat.size / 1024 / 1024).toFixed(2)}MB` - ); + ) } else { - await fs.remove(tempPath); - console.log("Compression didn't reduce size, keeping original"); + await fs.remove(tempPath) + console.log("Compression didn't reduce size, keeping original") } } catch (error) { - console.error("Background compression failed:", error.message); + console.error('Background compression failed:', error.message) if (await fs.pathExists(tempPath)) { - await fs.remove(tempPath); + await fs.remove(tempPath) } } -}; +} const getUploadStatus = async (req, res) => { try { - const { courseId, originalFileName } = req.query; - const uploadKey = `${courseId}_${originalFileName}`; - const chunkDir = getChunkDir(uploadKey); + const { courseId, originalFileName } = req.query + const uploadKey = `${courseId}_${originalFileName}` + const chunkDir = getChunkDir(uploadKey) if (!(await fs.pathExists(chunkDir))) { return res.status(200).json({ success: true, uploadedChunks: [], - totalChunks: 0, - }); + totalChunks: 0 + }) } - const files = await fs.readdir(chunkDir); + const files = await fs.readdir(chunkDir) const chunkFiles = files - .filter((f) => f.startsWith("chunk_")) - .map((f) => parseInt(f.replace("chunk_", ""))) - .sort((a, b) => a - b); + .filter((f) => f.startsWith('chunk_')) + .map((f) => parseInt(f.replace('chunk_', ''))) + .sort((a, b) => a - b) - const metaPath = path.join(chunkDir, "meta.json"); - let totalChunks = 0; + const metaPath = path.join(chunkDir, 'meta.json') + let totalChunks = 0 if (await fs.pathExists(metaPath)) { - const metaData = await fs.readJson(metaPath); - totalChunks = metaData.totalChunks; + const metaData = await fs.readJson(metaPath) + totalChunks = metaData.totalChunks } res.status(200).json({ success: true, uploadedChunks: chunkFiles, - totalChunks: totalChunks, - }); + totalChunks + }) } catch (error) { - res.status(500).json({ success: false, error: error.message }); + res.status(500).json({ success: false, error: error.message }) } -}; - - +} // ایجاد slug یکتا const generateUniqueSlug = async (title, excludeId = null) => { - let slug = title - .replace(/[^\u0600-\u06FF\uFB8A\u067E\u0686\u06AF\u200C\uFB8E\u0698a-zA-Z0-9\s]/g, '') - .trim() - .replace(/\s+/g, '-') - .toLowerCase(); - - let uniqueSlug = slug; - let counter = 1; - - while (true) { - const query = { slug: uniqueSlug }; - if (excludeId) query._id = { $ne: excludeId }; - const exists = await AcademyCategoryModel.findOne(query); - if (!exists) break; - uniqueSlug = `${slug}-${counter}`; - counter++; - } - - return uniqueSlug; -}; + const slug = title + .replace(/[^\u0600-\u06FF\uFB8A\u067E\u0686\u06AF\u200C\uFB8E\u0698a-zA-Z0-9\s]/g, '') + .trim() + .replace(/\s+/g, '-') + .toLowerCase() + + let uniqueSlug = slug + let counter = 1 + + while (true) { + const query = { slug: uniqueSlug } + if (excludeId) query._id = { $ne: excludeId } + const exists = await AcademyCategoryModel.findOne(query) + if (!exists) break + uniqueSlug = `${slug}-${counter}` + counter++ + } + + return uniqueSlug +} // ==================== GET - دریافت لیست دسته‌بندی‌ها ==================== const getAll = async (req, res) => { - try { - const { - page = 1, - limit = 20, - sortBy = 'order', - sortOrder = 'asc', - status, - parent, - search, - is_featured, - withChildren = 'false' - } = req.query; + try { + const { + page = 1, + limit = 20, + sortBy = 'order', + sortOrder = 'asc', + status, + parent, + search, + is_featured, + withChildren = 'false' + } = req.query - let query = {}; - - if (status) query.status = status; - if (parent !== undefined) query.parent = parent === 'null' ? null : parent; - if (is_featured === 'true') query.is_featured = true; - - if (search) { - query.$or = [ - { title: { $regex: search, $options: 'i' } }, - { description: { $regex: search, $options: 'i' } } - ]; - } + const query = {} - const sortOptions = {}; - sortOptions[sortBy] = sortOrder === 'asc' ? 1 : -1; + if (status) query.status = status + if (parent !== undefined) query.parent = parent === 'null' ? null : parent + if (is_featured === 'true') query.is_featured = true - const options = { - page: parseInt(page), - limit: parseInt(limit), - sort: sortOptions, - populate: [ - { path: 'parent', select: 'title slug' }, - { path: 'created_by', select: 'user_name name' }, - { path: 'updated_by', select: 'user_name name' } - ] - }; - - let categories = await AcademyCategoryModel.paginate(query, options); - - // دریافت درخت دسته‌بندی (اختیاری) - if (withChildren === 'true' && !parent) { - const tree = await AcademyCategoryModel.getTree(); - categories = { ...categories, tree }; - } - - res.status(200).json({ - success: true, - message: 'لیست دسته‌بندی‌ها با موفقیت دریافت شد', - data: categories - }); - } catch (error) { - console.error('Error in getAll:', error); - res.status(500).json({ - success: false, - message: 'خطا در دریافت دسته‌بندی‌ها', - error: error.message - }); + if (search) { + query.$or = [ + { title: { $regex: search, $options: 'i' } }, + { description: { $regex: search, $options: 'i' } } + ] } -}; + + const sortOptions = {} + sortOptions[sortBy] = sortOrder === 'asc' ? 1 : -1 + + const options = { + page: parseInt(page), + limit: parseInt(limit), + sort: sortOptions, + populate: [ + { path: 'parent', select: 'title slug' }, + { path: 'created_by', select: 'user_name name' }, + { path: 'updated_by', select: 'user_name name' } + ] + } + + let categories = await AcademyCategoryModel.paginate(query, options) + + // دریافت درخت دسته‌بندی (اختیاری) + if (withChildren === 'true' && !parent) { + const tree = await AcademyCategoryModel.getTree() + categories = { ...categories, tree } + } + + res.status(200).json({ + success: true, + message: 'لیست دسته‌بندی‌ها با موفقیت دریافت شد', + data: categories + }) + } catch (error) { + console.error('Error in getAll:', error) + res.status(500).json({ + success: false, + message: 'خطا در دریافت دسته‌بندی‌ها', + error: error.message + }) + } +} // ==================== GET - دریافت یک دسته‌بندی ==================== const getOne = async (req, res) => { - try { - const { id } = req.params; - const { includeCourses = 'false' } = req.query; + try { + const { id } = req.params + const { includeCourses = 'false' } = req.query - let query = AcademyCategoryModel.findById(id); - - query = query.populate('parent', 'title slug icon color'); - query = query.populate('children', 'title slug icon color course_count order'); - query = query.populate('created_by', 'user_name name'); - - if (includeCourses === 'true') { - query = query.populate({ - path: 'courses', - select: 'cuorse_name course_image price offer', - options: { limit: 10 } - }); - } + let query = AcademyCategoryModel.findById(id) - const category = await query; + query = query.populate('parent', 'title slug icon color') + query = query.populate('children', 'title slug icon color course_count order') + query = query.populate('created_by', 'user_name name') - if (!category) { - return res.status(404).json({ - success: false, - message: 'دسته‌بندی یافت نشد' - }); - } - - res.status(200).json({ - success: true, - message: 'دسته‌بندی با موفقیت دریافت شد', - data: category - }); - } catch (error) { - console.error('Error in getOne:', error); - res.status(500).json({ - success: false, - message: 'خطا در دریافت دسته‌بندی', - error: error.message - }); + if (includeCourses === 'true') { + query = query.populate({ + path: 'courses', + select: 'cuorse_name course_image price offer', + options: { limit: 10 } + }) } -}; + + const category = await query + + if (!category) { + return res.status(404).json({ + success: false, + message: 'دسته‌بندی یافت نشد' + }) + } + + res.status(200).json({ + success: true, + message: 'دسته‌بندی با موفقیت دریافت شد', + data: category + }) + } catch (error) { + console.error('Error in getOne:', error) + res.status(500).json({ + success: false, + message: 'خطا در دریافت دسته‌بندی', + error: error.message + }) + } +} // ==================== GET - دریافت درخت دسته‌بندی ==================== const getTree = async (req, res) => { - try { - const tree = await AcademyCategoryModel.getTree(); - - res.status(200).json({ - success: true, - message: 'درخت دسته‌بندی با موفقیت دریافت شد', - data: tree - }); - } catch (error) { - console.error('Error in getTree:', error); - res.status(500).json({ - success: false, - message: 'خطا در دریافت درخت دسته‌بندی', - error: error.message - }); - } -}; + try { + const tree = await AcademyCategoryModel.getTree() + + res.status(200).json({ + success: true, + message: 'درخت دسته‌بندی با موفقیت دریافت شد', + data: tree + }) + } catch (error) { + console.error('Error in getTree:', error) + res.status(500).json({ + success: false, + message: 'خطا در دریافت درخت دسته‌بندی', + error: error.message + }) + } +} // ==================== GET - دریافت توسط اسلاگ ==================== const getBySlug = async (req, res) => { - try { - const { slug } = req.params; - const { page = 1, limit = 20 } = req.query; + try { + const { slug } = req.params + const { page = 1, limit = 20 } = req.query - const category = await AcademyCategoryModel.getBySlug(slug); + const category = await AcademyCategoryModel.getBySlug(slug) - if (!category) { - return res.status(404).json({ - success: false, - message: 'دسته‌بندی یافت نشد' - }); - } - - // دریافت دوره‌های این دسته - const courses = await CourseModel.find({ - category_id: category._id, - status: 'accept' - }) - .sort({ createdAt: -1 }) - .skip((page - 1) * limit) - .limit(parseInt(limit)); - - const totalCourses = await CourseModel.countDocuments({ - category_id: category._id, - status: 'accept' - }); - - res.status(200).json({ - success: true, - message: 'دسته‌بندی با موفقیت دریافت شد', - data: { - category, - courses: { - data: courses, - pagination: { - page: parseInt(page), - limit: parseInt(limit), - total: totalCourses, - pages: Math.ceil(totalCourses / limit) - } - } - } - }); - } catch (error) { - console.error('Error in getBySlug:', error); - res.status(500).json({ - success: false, - message: 'خطا در دریافت دسته‌بندی', - error: error.message - }); + if (!category) { + return res.status(404).json({ + success: false, + message: 'دسته‌بندی یافت نشد' + }) } -}; + + // دریافت دوره‌های این دسته + const courses = await CourseModel.find({ + category_id: category._id, + status: 'accept' + }) + .sort({ createdAt: -1 }) + .skip((page - 1) * limit) + .limit(parseInt(limit)) + + const totalCourses = await CourseModel.countDocuments({ + category_id: category._id, + status: 'accept' + }) + + res.status(200).json({ + success: true, + message: 'دسته‌بندی با موفقیت دریافت شد', + data: { + category, + courses: { + data: courses, + pagination: { + page: parseInt(page), + limit: parseInt(limit), + total: totalCourses, + pages: Math.ceil(totalCourses / limit) + } + } + } + }) + } catch (error) { + console.error('Error in getBySlug:', error) + res.status(500).json({ + success: false, + message: 'خطا در دریافت دسته‌بندی', + error: error.message + }) + } +} // ==================== POST - ایجاد دسته‌بندی جدید ==================== const create = async (req, res) => { - try { - const token = req.header("Authorization")?.split(" ")[1]; - if (!token) return res.status(401).json({ success: false, message: "Access Denied" }); - - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - const userId = decodedToken.id; + try { + const token = req.header('Authorization')?.split(' ')[1] + if (!token) return res.status(401).json({ success: false, message: 'Access Denied' }) - const { - title, - description, - icon, - color, - parent, - order, - image, - is_featured, - seo_title, - seo_description - } = req.body; + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id - // اعتبارسنجی - if (!title || title.trim().length < 2) { - return res.status(422).json({ - success: false, - message: 'عنوان دسته‌بندی باید حداقل ۲ کاراکتر باشد' - }); - } + const { + title, + description, + icon, + color, + parent, + order, + image, + is_featured, + seo_title, + seo_description + } = req.body - // بررسی تکراری نبودن عنوان - const existingTitle = await AcademyCategoryModel.findOne({ title }); - if (existingTitle) { - return res.status(409).json({ - success: false, - message: 'این عنوان قبلاً ثبت شده است' - }); - } - - // تولید slug یکتا - const slug = await generateUniqueSlug(title); - - // بررسی وجود parent - let level = 0; - if (parent) { - const parentCategory = await AcademyCategoryModel.findById(parent); - if (!parentCategory) { - return res.status(404).json({ - success: false, - message: 'دسته‌بندی والد یافت نشد' - }); - } - level = parentCategory.level + 1; - - if (level > 3) { - return res.status(422).json({ - success: false, - message: 'عمق دسته‌بندی نمی‌تواند بیشتر از ۳ سطح باشد' - }); - } - } - - const newCategory = new AcademyCategoryModel({ - title, - slug, - description: description || null, - icon: icon || null, - color: color || '#3B82F6', - parent: parent || null, - level, - order: order || 0, - image: image || null, - is_featured: is_featured || false, - seo_title: seo_title || title, - seo_description: seo_description || description, - created_by: userId, - status: 'active' - }); - - await newCategory.save(); - - res.status(201).json({ - success: true, - message: 'دسته‌بندی با موفقیت ایجاد شد', - data: newCategory - }); - } catch (error) { - console.error('Error in create:', error); - res.status(500).json({ - success: false, - message: 'خطا در ایجاد دسته‌بندی', - error: error.message - }); + // اعتبارسنجی + if (!title || title.trim().length < 2) { + return res.status(422).json({ + success: false, + message: 'عنوان دسته‌بندی باید حداقل ۲ کاراکتر باشد' + }) } -}; + + // بررسی تکراری نبودن عنوان + const existingTitle = await AcademyCategoryModel.findOne({ title }) + if (existingTitle) { + return res.status(409).json({ + success: false, + message: 'این عنوان قبلاً ثبت شده است' + }) + } + + // تولید slug یکتا + const slug = await generateUniqueSlug(title) + + // بررسی وجود parent + let level = 0 + if (parent) { + const parentCategory = await AcademyCategoryModel.findById(parent) + if (!parentCategory) { + return res.status(404).json({ + success: false, + message: 'دسته‌بندی والد یافت نشد' + }) + } + level = parentCategory.level + 1 + + if (level > 3) { + return res.status(422).json({ + success: false, + message: 'عمق دسته‌بندی نمی‌تواند بیشتر از ۳ سطح باشد' + }) + } + } + + const newCategory = new AcademyCategoryModel({ + title, + slug, + description: description || null, + icon: icon || null, + color: color || '#3B82F6', + parent: parent || null, + level, + order: order || 0, + image: image || null, + is_featured: is_featured || false, + seo_title: seo_title || title, + seo_description: seo_description || description, + created_by: userId, + status: 'active' + }) + + await newCategory.save() + + res.status(201).json({ + success: true, + message: 'دسته‌بندی با موفقیت ایجاد شد', + data: newCategory + }) + } catch (error) { + console.error('Error in create:', error) + res.status(500).json({ + success: false, + message: 'خطا در ایجاد دسته‌بندی', + error: error.message + }) + } +} // ==================== PUT/PATCH - ویرایش دسته‌بندی ==================== const update = async (req, res) => { - try { - const token = req.header("Authorization")?.split(" ")[1]; - if (!token) return res.status(401).json({ success: false, message: "Access Denied" }); - - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - const userId = decodedToken.id; + try { + const token = req.header('Authorization')?.split(' ')[1] + if (!token) return res.status(401).json({ success: false, message: 'Access Denied' }) - const { id } = req.params; - const updateData = req.body; + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id - const category = await AcademyCategoryModel.findById(id); - if (!category) { - return res.status(404).json({ - success: false, - message: 'دسته‌بندی یافت نشد' - }); - } + const { id } = req.params + const updateData = req.body - // بررسی تکراری نبودن عنوان - if (updateData.title && updateData.title !== category.title) { - const existingTitle = await AcademyCategoryModel.findOne({ - title: updateData.title, - _id: { $ne: id } - }); - if (existingTitle) { - return res.status(409).json({ - success: false, - message: 'این عنوان قبلاً ثبت شده است' - }); - } - - // آپدیت slug - updateData.slug = await generateUniqueSlug(updateData.title, id); - } - - // بررسی parent - if (updateData.parent) { - if (updateData.parent === id) { - return res.status(422).json({ - success: false, - message: 'یک دسته‌بندی نمی‌تواند والد خودش باشد' - }); - } - - const parentCategory = await AcademyCategoryModel.findById(updateData.parent); - if (!parentCategory) { - return res.status(404).json({ - success: false, - message: 'دسته‌بندی والد یافت نشد' - }); - } - - updateData.level = parentCategory.level + 1; - - if (updateData.level > 3) { - return res.status(422).json({ - success: false, - message: 'عمق دسته‌بندی نمی‌تواند بیشتر از ۳ سطح باشد' - }); - } - } - - updateData.updated_by = userId; - updateData.updatedAt = new Date(); - - const updatedCategory = await AcademyCategoryModel.findByIdAndUpdate( - id, - updateData, - { new: true, runValidators: true } - ); - - res.status(200).json({ - success: true, - message: 'دسته‌بندی با موفقیت به‌روزرسانی شد', - data: updatedCategory - }); - } catch (error) { - console.error('Error in update:', error); - res.status(500).json({ - success: false, - message: 'خطا در به‌روزرسانی دسته‌بندی', - error: error.message - }); + const category = await AcademyCategoryModel.findById(id) + if (!category) { + return res.status(404).json({ + success: false, + message: 'دسته‌بندی یافت نشد' + }) } -}; + + // بررسی تکراری نبودن عنوان + if (updateData.title && updateData.title !== category.title) { + const existingTitle = await AcademyCategoryModel.findOne({ + title: updateData.title, + _id: { $ne: id } + }) + if (existingTitle) { + return res.status(409).json({ + success: false, + message: 'این عنوان قبلاً ثبت شده است' + }) + } + + // آپدیت slug + updateData.slug = await generateUniqueSlug(updateData.title, id) + } + + // بررسی parent + if (updateData.parent) { + if (updateData.parent === id) { + return res.status(422).json({ + success: false, + message: 'یک دسته‌بندی نمی‌تواند والد خودش باشد' + }) + } + + const parentCategory = await AcademyCategoryModel.findById(updateData.parent) + if (!parentCategory) { + return res.status(404).json({ + success: false, + message: 'دسته‌بندی والد یافت نشد' + }) + } + + updateData.level = parentCategory.level + 1 + + if (updateData.level > 3) { + return res.status(422).json({ + success: false, + message: 'عمق دسته‌بندی نمی‌تواند بیشتر از ۳ سطح باشد' + }) + } + } + + updateData.updated_by = userId + updateData.updatedAt = new Date() + + const updatedCategory = await AcademyCategoryModel.findByIdAndUpdate( + id, + updateData, + { new: true, runValidators: true } + ) + + res.status(200).json({ + success: true, + message: 'دسته‌بندی با موفقیت به‌روزرسانی شد', + data: updatedCategory + }) + } catch (error) { + console.error('Error in update:', error) + res.status(500).json({ + success: false, + message: 'خطا در به‌روزرسانی دسته‌بندی', + error: error.message + }) + } +} // ==================== DELETE - حذف (سافت دیلیت) ==================== const remove = async (req, res) => { - try { - const token = req.header("Authorization")?.split(" ")[1]; - if (!token) return res.status(401).json({ success: false, message: "Access Denied" }); - - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - const userId = decodedToken.id; + try { + const token = req.header('Authorization')?.split(' ')[1] + if (!token) return res.status(401).json({ success: false, message: 'Access Denied' }) - const { id } = req.params; - const { permanent = 'false' } = req.query; + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id - const category = await AcademyCategoryModel.findById(id); - if (!category) { - return res.status(404).json({ - success: false, - message: 'دسته‌بندی یافت نشد' - }); - } + const { id } = req.params + const { permanent = 'false' } = req.query - // بررسی وجود زیردسته‌ها - const hasChildren = await AcademyCategoryModel.exists({ parent: id }); - if (hasChildren) { - return res.status(422).json({ - success: false, - message: 'این دسته‌بندی دارای زیردسته است. ابتدا زیردسته‌ها را حذف کنید' - }); - } - - // بررسی وجود دوره در این دسته - const hasCourses = await CourseModel.exists({ category_id: id }); - if (hasCourses && permanent === 'true') { - return res.status(422).json({ - success: false, - message: 'این دسته‌بندی دارای دوره است. ابتدا دوره‌ها را جابه‌جا کنید' - }); - } - - if (permanent === 'true') { - // حذف فیزیکی - await category.deleteOne(); - res.status(200).json({ - success: true, - message: 'دسته‌بندی با موفقیت حذف شد' - }); - } else { - // سافت دیلیت - category.status = 'deleted'; - category.updated_by = userId; - await category.save(); - - res.status(200).json({ - success: true, - message: 'دسته‌بندی با موفقیت غیرفعال شد' - }); - } - } catch (error) { - console.error('Error in remove:', error); - res.status(500).json({ - success: false, - message: 'خطا در حذف دسته‌بندی', - error: error.message - }); + const category = await AcademyCategoryModel.findById(id) + if (!category) { + return res.status(404).json({ + success: false, + message: 'دسته‌بندی یافت نشد' + }) } -}; + + // بررسی وجود زیردسته‌ها + const hasChildren = await AcademyCategoryModel.exists({ parent: id }) + if (hasChildren) { + return res.status(422).json({ + success: false, + message: 'این دسته‌بندی دارای زیردسته است. ابتدا زیردسته‌ها را حذف کنید' + }) + } + + // بررسی وجود دوره در این دسته + const hasCourses = await CourseModel.exists({ category_id: id }) + if (hasCourses && permanent === 'true') { + return res.status(422).json({ + success: false, + message: 'این دسته‌بندی دارای دوره است. ابتدا دوره‌ها را جابه‌جا کنید' + }) + } + + if (permanent === 'true') { + // حذف فیزیکی + await category.deleteOne() + res.status(200).json({ + success: true, + message: 'دسته‌بندی با موفقیت حذف شد' + }) + } else { + // سافت دیلیت + category.status = 'deleted' + category.updated_by = userId + await category.save() + + res.status(200).json({ + success: true, + message: 'دسته‌بندی با موفقیت غیرفعال شد' + }) + } + } catch (error) { + console.error('Error in remove:', error) + res.status(500).json({ + success: false, + message: 'خطا در حذف دسته‌بندی', + error: error.message + }) + } +} // ==================== POST - تغییر وضعیت (اکتیو/غیراکتیو) ==================== const toggleStatus = async (req, res) => { - try { - const token = req.header("Authorization")?.split(" ")[1]; - if (!token) return res.status(401).json({ success: false, message: "Access Denied" }); - - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - const userId = decodedToken.id; + try { + const token = req.header('Authorization')?.split(' ')[1] + if (!token) return res.status(401).json({ success: false, message: 'Access Denied' }) - const { id } = req.params; - const { status } = req.body; // 'active' or 'inactive' + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id - if (!['active', 'inactive'].includes(status)) { - return res.status(422).json({ - success: false, - message: 'وضعیت نامعتبر است' - }); - } + const { id } = req.params + const { status } = req.body // 'active' or 'inactive' - const category = await AcademyCategoryModel.findByIdAndUpdate( - id, - { - status, - updated_by: userId, - updatedAt: new Date() - }, - { new: true } - ); - - if (!category) { - return res.status(404).json({ - success: false, - message: 'دسته‌بندی یافت نشد' - }); - } - - res.status(200).json({ - success: true, - message: `وضعیت دسته‌بندی با موفقیت به ${status === 'active' ? 'فعال' : 'غیرفعال'} تغییر کرد`, - data: category - }); - } catch (error) { - console.error('Error in toggleStatus:', error); - res.status(500).json({ - success: false, - message: 'خطا در تغییر وضعیت دسته‌بندی', - error: error.message - }); + if (!['active', 'inactive'].includes(status)) { + return res.status(422).json({ + success: false, + message: 'وضعیت نامعتبر است' + }) } -}; + + const category = await AcademyCategoryModel.findByIdAndUpdate( + id, + { + status, + updated_by: userId, + updatedAt: new Date() + }, + { new: true } + ) + + if (!category) { + return res.status(404).json({ + success: false, + message: 'دسته‌بندی یافت نشد' + }) + } + + res.status(200).json({ + success: true, + message: `وضعیت دسته‌بندی با موفقیت به ${status === 'active' ? 'فعال' : 'غیرفعال'} تغییر کرد`, + data: category + }) + } catch (error) { + console.error('Error in toggleStatus:', error) + res.status(500).json({ + success: false, + message: 'خطا در تغییر وضعیت دسته‌بندی', + error: error.message + }) + } +} // ==================== POST - افزایش تعداد دوره‌ها ==================== const incrementCourseCount = async (req, res) => { - try { - const { id } = req.params; - const { increment = 1 } = req.body; + try { + const { id } = req.params + const { increment = 1 } = req.body - const category = await AcademyCategoryModel.incrementCourseCount(id, increment); - - if (!category) { - return res.status(404).json({ - success: false, - message: 'دسته‌بندی یافت نشد' - }); - } + const category = await AcademyCategoryModel.incrementCourseCount(id, increment) - res.status(200).json({ - success: true, - message: 'تعداد دوره‌ها به‌روزرسانی شد', - data: { course_count: category.course_count } - }); - } catch (error) { - console.error('Error in incrementCourseCount:', error); - res.status(500).json({ - success: false, - message: 'خطا در به‌روزرسانی تعداد دوره‌ها', - error: error.message - }); + if (!category) { + return res.status(404).json({ + success: false, + message: 'دسته‌بندی یافت نشد' + }) } -}; + + res.status(200).json({ + success: true, + message: 'تعداد دوره‌ها به‌روزرسانی شد', + data: { course_count: category.course_count } + }) + } catch (error) { + console.error('Error in incrementCourseCount:', error) + res.status(500).json({ + success: false, + message: 'خطا در به‌روزرسانی تعداد دوره‌ها', + error: error.message + }) + } +} // ==================== GET - دریافت دسته‌بندی‌های ویژه (featured) ==================== const getFeatured = async (req, res) => { - try { - const { limit = 10 } = req.query; - - const categories = await AcademyCategoryModel.find({ - is_featured: true, - status: 'active' - }) - .sort({ order: 1 }) - .limit(parseInt(limit)) - .populate('parent', 'title slug'); + try { + const { limit = 10 } = req.query - res.status(200).json({ - success: true, - message: 'دسته‌بندی‌های ویژه با موفقیت دریافت شد', - data: categories - }); - } catch (error) { - console.error('Error in getFeatured:', error); - res.status(500).json({ - success: false, - message: 'خطا در دریافت دسته‌بندی‌های ویژه', - error: error.message - }); - } -}; + const categories = await AcademyCategoryModel.find({ + is_featured: true, + status: 'active' + }) + .sort({ order: 1 }) + .limit(parseInt(limit)) + .populate('parent', 'title slug') + res.status(200).json({ + success: true, + message: 'دسته‌بندی‌های ویژه با موفقیت دریافت شد', + data: categories + }) + } catch (error) { + console.error('Error in getFeatured:', error) + res.status(500).json({ + success: false, + message: 'خطا در دریافت دسته‌بندی‌های ویژه', + error: error.message + }) + } +} const getFreeAcademyExploreContent = async (req, res, next) => { try { - const page = parseInt(req.query.page, 10) || 1; - const limit = parseInt(req.query.limit, 10) || 15; - const skip = (page - 1) * limit; + const page = parseInt(req.query.page, 10) || 1 + const limit = parseInt(req.query.limit, 10) || 15 + const skip = (page - 1) * limit const freeCourses = await CourseModel.find({ is_free: true }) - .select("_id") - .lean(); - const freeCourseIds = freeCourses.map((course) => String(course._id)); + .select('_id') + .lean() + const freeCourseIds = freeCourses.map((course) => String(course._id)) const query = { status: academyContentPublicStatusFilter(), $or: [ { is_free: true }, - { type: "video", courseId: { $in: freeCourseIds } }, - ], - }; + { type: 'video', courseId: { $in: freeCourseIds } } + ] + } - const totalItems = await AcademyContentModel.countDocuments(query); + const totalItems = await AcademyContentModel.countDocuments(query) const contents = await AcademyContentModel.find(query) .sort({ createdAt: -1 }) .skip(skip) .limit(limit) - .lean(); + .lean() const courseIds = [ - ...new Set(contents.map((item) => item.courseId).filter(Boolean)), - ]; + ...new Set(contents.map((item) => item.courseId).filter(Boolean)) + ] const courses = courseIds.length ? await CourseModel.find({ _id: { $in: courseIds } }) - .select( - "cuorse_name course_image teacher_name academyId user_id caption likes category" - ) - .lean() - : []; + .select( + 'cuorse_name course_image teacher_name academyId user_id caption likes category' + ) + .lean() + : [] const courseMap = Object.fromEntries( courses.map((course) => [String(course._id), course]) - ); + ) const userIds = [ - ...new Set(courses.map((course) => String(course.user_id)).filter(Boolean)), - ]; + ...new Set(courses.map((course) => String(course.user_id)).filter(Boolean)) + ] const users = userIds.length ? await UserModel.find({ _id: { $in: userIds } }) - .select("user_name first_name last_name profile_image") - .lean() - : []; + .select('user_name first_name last_name profile_image') + .lean() + : [] const userMap = Object.fromEntries( users.map((user) => [String(user._id), user]) - ); + ) const mapContentItem = (content) => { - const course = courseMap[String(content.courseId)] || {}; - const user = userMap[String(course.user_id)] || {}; + const course = courseMap[String(content.courseId)] || {} + const user = userMap[String(course.user_id)] || {} return { ...content, - course_name: course.cuorse_name || content.file_name || "", - course_image: course.course_image || "", - teacher_name: course.teacher_name || "", - academyId: course.academyId || "", - user_name: user.user_name || "", - first_name: user.first_name || "", - last_name: user.last_name || "", - profile_image: user.profile_image || "", - likesCount: Array.isArray(course.likes) ? course.likes.length : 0, - }; - }; + course_name: course.cuorse_name || content.file_name || '', + course_image: course.course_image || '', + teacher_name: course.teacher_name || '', + academyId: course.academyId || '', + user_name: user.user_name || '', + first_name: user.first_name || '', + last_name: user.last_name || '', + profile_image: user.profile_image || '', + likesCount: Array.isArray(course.likes) ? course.likes.length : 0 + } + } - let items = contents.map(mapContentItem); + let items = contents.map(mapContentItem) // بلند کردن محتوای مرتبط با علایق کاربر (فقط صفحه اول، بدون فیلتر دستی) if (page === 1) { try { - const token = req.header("Authorization")?.split(" ")[1]; - const viewerId = token ? jwt.verify(token, process.env.APP_SECRET)?.id : null; + const token = req.header('Authorization')?.split(' ')[1] + const viewerId = token ? jwt.verify(token, process.env.APP_SECRET)?.id : null if (viewerId) { - const interestWeights = await buildUnifiedInterestScore(viewerId); - const tags = Object.keys(interestWeights).filter((tag) => interestWeights[tag] > 0); + const interestWeights = await buildUnifiedInterestScore(viewerId) + const tags = Object.keys(interestWeights).filter((tag) => interestWeights[tag] > 0) if (tags.length) { const interestCategories = await AcademyCategoryModel.find({ - relatedExpertise: { $in: tags }, + relatedExpertise: { $in: tags } }) - .select("title") - .lean(); - const categoryTitles = interestCategories.map((c) => c.title); + .select('title') + .lean() + const categoryTitles = interestCategories.map((c) => c.title) if (categoryTitles.length) { const interestCourseIds = ( - await CourseModel.find({ category: { $in: categoryTitles } }).select("_id").lean() - ).map((c) => String(c._id)); + await CourseModel.find({ category: { $in: categoryTitles } }).select('_id').lean() + ).map((c) => String(c._id)) if (interestCourseIds.length) { - const existingContentIds = items.map((i) => i._id); + const existingContentIds = items.map((i) => i._id) const interestContents = await AcademyContentModel.find({ ...query, courseId: { $in: interestCourseIds }, - _id: { $nin: existingContentIds }, + _id: { $nin: existingContentIds } }) .sort({ createdAt: -1 }) .limit(5) - .lean(); + .lean() if (interestContents.length) { const extraCourseIds = interestContents .map((c) => String(c.courseId)) - .filter((id) => !courseMap[id]); + .filter((id) => !courseMap[id]) if (extraCourseIds.length) { const extraCourses = await CourseModel.find({ _id: { $in: extraCourseIds } }) - .select("cuorse_name course_image teacher_name academyId user_id caption likes category") - .lean(); + .select('cuorse_name course_image teacher_name academyId user_id caption likes category') + .lean() extraCourses.forEach((course) => { - courseMap[String(course._id)] = course; - }); + courseMap[String(course._id)] = course + }) const extraUserIds = extraCourses .map((c) => String(c.user_id)) - .filter((id) => id && !userMap[id]); + .filter((id) => id && !userMap[id]) if (extraUserIds.length) { const extraUsers = await UserModel.find({ _id: { $in: extraUserIds } }) - .select("user_name first_name last_name profile_image") - .lean(); + .select('user_name first_name last_name profile_image') + .lean() extraUsers.forEach((user) => { - userMap[String(user._id)] = user; - }); + userMap[String(user._id)] = user + }) } } - const mappedInterestContents = interestContents.map(mapContentItem); + const mappedInterestContents = interestContents.map(mapContentItem) items = blendInterestIntoRanked( items, [...items, ...mappedInterestContents], 0.25 - ).slice(0, limit); + ).slice(0, limit) } } } @@ -4748,7 +4749,7 @@ const getFreeAcademyExploreContent = async (req, res, next) => { } } - const totalPages = Math.ceil(totalItems / limit) || 1; + const totalPages = Math.ceil(totalItems / limit) || 1 return res.status(200).json({ success: true, @@ -4758,285 +4759,284 @@ const getFreeAcademyExploreContent = async (req, res, next) => { currentPage: page, totalPages, totalItems, - hasNextPage: page < totalPages, - }, - }, - }); + hasNextPage: page < totalPages + } + } + }) } catch (error) { - console.error("Error in getFreeAcademyExploreContent:", error); - next(error); + console.error('Error in getFreeAcademyExploreContent:', error) + next(error) } -}; - +} module.exports = { // ========================================== // 🏢 ماژول آکادمی (Academy) // ========================================== - + // پیدا کردن آکادمی کاربر جاری findAcademy, - + // پیدا کردن آکادمی با شناسه findAcademyById, - + // تکمیل و ویرایش پروفایل آکادمی academyProfile, - + // تغییر وضعیت آکادمی (فعال/غیرفعال - فقط ادمین) updateAcademyStatus, - + // دریافت اطلاعات یک آکادمی با شناسه getAcademyById, - + // ایجاد آکادمی جدید (دستی - معمولاً خودکار ایجاد می‌شود) createAcademy, - + // ویرایش اطلاعات آکادمی updateAcademy, - + // حذف آکادمی (به همراه تمام دوره‌های آن) deleteAcademy, - + // دریافت آمار کلی آکادمی‌ها (تعداد، میانگین امتیاز و...) getAcademyStats, // ========================================== // 📚 ماژول دوره‌ها (Course) // ========================================== - + // ایجاد دوره جدید creatCourse, - + // ویرایش اطلاعات دوره updateCourse, - + // حذف دوره (تغییر وضعیت به reject) deleteCourse, - + // دریافت لیست دوره‌های آکادمی جاری getCourse, - + // دریافت لیست همه دوره‌ها با فیلتر و جستجو getCourses, // محتوای رایگان آموزشگاه برای اکسپلور getFreeAcademyExploreContent, - + // دریافت دوره‌های یک آکادمی خاص getAcademyCourse, - + // آپلود ویدیو با روش Multipart creatCourseVideo, - + // آپلود ویدیو با روش Base64 (برای فایل‌های کوچک) creatCourseVideoBase64, // ========================================== // 💳 ماژول پرداخت (Payment) // ========================================== - + // پرداخت برای اشتراک پرو proPayment, - + // کال‌بک درگاه پرداخت زرین‌پال (اشتراک پرو) handleCoursePaymentCallback, - + // پرداخت برای خرید دوره coursePayment, - + // کال‌بک درگاه پرداخت زرین‌پال (خرید دوره) CoursePaymentCallback, // ========================================== // 🎬 ماژول محتوای دوره (Course Content) // ========================================== - + // دریافت محتوای دوره (لیست ویدیوها و فایل‌ها) getCourseContent, - + // حذف ویدیو از دوره deleteCourseVideo, - + // لایک کردن محتوا likeCourseContent, - + // حذف لایک (دیسلایک) dontLikeCourseContent, - + // دریافت تعداد لایک‌های یک دوره getCourseLikesCount, - + // دریافت لیست دوره‌های لایک شده توسط کاربر getUserLikedCourses, - + // بررسی وضعیت لایک کاربر (آیا لایک کرده یا نه) isLikeCourseContent, - + // ========================================== // 💬 ماژول کامنت‌ها (Comments) // ========================================== - + // ایجاد کامنت جدید createComment, - + // دریافت تعداد کل کامنت‌های یک دوره getTotalCommentsCount, - + // دریافت لیست کامنت‌های یک دوره (با فیلتر و صفحه‌بندی) getCourseComments, - + // تغییر وضعیت کامنت (تایید/رد/در انتظار) updateCommentStatus, - + // دریافت کامنت‌های یک کاربر خاص getUserComments, - + // حذف کامنت deleteComment, - + // ========================================== // 🛒 ماژول دوره‌های خریداری شده (Purchased) // ========================================== - + // دریافت دوره‌های خریداری شده توسط کاربر جاری getUserPurchasedCourses, - + // دریافت دوره‌های خریداری شده (با populate - روش بهینه) getUserPurchasedCoursesWithPopulate, - + // دریافت همه پرداخت‌های آکادمی getAllAcademyPayments, getAcademyAnalytics, - + // بررسی خرید یک دوره توسط کاربر (برای صفحه دوره) checkCoursePurchase, // ========================================== // 👑 ماژول مدیریتی (Admin Panel) // ========================================== - + // ------ مدیریت پرداخت‌ها ------ - + // دریافت همه پرداخت‌های سیستم (ادمین) getAllPayments, - + // تغییر وضعیت پرداخت (pending/success/settled/failed) updatePaymentStatus, - + // ------ مدیریت دوره‌ها ------ - + // دریافت همه دوره‌های سیستم (ادمین) getAllCourses, - + // تغییر وضعیت دوره (تایید/رد/در انتظار) updateCourseStatus, - + // حذف کامل دوره از سیستم (ادمین) admindeleteCourse, - + // ------ مدیریت دوره‌های خریداری شده ------ - + // دریافت همه خریدهای سیستم (ادمین) getAllPurchasedCourses, - + // ------ مدیریت آکادمی‌ها ------ - + // دریافت همه آکادمی‌های سیستم (ادمین) getAllAcademies, - + // ------ مدیریت تنظیمات ------ - + // دریافت تنظیمات (تکس، قیمت‌ها و...) getSettings, - + // به‌روزرسانی تنظیمات updateSettings, - + // دریافت قیمت دوره‌ها (پرو، لجند، معمولی) getCoursePrices, - + // به‌روزرسانی قیمت یک نوع دوره updateCoursePrice, - + // دریافت درصد مالیات getTax, - + // به‌روزرسانی درصد مالیات updateTax, - + // دریافت همه تنظیمات یکجا getAllSettings, - + // به‌روزرسانی همه تنظیمات یکجا updateAllSettings, // ========================================== // 📊 ماژول آمار (Stats) // ========================================== - + // دریافت آمار پرداخت‌ها getPaymentStats, - + // دریافت اطلاعات یک دوره با شناسه getCourseById, - + // ایجاد دوره جدید (ادمین) createCourse, - + // ویرایش دوره (ادمین) updateCourse, - + // دریافت آمار کلی دوره‌ها getCourseStats, - + // دریافت آمار دوره‌های خریداری شده getPurchasedStats, - + // ========================================== // 📹 ماژول آپلود تکه‌تکه (Chunk Upload) // ========================================== - + // دریافت تکه ویدیو (هر بار یک تکه) creatCourseVideoChunk, - + // ادغام تکه‌ها بعد از اتمام آپلود mergeVideoChunks, - + // بررسی وضعیت آپلود (برای ادامه از جای قطع شده) getUploadStatus, // ========================================== // 🏷️ ماژول دسته‌بندی آکادمی (Category) // ========================================== - + // دریافت لیست دسته‌بندی‌ها (با فیلتر و صفحه‌بندی) getAll, - + // دریافت یک دسته‌بندی با شناسه getOne, - + // دریافت درخت دسته‌بندی (ساختار سلسله‌مراتبی والد-فرزند) getTree, - + // دریافت دسته‌بندی با اسلاگ (برای سئو و لینک‌های زیبا) getBySlug, - + // ایجاد دسته‌بندی جدید create, - + // ویرایش دسته‌بندی update, - + // حذف دسته‌بندی (سافت دیلیت - فقط غیرفعال می‌شود) remove, - + // تغییر وضعیت دسته‌بندی (فعال/غیرفعال) toggleStatus, - + // افزایش تعداد دوره‌های دسته‌بندی incrementCourseCount, - + // دریافت دسته‌بندی‌های ویژه (برای نمایش در صفحه اصلی) getFeatured -}; \ No newline at end of file +} diff --git a/controllers/application/advertising/advertisingController.js b/controllers/application/advertising/advertisingController.js index 6cfa6d4..92058a5 100644 --- a/controllers/application/advertising/advertisingController.js +++ b/controllers/application/advertising/advertisingController.js @@ -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) diff --git a/controllers/application/bookings/bookingPaymentController.js b/controllers/application/bookings/bookingPaymentController.js index 6e1e5f7..d5d77ef 100644 --- a/controllers/application/bookings/bookingPaymentController.js +++ b/controllers/application/bookings/bookingPaymentController.js @@ -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', diff --git a/controllers/application/financial/financialController.js b/controllers/application/financial/financialController.js index 4e60769..d24f8c6 100644 --- a/controllers/application/financial/financialController.js +++ b/controllers/application/financial/financialController.js @@ -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({ diff --git a/controllers/application/offer/offerController.js b/controllers/application/offer/offerController.js index d1b70a2..d1ca2c4 100644 --- a/controllers/application/offer/offerController.js +++ b/controllers/application/offer/offerController.js @@ -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, diff --git a/controllers/application/orders/orderController.js b/controllers/application/orders/orderController.js index 89dc30a..2b37c76 100644 --- a/controllers/application/orders/orderController.js +++ b/controllers/application/orders/orderController.js @@ -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, diff --git a/controllers/application/payment/paymentController.js b/controllers/application/payment/paymentController.js index 5039872..ec5b435 100644 --- a/controllers/application/payment/paymentController.js +++ b/controllers/application/payment/paymentController.js @@ -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 } diff --git a/controllers/application/payment/paymentControllerWeb.js b/controllers/application/payment/paymentControllerWeb.js index 1be0b00..8d08440 100644 --- a/controllers/application/payment/paymentControllerWeb.js +++ b/controllers/application/payment/paymentControllerWeb.js @@ -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 +} diff --git a/controllers/application/posts/postController.js b/controllers/application/posts/postController.js index 708389a..5c795b1 100644 --- a/controllers/application/posts/postController.js +++ b/controllers/application/posts/postController.js @@ -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) diff --git a/controllers/application/profile/accountController.js b/controllers/application/profile/accountController.js index 2350aec..e01aab5 100644 --- a/controllers/application/profile/accountController.js +++ b/controllers/application/profile/accountController.js @@ -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: 'الگوریتم بازنشانی شد' }) diff --git a/controllers/application/projects/createProjectController.js b/controllers/application/projects/createProjectController.js index a6c98e2..eac0d40 100644 --- a/controllers/application/projects/createProjectController.js +++ b/controllers/application/projects/createProjectController.js @@ -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}+` diff --git a/controllers/application/projects/getProjectController.js b/controllers/application/projects/getProjectController.js index 3e038d1..2fa2044 100644 --- a/controllers/application/projects/getProjectController.js +++ b/controllers/application/projects/getProjectController.js @@ -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) diff --git a/controllers/application/projects/updateProjectController.js b/controllers/application/projects/updateProjectController.js index 5a4d90e..28c52a7 100644 --- a/controllers/application/projects/updateProjectController.js +++ b/controllers/application/projects/updateProjectController.js @@ -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') { diff --git a/controllers/application/shop-products/shopProductController.js b/controllers/application/shop-products/shopProductController.js index 73b1f88..b0e373f 100644 --- a/controllers/application/shop-products/shopProductController.js +++ b/controllers/application/shop-products/shopProductController.js @@ -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 = { diff --git a/controllers/application/users/getUserController.js b/controllers/application/users/getUserController.js index 41003b4..50a3fc3 100644 --- a/controllers/application/users/getUserController.js +++ b/controllers/application/users/getUserController.js @@ -1,310 +1,310 @@ /* eslint-disable camelcase */ -const { default: mongoose } = require("mongoose"); -const LikeModel = require("../../../models/LikeModel"); -const UserModel = require("../../../models/UserModel"); -const AdvertisingProfileModel = require("../../../models/AdvertisingProfile"); -const jwt = require("jsonwebtoken"); -const moment = require("moment-jalaali"); -const NotificationModel = require("../../../models/NotificationModel"); -const CommentModel = require("../../../models/CommentModel"); +const { default: mongoose } = require('mongoose') +const LikeModel = require('../../../models/LikeModel') +const UserModel = require('../../../models/UserModel') +const AdvertisingProfileModel = require('../../../models/AdvertisingProfile') +const jwt = require('jsonwebtoken') +const moment = require('moment-jalaali') +const NotificationModel = require('../../../models/NotificationModel') +const CommentModel = require('../../../models/CommentModel') const { hasCreatorRatedUser, resolveRatingForComment, - recalculateUserRating, -} = require("../../../utils/commentRating"); -const { resolveCommentModerationStatus } = require("../../../utils/contentModeration"); -const { saveCommentWithModerationStatus } = require("../../../utils/commentStatusSave"); -const { createCommentNotification, createRatingNotification } = require("../../../utils/commentNotification"); -const { ProvinceModel, CityModel } = require("../../../models/StateCity"); -const OfferModel = require("../../../models/OfferModel"); -const PostModel = require("../../../models/PostModel"); -const ProjectModel = require("../../../models/ProjectModel"); -const LicenseModel = require("../../../models/license") + recalculateUserRating +} = require('../../../utils/commentRating') +const { resolveCommentModerationStatus } = require('../../../utils/contentModeration') +const { saveCommentWithModerationStatus } = require('../../../utils/commentStatusSave') +const { createCommentNotification, createRatingNotification } = require('../../../utils/commentNotification') +const { ProvinceModel, CityModel } = require('../../../models/StateCity') +const OfferModel = require('../../../models/OfferModel') +const PostModel = require('../../../models/PostModel') +const ProjectModel = require('../../../models/ProjectModel') +const LicenseModel = require('../../../models/license') const { viewerIsBlockedBy, viewerBlockedUser, userBlockedViewer, blockedProfilePayload, - getBlockerUserIds, -} = require("../../../utils/blockVisibility") -const { formatLastOnlineStatus } = require("../../../utils/onlineStatus") -const { getHiddenUserIds } = require("../../../utils/accountLifecycle") -const { normalizePostMediaFields } = require("../../../utils/storagePath") + getBlockerUserIds +} = require('../../../utils/blockVisibility') +const { formatLastOnlineStatus } = require('../../../utils/onlineStatus') +const { getHiddenUserIds } = require('../../../utils/accountLifecycle') +const { normalizePostMediaFields } = require('../../../utils/storagePath') const { filterPostsHiddenByPrivateProfile, - viewerCanSeeUserContent, -} = require("../../../utils/privateProfile") -const { applyReportHiddenFilter } = require("../../../utils/postModeration") + viewerCanSeeUserContent +} = require('../../../utils/privateProfile') +const { applyReportHiddenFilter } = require('../../../utils/postModeration') const { postStatusFilter, commentStatusFilter, PUBLIC_POST_STATUSES, - isPublicCommentStatus, -} = require("../../../utils/moderationStatus") -const { getFollowCounts } = require("../../../utils/followHelpers") -const UserFollowModel = require("../../../models/UserFollowModel") -const BookingConfigModel = require("../../../models/BookingConfigModel") -const { buildUserLevelMongoFilter, normalizeUserLevel } = require("../../../utils/userLevelFilter") -const { buildAppearanceUserFilter } = require("../../../utils/appearanceFilter") -const { paginatePersonalizedExplore } = require("../../../utils/exploreAlgorithm") -const ExploreInteractionModel = require("../../../models/ExploreInteractionModel") -const { attachPostEngagementFields } = require("../../../utils/postEngagement") + isPublicCommentStatus +} = require('../../../utils/moderationStatus') +const { getFollowCounts } = require('../../../utils/followHelpers') +const UserFollowModel = require('../../../models/UserFollowModel') +const BookingConfigModel = require('../../../models/BookingConfigModel') +const { buildUserLevelMongoFilter, normalizeUserLevel } = require('../../../utils/userLevelFilter') +const { buildAppearanceUserFilter } = require('../../../utils/appearanceFilter') +const { paginatePersonalizedExplore } = require('../../../utils/exploreAlgorithm') +const ExploreInteractionModel = require('../../../models/ExploreInteractionModel') +const { attachPostEngagementFields } = require('../../../utils/postEngagement') const { getFollowingReelsFeed, getSavedReelsFeed, - getNearMeReelsFeed, -} = require("../../../utils/reelsTabFeed") -const CommentLikeModel = require("../../../models/CommentLikeModel") -const { attachCommentLikeFields } = require("../../../utils/commentEngagement") + getNearMeReelsFeed +} = require('../../../utils/reelsTabFeed') +const CommentLikeModel = require('../../../models/CommentLikeModel') +const { attachCommentLikeFields } = require('../../../utils/commentEngagement') -const { filterPostsByDistance } = require("../../../utils/nearbyPosts"); +const { filterPostsByDistance } = require('../../../utils/nearbyPosts') -const EXPLORE_NEAR_RADIUS_METERS = 5000; +const EXPLORE_NEAR_RADIUS_METERS = 5000 -function haversineMeters(lat1, lon1, lat2, lon2) { - const toRad = (deg) => (deg * Math.PI) / 180; - const dLat = toRad(lat2 - lat1); - const dLon = toRad(lon2 - lon1); +function haversineMeters (lat1, lon1, lat2, lon2) { + const toRad = (deg) => (deg * Math.PI) / 180 + const dLat = toRad(lat2 - lat1) + const dLon = toRad(lon2 - lon1) const a = Math.sin(dLat / 2) ** 2 + - Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) ** 2; - return 6371000 * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) ** 2 + return 6371000 * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)) } -function escapeRegex(value) { - return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +function escapeRegex (value) { + return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&') } -async function applyCaptionSearchFilter(postFilter, { hashtag, q }, UserModel) { - const tag = hashtag ? String(hashtag).replace(/^#/, "").trim() : ""; +async function applyCaptionSearchFilter (postFilter, { hashtag, q }, UserModel) { + const tag = hashtag ? String(hashtag).replace(/^#/, '').trim() : '' if (tag) { postFilter.caption = { $regex: `#${escapeRegex(tag)}`, - $options: "i", - }; - return; + $options: 'i' + } + return } - const term = q ? String(q).trim() : ""; - if (!term) return; + const term = q ? String(q).trim() : '' + if (!term) return const matchingUsers = await UserModel.find({ $or: [ - { user_name: { $regex: escapeRegex(term), $options: "i" } }, - { first_name: { $regex: escapeRegex(term), $options: "i" } }, - { last_name: { $regex: escapeRegex(term), $options: "i" } }, - ], + { user_name: { $regex: escapeRegex(term), $options: 'i' } }, + { first_name: { $regex: escapeRegex(term), $options: 'i' } }, + { last_name: { $regex: escapeRegex(term), $options: 'i' } } + ] }) - .select("_id") - .lean(); + .select('_id') + .lean() - const userIds = matchingUsers.map((user) => user._id); + const userIds = matchingUsers.map((user) => user._id) const searchClauses = [ - { caption: { $regex: escapeRegex(term), $options: "i" } }, - ]; + { caption: { $regex: escapeRegex(term), $options: 'i' } } + ] if (userIds.length) { - searchClauses.push({ user_id: { $in: userIds } }); + searchClauses.push({ user_id: { $in: userIds } }) } if (postFilter.$and) { - postFilter.$and.push({ $or: searchClauses }); + postFilter.$and.push({ $or: searchClauses }) } else { - postFilter.$or = searchClauses; + postFilter.$or = searchClauses } } -function applyExploreFilterToUserFilter(exploreFilter, userFilter) { - if (!exploreFilter) return; +function applyExploreFilterToUserFilter (exploreFilter, userFilter) { + if (!exploreFilter) return switch (exploreFilter) { - case "model": - userFilter.expertise = "مدل"; - break; - case "photographer": - userFilter.expertise = "عکاس"; - break; - case "hairstylist": - userFilter.expertise = "آرایشگر"; - break; - case "makeup": - userFilter.sub_expertise = "میکاپ"; - break; - case "professional": - Object.assign(userFilter, buildUserLevelMongoFilter("حرفه‌ای")); - break; - default: - break; + case 'model': + userFilter.expertise = 'مدل' + break + case 'photographer': + userFilter.expertise = 'عکاس' + break + case 'hairstylist': + userFilter.expertise = 'آرایشگر' + break + case 'makeup': + userFilter.sub_expertise = 'میکاپ' + break + case 'professional': + Object.assign(userFilter, buildUserLevelMongoFilter('حرفه‌ای')) + break + default: + break } } -async function mapPostsWithUsers(posts, users, decodedToken) { +async function mapPostsWithUsers (posts, users, decodedToken) { let filteredPosts = await filterPostsHiddenByPrivateProfile( posts, decodedToken?.id - ); + ) if (decodedToken?.id) { const blockerIds = new Set( (await getBlockerUserIds(UserModel, decodedToken.id)).map(String) - ); + ) filteredPosts = filteredPosts.filter( (post) => !blockerIds.has(String(post.user_id)) - ); + ) } const likes = decodedToken ? await LikeModel.find({ - userId: decodedToken.id, - postId: { $in: filteredPosts.map((p) => p._id) }, - }).lean() - : []; - const likesMap = {}; + userId: decodedToken.id, + postId: { $in: filteredPosts.map((p) => p._id) } + }).lean() + : [] + const likesMap = {} likes.forEach((l) => { - likesMap[l.postId.toString()] = true; - }); + likesMap[l.postId.toString()] = true + }) const comments = await CommentModel.aggregate([ { $match: { post: { $in: filteredPosts.map((p) => p._id) }, - status: commentStatusFilter(), - }, + status: commentStatusFilter() + } }, - { $group: { _id: "$post", count: { $sum: 1 } } }, - ]); - const commentsMap = {}; + { $group: { _id: '$post', count: { $sum: 1 } } } + ]) + const commentsMap = {} comments.forEach((c) => { - commentsMap[c._id.toString()] = c.count; - }); + commentsMap[c._id.toString()] = c.count + }) const mapped = filteredPosts.map((post) => { const user = - users.find((u) => u._id.toString() === post.user_id.toString()) || {}; - const normalized = normalizePostMediaFields(post); + users.find((u) => u._id.toString() === post.user_id.toString()) || {} + const normalized = normalizePostMediaFields(post) return { ...normalized, - user_name: user.user_name || "", - first_name: user.first_name || "", - last_name: user.last_name || "", - expertise: user.expertise || "", + user_name: user.user_name || '', + first_name: user.first_name || '', + last_name: user.last_name || '', + expertise: user.expertise || '', sub_expertise: user.sub_expertise || [], province: user.show_location ? user.province || {} : {}, city: user.show_location ? user.city || {} : {}, show_location: user.show_location, - user_level: user.user_level || "", - profile_image: user.profile_image || "", - is_verified: user.is_verified || "", - verify_badge: user.verify_badge || "", + user_level: user.user_level || '', + profile_image: user.profile_image || '', + is_verified: user.is_verified || '', + verify_badge: user.verify_badge || '', user_score: user.user_score || 0, rate: user.rate || 0, likesCount: post.likes ? post.likes.length : 0, is_liked: !!likesMap[post._id.toString()], - commentsCount: commentsMap[post._id.toString()] || 0, - }; - }); + commentsCount: commentsMap[post._id.toString()] || 0 + } + }) - return attachPostEngagementFields(mapped, decodedToken?.id); + return attachPostEngagementFields(mapped, decodedToken?.id) } -async function getRankedExplorePosts({ +async function getRankedExplorePosts ({ windowDays, page, limit, postFilter, - decodedToken, + decodedToken }) { - const since = new Date(Date.now() - windowDays * 24 * 60 * 60 * 1000); - const postIdsWithActivity = new Set(); + const since = new Date(Date.now() - windowDays * 24 * 60 * 60 * 1000) + const postIdsWithActivity = new Set() const recentLikes = await LikeModel.find({ createdAt: { $gte: since } }) - .select("postId") - .lean(); + .select('postId') + .lean() recentLikes.forEach((like) => { - if (like.postId) postIdsWithActivity.add(String(like.postId)); - }); + if (like.postId) postIdsWithActivity.add(String(like.postId)) + }) const recentComments = await CommentModel.find({ createdAt: { $gte: since }, status: commentStatusFilter(), - comment_for: "post", - post: { $ne: null }, + comment_for: 'post', + post: { $ne: null } }) - .select("post") - .lean(); + .select('post') + .lean() recentComments.forEach((comment) => { - if (comment.post) postIdsWithActivity.add(String(comment.post)); - }); + if (comment.post) postIdsWithActivity.add(String(comment.post)) + }) const recentPosts = await PostModel.find({ ...postFilter, - createdAt: { $gte: since }, + createdAt: { $gte: since } }) - .select("_id") - .lean(); - recentPosts.forEach((post) => postIdsWithActivity.add(String(post._id))); + .select('_id') + .lean() + recentPosts.forEach((post) => postIdsWithActivity.add(String(post._id))) if (!postIdsWithActivity.size) { - return { posts: [], totalItems: 0 }; + return { posts: [], totalItems: 0 } } let posts = await PostModel.find({ ...postFilter, - _id: { $in: [...postIdsWithActivity] }, - }).lean(); + _id: { $in: [...postIdsWithActivity] } + }).lean() - const likeCounts = {}; + const likeCounts = {} recentLikes.forEach((like) => { - const id = String(like.postId); - likeCounts[id] = (likeCounts[id] || 0) + 1; - }); + const id = String(like.postId) + likeCounts[id] = (likeCounts[id] || 0) + 1 + }) - const commentCounts = {}; + const commentCounts = {} recentComments.forEach((comment) => { - const id = String(comment.post); - commentCounts[id] = (commentCounts[id] || 0) + 1; - }); + const id = String(comment.post) + commentCounts[id] = (commentCounts[id] || 0) + 1 + }) posts = posts .map((post) => { - const id = String(post._id); + const id = String(post._id) const score = (likeCounts[id] || 0) + (commentCounts[id] || 0) + - (post.likes ? post.likes.length : 0) * 0.1; - return { ...post, exploreScore: score }; + (post.likes ? post.likes.length : 0) * 0.1 + return { ...post, exploreScore: score } }) - .sort((a, b) => b.exploreScore - a.exploreScore); + .sort((a, b) => b.exploreScore - a.exploreScore) - const userIds = [...new Set(posts.map((p) => String(p.user_id)))]; + const userIds = [...new Set(posts.map((p) => String(p.user_id)))] const users = await UserModel.find({ _id: { $in: userIds } }) .select( - "_id user_name first_name last_name expertise sub_expertise province city user_level show_location profile_image is_verified verify_badge user_score rate blocked_users" + '_id user_name first_name last_name expertise sub_expertise province city user_level show_location profile_image is_verified verify_badge user_score rate blocked_users' ) - .lean(); + .lean() - posts = await mapPostsWithUsers(posts, users, decodedToken); + posts = await mapPostsWithUsers(posts, users, decodedToken) - const totalItems = posts.length; - const startIndex = (page - 1) * limit; - const paginatedPosts = posts.slice(startIndex, startIndex + parseInt(limit, 10)); + const totalItems = posts.length + const startIndex = (page - 1) * limit + const paginatedPosts = posts.slice(startIndex, startIndex + parseInt(limit, 10)) - return { posts: paginatedPosts, totalItems }; + return { posts: paginatedPosts, totalItems } } /** * پیش‌بینی ترند: پست‌های نسبتاً تازه با شتاب تعامل بالا * (هنوز ترند مطلق نشده‌اند، ولی پتانسیل رشد دارند) */ -async function getPredictedTrendPosts({ +async function getPredictedTrendPosts ({ page, limit, postFilter, - decodedToken, + decodedToken }) { - const lookbackDays = 14; - const recentHours = 36; - const since = new Date(Date.now() - lookbackDays * 24 * 60 * 60 * 1000); - const recentSince = new Date(Date.now() - recentHours * 60 * 60 * 1000); + const lookbackDays = 14 + const recentHours = 36 + const since = new Date(Date.now() - lookbackDays * 24 * 60 * 60 * 1000) + const recentSince = new Date(Date.now() - recentHours * 60 * 60 * 1000) const [likesLookback, commentsLookback, likesRecent, commentsRecent] = await Promise.all([ @@ -326,135 +326,135 @@ async function getPredictedTrendPosts({ }) .select('post') .lean() - ]); + ]) - const candidateIds = new Set(); + const candidateIds = new Set() likesLookback.forEach((like) => { - if (like.postId) candidateIds.add(String(like.postId)); - }); + if (like.postId) candidateIds.add(String(like.postId)) + }) commentsLookback.forEach((comment) => { - if (comment.post) candidateIds.add(String(comment.post)); - }); + if (comment.post) candidateIds.add(String(comment.post)) + }) const freshPosts = await PostModel.find({ ...postFilter, createdAt: { $gte: since } }) .select('_id') - .lean(); - freshPosts.forEach((post) => candidateIds.add(String(post._id))); + .lean() + freshPosts.forEach((post) => candidateIds.add(String(post._id))) if (!candidateIds.size) { - return { posts: [], totalItems: 0 }; + return { posts: [], totalItems: 0 } } - let posts = await PostModel.find({ + const posts = await PostModel.find({ ...postFilter, _id: { $in: [...candidateIds] }, createdAt: { $gte: since } - }).lean(); + }).lean() if (!posts.length) { - return { posts: [], totalItems: 0 }; + return { posts: [], totalItems: 0 } } - const recentLikeCounts = {}; + const recentLikeCounts = {} likesRecent.forEach((like) => { - const id = String(like.postId); - recentLikeCounts[id] = (recentLikeCounts[id] || 0) + 1; - }); + const id = String(like.postId) + recentLikeCounts[id] = (recentLikeCounts[id] || 0) + 1 + }) - const recentCommentCounts = {}; + const recentCommentCounts = {} commentsRecent.forEach((comment) => { - const id = String(comment.post); - recentCommentCounts[id] = (recentCommentCounts[id] || 0) + 1; - }); + const id = String(comment.post) + recentCommentCounts[id] = (recentCommentCounts[id] || 0) + 1 + }) - const lookbackLikeCounts = {}; + const lookbackLikeCounts = {} likesLookback.forEach((like) => { - const id = String(like.postId); - lookbackLikeCounts[id] = (lookbackLikeCounts[id] || 0) + 1; - }); + const id = String(like.postId) + lookbackLikeCounts[id] = (lookbackLikeCounts[id] || 0) + 1 + }) - const lookbackCommentCounts = {}; + const lookbackCommentCounts = {} commentsLookback.forEach((comment) => { - const id = String(comment.post); - lookbackCommentCounts[id] = (lookbackCommentCounts[id] || 0) + 1; - }); + const id = String(comment.post) + lookbackCommentCounts[id] = (lookbackCommentCounts[id] || 0) + 1 + }) const totalLikesList = posts .map((p) => (Array.isArray(p.likes) ? p.likes.length : 0)) - .sort((a, b) => a - b); + .sort((a, b) => a - b) const viralCutoff = totalLikesList[Math.floor(totalLikesList.length * 0.9)] || totalLikesList[totalLikesList.length - 1] || - 50; + 50 const scored = posts .map((post) => { - const id = String(post._id); - const totalLikes = Array.isArray(post.likes) ? post.likes.length : 0; - const recentLikes = recentLikeCounts[id] || 0; - const recentComments = recentCommentCounts[id] || 0; - const windowLikes = lookbackLikeCounts[id] || 0; - const windowComments = lookbackCommentCounts[id] || 0; + const id = String(post._id) + const totalLikes = Array.isArray(post.likes) ? post.likes.length : 0 + const recentLikes = recentLikeCounts[id] || 0 + const recentComments = recentCommentCounts[id] || 0 + const windowLikes = lookbackLikeCounts[id] || 0 + const windowComments = lookbackCommentCounts[id] || 0 const ageHours = Math.max( 1, (Date.now() - new Date(post.createdAt).getTime()) / (1000 * 60 * 60) - ); + ) - const recentSignal = recentLikes + recentComments; - const windowSignal = windowLikes + windowComments + totalLikes * 0.15; + const recentSignal = recentLikes + recentComments + const windowSignal = windowLikes + windowComments + totalLikes * 0.15 if (recentSignal < 1 && windowSignal < 2) { - return null; + return null } const recentVelocity = (recentLikes * 0.7 + recentComments * 1.4) / - Math.sqrt(Math.min(ageHours, recentHours) / 12); + Math.sqrt(Math.min(ageHours, recentHours) / 12) const earlyMomentum = - (totalLikes * 0.55 + windowComments * 1.1) / Math.sqrt(ageHours / 24); + (totalLikes * 0.55 + windowComments * 1.1) / Math.sqrt(ageHours / 24) const acceleration = recentLikes / Math.max(1, totalLikes) + - recentComments / Math.max(1, windowComments || 1); - const freshnessBoost = Math.exp(-ageHours / (lookbackDays * 24 * 0.55)); + recentComments / Math.max(1, windowComments || 1) + const freshnessBoost = Math.exp(-ageHours / (lookbackDays * 24 * 0.55)) // پست‌های از قبل وایرال → مناسب فیلتر «ترند»، نه پیش‌بینی const alreadyViralPenalty = - totalLikes >= Math.max(viralCutoff, 40) && recentSignal < 3 ? 0.25 : 1; + totalLikes >= Math.max(viralCutoff, 40) && recentSignal < 3 ? 0.25 : 1 const score = (recentVelocity * 2.8 + earlyMomentum * 1.1 + acceleration * 3.5 + freshnessBoost * 2) * - alreadyViralPenalty; + alreadyViralPenalty - return { ...post, exploreScore: score }; + return { ...post, exploreScore: score } }) .filter(Boolean) - .sort((a, b) => b.exploreScore - a.exploreScore); + .sort((a, b) => b.exploreScore - a.exploreScore) if (!scored.length) { - return { posts: [], totalItems: 0 }; + return { posts: [], totalItems: 0 } } - const userIds = [...new Set(scored.map((p) => String(p.user_id)))]; + const userIds = [...new Set(scored.map((p) => String(p.user_id)))] const users = await UserModel.find({ _id: { $in: userIds } }) .select( '_id user_name first_name last_name expertise sub_expertise province city user_level show_location profile_image is_verified verify_badge user_score rate blocked_users' ) - .lean(); + .lean() - const mapped = await mapPostsWithUsers(scored, users, decodedToken); - const totalItems = mapped.length; - const startIndex = (page - 1) * limit; + const mapped = await mapPostsWithUsers(scored, users, decodedToken) + const totalItems = mapped.length + const startIndex = (page - 1) * limit const paginatedPosts = mapped.slice( startIndex, startIndex + parseInt(limit, 10) - ); + ) - return { posts: paginatedPosts, totalItems }; + return { posts: paginatedPosts, totalItems } } const getAllLicenses = async (req, res, next) => { @@ -468,7 +468,6 @@ const getAllLicenses = async (req, res, next) => { } } - // GET: دریافت License بر اساس userId const getLicenseByUserId = async (req, res, next) => { try { @@ -476,32 +475,30 @@ const getLicenseByUserId = async (req, res, next) => { const { userId } = req.params if (!mongoose.Types.ObjectId.isValid(userId)) { - return res.status(400).json({ message: "شناسه کاربر معتبر نیست" }) + return res.status(400).json({ message: 'شناسه کاربر معتبر نیست' }) } if (requesterId !== String(userId)) { - return res.status(403).json({ message: "فقط مالک می‌تواند مجوز خود را ببیند" }) + return res.status(403).json({ message: 'فقط مالک می‌تواند مجوز خود را ببیند' }) } const license = await LicenseModel.findOne({ userId }).populate({ - path: "userId", + path: 'userId', model: UserModel, - select: "user_name first_name last_name is_verified verify_badge profile_image", + select: 'user_name first_name last_name is_verified verify_badge profile_image' }) if (!license) { - return res.status(404).json({ message: "License یافت نشد" }) + return res.status(404).json({ message: 'License یافت نشد' }) } res.status(200).json(license) } catch (error) { - console.error("Error fetching license:", error) + console.error('Error fetching license:', error) next(error) } } - - // POST: ذخیره License به صورت Base64 const createLicense = async (req, res, next) => { try { @@ -509,19 +506,19 @@ const createLicense = async (req, res, next) => { const { userId, image } = req.body if (!userId || !image) { - return res.status(400).json({ message: "userId و تصویر الزامی هستند" }) + return res.status(400).json({ message: 'userId و تصویر الزامی هستند' }) } if (!mongoose.Types.ObjectId.isValid(userId)) { - return res.status(400).json({ message: "شناسه کاربر معتبر نیست" }) + return res.status(400).json({ message: 'شناسه کاربر معتبر نیست' }) } if (requesterId !== String(userId)) { - return res.status(403).json({ message: "فقط می‌توانید برای حساب خود مجوز ثبت کنید" }) + return res.status(403).json({ message: 'فقط می‌توانید برای حساب خود مجوز ثبت کنید' }) } - if (!image.startsWith("data:image/")) { - return res.status(400).json({ message: "فقط فایل‌های تصویری مجاز هستند" }) + if (!image.startsWith('data:image/')) { + return res.status(400).json({ message: 'فقط فایل‌های تصویری مجاز هستند' }) } const existingLicense = await LicenseModel.findOne({ userId }) @@ -531,26 +528,25 @@ const createLicense = async (req, res, next) => { await existingLicense.save() return res.status(200).json({ - message: "تصویر مجوز با موفقیت به‌روزرسانی شد", - license: existingLicense, + message: 'تصویر مجوز با موفقیت به‌روزرسانی شد', + license: existingLicense }) } const license = new LicenseModel({ userId, Confirmation: false, - licenseImg: image, + licenseImg: image }) await license.save() - res.status(201).json({ message: "License با موفقیت ذخیره شد", license }) + res.status(201).json({ message: 'License با موفقیت ذخیره شد', license }) } catch (error) { - console.error("Error saving license:", error) + console.error('Error saving license:', error) next(error) } } - // PUT: به‌روزرسانی Confirmation — فقط ادمین پنل (این مسیر اپ دیگر self-verify نیست) const updateLicenseConfirmation = async (req, res, next) => { try { @@ -562,7 +558,6 @@ const updateLicenseConfirmation = async (req, res, next) => { } } - // const getUsers = async (req, res, next) => { // try { // const token = req.header('Authorization').split(' ')[1] @@ -658,10 +653,10 @@ const updateLicenseConfirmation = async (req, res, next) => { // } const getUsers = async (req, res, next) => { try { - const token = req.header("Authorization").split(" ")[1]; - if (!token) return res.status(401).send("Access Denied"); + 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 decodedToken = jwt.verify(token, process.env.APP_SECRET) const { expertise, page = 1, @@ -669,27 +664,27 @@ const getUsers = async (req, res, next) => { province, city, userLevel, - rateFilter, - } = req.query; - const filter = {}; + rateFilter + } = req.query + const filter = {} if (expertise) { - filter.expertise = expertise; + filter.expertise = expertise } if (province) { const provinceFind = await ProvinceModel.findOne( { id: province }, { _id: 0 } - ); - console.log("provinceFind:", provinceFind); + ) + console.log('provinceFind:', provinceFind) if (provinceFind) { - filter["province.id"] = provinceFind.id; + filter['province.id'] = provinceFind.id } } if (city) { - const cityFind = await CityModel.findOne({ id: city }); - console.log("cityFind:", cityFind); + const cityFind = await CityModel.findOne({ id: city }) + console.log('cityFind:', cityFind) if (cityFind) { - filter["city.id"] = cityFind.id; + filter['city.id'] = cityFind.id } } if (userLevel) { @@ -702,35 +697,35 @@ const getUsers = async (req, res, next) => { { $match: { last_post: { $ne: null } } }, { $addFields: { - "last_post.likesCount": { - $size: { $ifNull: ["$last_post.likes", []] }, - }, - }, + 'last_post.likesCount': { + $size: { $ifNull: ['$last_post.likes', []] } + } + } }, { $lookup: { - from: "comments", - let: { userId: "$_id" }, + from: 'comments', + let: { userId: '$_id' }, pipeline: [ { $match: { $expr: { $and: [ - { $eq: ["$user", "$$userId"] }, - { $eq: ["$status", "accepted"] }, - ], - }, - }, + { $eq: ['$user', '$$userId'] }, + { $eq: ['$status', 'accepted'] } + ] + } + } }, - { $count: "commentsCount" }, + { $count: 'commentsCount' } ], - as: "comments", - }, + as: 'comments' + } }, { $addFields: { - commentsCount: { $arrayElemAt: ["$comments.commentsCount", 0] }, - }, + commentsCount: { $arrayElemAt: ['$comments.commentsCount', 0] } + } }, { $project: { @@ -760,59 +755,59 @@ const getUsers = async (req, res, next) => { createdAt: 1, __v: 1, likesCount: 1, - is_liked: 1, + is_liked: 1 }, - commentsCount: 1, - }, + commentsCount: 1 + } }, - { $sort: { "last_post.createdAt": -1 } }, - ]); + { $sort: { 'last_post.createdAt': -1 } } + ]) - users = users.filter((user) => user.last_post !== null); + users = users.filter((user) => user.last_post !== null) if (!users || users.length === 0) { return res .status(404) - .json({ message: "هیچ کاربری با این مشخصات یافت نشد" }); + .json({ message: 'هیچ کاربری با این مشخصات یافت نشد' }) } users = await Promise.all( users.map(async (user) => { const isLikedDocument = await LikeModel.findOne({ postId: user.last_post._id, - userId: decodedToken.id, - }); - user.last_post.is_liked = !!isLikedDocument; - return user; + userId: decodedToken.id + }) + user.last_post.is_liked = !!isLikedDocument + return user }) - ); + ) if (rateFilter) { users.sort((a, b) => { - if (rateFilter === "کمترین") { - return a.rate - b.rate; - } else if (rateFilter === "بیشترین") { - return b.rate - a.rate; + if (rateFilter === 'کمترین') { + return a.rate - b.rate + } else if (rateFilter === 'بیشترین') { + return b.rate - a.rate } - return 0; - }); + return 0 + }) } - const startIndex = (page - 1) * limit; - const endIndex = page * limit; - const totalItems = users.length; + const startIndex = (page - 1) * limit + const endIndex = page * limit + const totalItems = users.length - const paginatedUsers = users.slice(startIndex, endIndex); + const paginatedUsers = users.slice(startIndex, endIndex) return res.status(200).json({ users: paginatedUsers, totalPages: Math.ceil(totalItems / limit), - totalItems, - }); + totalItems + }) } catch (error) { - next(error); + next(error) } -}; +} // const getUsersWeb = async (req, res, next) => { // try { // const token = req.header('Authorization')?.split(' ')[1] @@ -1133,13 +1128,13 @@ const getUsers = async (req, res, next) => { const getPostsWeb = async (req, res, next) => { try { - const token = req.header("Authorization")?.split(" ")[1]; - let decodedToken = null; + const token = req.header('Authorization')?.split(' ')[1] + let decodedToken = null if (token) { try { - decodedToken = jwt.verify(token, process.env.APP_SECRET); + decodedToken = jwt.verify(token, process.env.APP_SECRET) } catch (err) { - console.warn("توکن نامعتبر است، ادامه بدون احراز هویت"); + console.warn('توکن نامعتبر است، ادامه بدون احراز هویت') } } @@ -1162,33 +1157,33 @@ const getPostsWeb = async (req, res, next) => { sort, reelsTab, hashtag, - q, - } = req.query; + q + } = req.query - const reelsMode = feedMode === "reels" || type === "video"; - const gridMode = feedMode === "grid"; + const reelsMode = feedMode === 'reels' || type === 'video' + const gridMode = feedMode === 'grid' - const postFilter = { status: postStatusFilter(), is_archived: { $ne: true } }; + const postFilter = { status: postStatusFilter(), is_archived: { $ne: true } } if (type === 'video' || type === 'image') { - postFilter.type = type; + postFilter.type = type } - applyCaptionSearchFilter(postFilter, { hashtag, q }, UserModel); + applyCaptionSearchFilter(postFilter, { hashtag, q }, UserModel) - const hiddenUserIds = await getHiddenUserIds(); + const hiddenUserIds = await getHiddenUserIds() if (hiddenUserIds.length) { - if (!postFilter.$and) postFilter.$and = []; - postFilter.$and.push({ user_id: { $nin: hiddenUserIds } }); + if (!postFilter.$and) postFilter.$and = [] + postFilter.$and.push({ user_id: { $nin: hiddenUserIds } }) } - applyReportHiddenFilter(postFilter); + applyReportHiddenFilter(postFilter) if ( - reelsTab === "following" || - reelsTab === "saved" || - reelsTab === "near_me" + reelsTab === 'following' || + reelsTab === 'saved' || + reelsTab === 'near_me' ) { - if (reelsTab === "near_me") { + if (reelsTab === 'near_me') { const tabResult = await getNearMeReelsFeed({ lat, lng, @@ -1196,7 +1191,7 @@ const getPostsWeb = async (req, res, next) => { limit, postFilter, mapPostsWithUsers, - decodedToken, + decodedToken }) return res.status(200).json(tabResult) } @@ -1208,38 +1203,38 @@ const getPostsWeb = async (req, res, next) => { totalItems: 0, feedMeta: { requiresAuth: true, - emptyFollowing: reelsTab === "following", - emptySaved: reelsTab === "saved", - }, - }); + emptyFollowing: reelsTab === 'following', + emptySaved: reelsTab === 'saved' + } + }) } const tabResult = - reelsTab === "following" + reelsTab === 'following' ? await getFollowingReelsFeed({ - userId: decodedToken.id, - page, - limit, - postFilter, - mapPostsWithUsers, - }) + userId: decodedToken.id, + page, + limit, + postFilter, + mapPostsWithUsers + }) : await getSavedReelsFeed({ - userId: decodedToken.id, - page, - limit, - postFilter, - mapPostsWithUsers, - }); + userId: decodedToken.id, + page, + limit, + postFilter, + mapPostsWithUsers + }) - return res.status(200).json(tabResult); + return res.status(200).json(tabResult) } - const parsedLat = parseFloat(lat); - const parsedLng = parseFloat(lng); + const parsedLat = parseFloat(lat) + const parsedLng = parseFloat(lng) const hasNearMeCoords = - exploreFilter === "near_me" && + exploreFilter === 'near_me' && Number.isFinite(parsedLat) && - Number.isFinite(parsedLng); + Number.isFinite(parsedLng) // اکسپلور: همه پست‌ها (عکس/ویدئو) بدون فیلتر تخصص/مکان — feedMode=list (مثلاً «یافتن // متخصصین») عمداً از این مسیر (و wraparound بی‌نهایتِ paginatePersonalizedExplore @@ -1256,7 +1251,7 @@ const getPostsWeb = async (req, res, next) => { !exploreFilter && !subExpertise && !hasNearMeCoords && - feedMode !== "list" && + feedMode !== 'list' && !req.query.heightMin && !req.query.heightMax && !req.query.weightMin && @@ -1264,99 +1259,99 @@ const getPostsWeb = async (req, res, next) => { !req.query.sizeMin && !req.query.sizeMax && !req.query.hair_color && - !req.query.eye_color; + !req.query.eye_color - if (exploreFilter === "trending" || exploreFilter === "best_month") { - const windowDays = exploreFilter === "trending" ? 7 : 30; + if (exploreFilter === 'trending' || exploreFilter === 'best_month') { + const windowDays = exploreFilter === 'trending' ? 7 : 30 const ranked = await getRankedExplorePosts({ windowDays, page: parseInt(page, 10), limit: parseInt(limit, 10), postFilter: { ...postFilter, status: postStatusFilter() }, - decodedToken, - }); + decodedToken + }) return res.status(200).json({ posts: ranked.posts, totalPages: Math.ceil(ranked.totalItems / limit) || 1, - totalItems: ranked.totalItems, - }); + totalItems: ranked.totalItems + }) } - if (exploreFilter === "predict_trends") { + if (exploreFilter === 'predict_trends') { const predicted = await getPredictedTrendPosts({ page: parseInt(page, 10), limit: parseInt(limit, 10), postFilter: { ...postFilter, status: postStatusFilter() }, - decodedToken, - }); + decodedToken + }) return res.status(200).json({ posts: predicted.posts, totalPages: Math.ceil(predicted.totalItems / limit) || 1, - totalItems: predicted.totalItems, - }); + totalItems: predicted.totalItems + }) } if (hasNearMeCoords) { let posts = await PostModel.find({ ...postFilter, status: postStatusFilter(), - lat: { $nin: [null, ""] }, - lng: { $nin: [null, ""] }, + lat: { $nin: [null, ''] }, + lng: { $nin: [null, ''] } }) .sort({ createdAt: -1 }) - .lean(); + .lean() posts = filterPostsByDistance( posts, parsedLat, parsedLng, EXPLORE_NEAR_RADIUS_METERS - ); + ) - const userIds = [...new Set(posts.map((p) => String(p.user_id)))]; + const userIds = [...new Set(posts.map((p) => String(p.user_id)))] const users = await UserModel.find({ _id: { $in: userIds } }) .select( - "_id user_name first_name last_name expertise sub_expertise province city user_level show_location profile_image is_verified verify_badge user_score rate lat lng blocked_users" + '_id user_name first_name last_name expertise sub_expertise province city user_level show_location profile_image is_verified verify_badge user_score rate lat lng blocked_users' ) - .lean(); + .lean() - posts = await mapPostsWithUsers(posts, users, decodedToken); + posts = await mapPostsWithUsers(posts, users, decodedToken) - const totalItems = posts.length; - const startIndex = (parseInt(page, 10) - 1) * parseInt(limit, 10); + const totalItems = posts.length + const startIndex = (parseInt(page, 10) - 1) * parseInt(limit, 10) const paginatedPosts = posts.slice( startIndex, startIndex + parseInt(limit, 10) - ); + ) return res.status(200).json({ posts: paginatedPosts, totalPages: Math.ceil(totalItems / limit) || 1, - totalItems, - }); + totalItems + }) } if (isExploreFeed) { let explorePosts = await PostModel.find({ ...postFilter, status: postStatusFilter() }) - .lean(); - const userIds = [...new Set(explorePosts.map((p) => String(p.user_id)))]; + .lean() + const userIds = [...new Set(explorePosts.map((p) => String(p.user_id)))] const usersMap = await UserModel.find({ _id: { $in: userIds } }) .select('_id user_name first_name last_name expertise sub_expertise province city user_level show_location profile_image is_verified verify_badge user_score rate blocked_users') - .lean(); - const usersById = Object.fromEntries(usersMap.map((u) => [String(u._id), u])); + .lean() + const usersById = Object.fromEntries(usersMap.map((u) => [String(u._id), u])) if (decodedToken?.id) { const blockerIds = new Set( (await getBlockerUserIds(UserModel, decodedToken.id)).map(String) - ); + ) explorePosts = explorePosts.filter( (post) => !blockerIds.has(String(post.user_id)) - ); + ) } - explorePosts = await mapPostsWithUsers(explorePosts, usersMap, decodedToken); + explorePosts = await mapPostsWithUsers(explorePosts, usersMap, decodedToken) const { posts: paginatedPosts, totalItems, isColdStart } = await paginatePersonalizedExplore({ @@ -1368,36 +1363,36 @@ const getPostsWeb = async (req, res, next) => { seedPostId: reelsMode ? seedPostId : undefined, reelsMode, blendFresh: gridMode || (!reelsMode && !seedPostId), - cacheScope: reelsMode ? "reels" : "explore", - }); + cacheScope: reelsMode ? 'reels' : 'explore' + }) return res.status(200).json({ posts: paginatedPosts, totalPages: Math.ceil(totalItems / limit) || 1, totalItems, - feedMeta: { isColdStart: !!isColdStart }, - }); + feedMeta: { isColdStart: !!isColdStart } + }) } - const userFilter = {}; + const userFilter = {} - applyExploreFilterToUserFilter(exploreFilter, userFilter); - if (expertise) userFilter.expertise = expertise; - if (subExpertise) userFilter.sub_expertise = subExpertise; - let resolvedPostProvinceId = null; - let resolvedPostCityId = null; + applyExploreFilterToUserFilter(exploreFilter, userFilter) + if (expertise) userFilter.expertise = expertise + if (subExpertise) userFilter.sub_expertise = subExpertise + let resolvedPostProvinceId = null + let resolvedPostCityId = null if (province) { - const provinceFind = await ProvinceModel.findOne({ id: province }, { _id: 0 }); + const provinceFind = await ProvinceModel.findOne({ id: province }, { _id: 0 }) if (provinceFind) { - userFilter["province.id"] = provinceFind.id; - resolvedPostProvinceId = provinceFind.id; + userFilter['province.id'] = provinceFind.id + resolvedPostProvinceId = provinceFind.id } } if (city) { - const cityFind = await CityModel.findOne({ id: city }); + const cityFind = await CityModel.findOne({ id: city }) if (cityFind) { - userFilter["city.id"] = cityFind.id; - resolvedPostCityId = cityFind.id; + userFilter['city.id'] = cityFind.id + resolvedPostCityId = cityFind.id } } if (userLevel) { @@ -1405,61 +1400,61 @@ const getPostsWeb = async (req, res, next) => { if (levelFilter) Object.assign(userFilter, levelFilter) } Object.assign(userFilter, buildAppearanceUserFilter(req.query)) - if (_id) userFilter._id = _id; + if (_id) userFilter._id = _id if (_id && decodedToken?.id) { - const profileOwner = await UserModel.findById(_id).select('blocked_users is_private account_status'); + const profileOwner = await UserModel.findById(_id).select('blocked_users is_private account_status') if (viewerIsBlockedBy(profileOwner, decodedToken.id)) { - return res.status(200).json({ posts: [], totalPages: 0, totalItems: 0 }); + return res.status(200).json({ posts: [], totalPages: 0, totalItems: 0 }) } } if (_id) { - const profileOwner = await UserModel.findById(_id).select('blocked_users is_private account_status'); - const canView = await viewerCanSeeUserContent(decodedToken?.id, profileOwner); + const profileOwner = await UserModel.findById(_id).select('blocked_users is_private account_status') + const canView = await viewerCanSeeUserContent(decodedToken?.id, profileOwner) if (!canView) { return res.status(200).json({ posts: [], totalPages: 0, totalItems: 0, - privateLocked: true, - }); + privateLocked: true + }) } } - const userSelectFields = "_id user_name first_name last_name expertise sub_expertise province city user_level show_location profile_image is_verified verify_badge user_score rate pinned_post_ids"; + const userSelectFields = '_id user_name first_name last_name expertise sub_expertise province city user_level show_location profile_image is_verified verify_badge user_score rate pinned_post_ids' let users = await UserModel.find(userFilter) .select(userSelectFields) - .lean(); + .lean() - const userIds = (users || []).map(u => u._id); + const userIds = (users || []).map(u => u._id) // پست‌هایی که خودشون شهر/استان مطابق دارن هم دیده بشن، حتی اگه نویسنده‌شون بر اساس // مکان پروفایلش توی userFilter جا نشه — مثل بیلبورد/پروژه که مکان خودِ آگهی ملاکه، // نه فقط مکان صاحبش - const postLocationOr = []; - if (resolvedPostProvinceId != null) postLocationOr.push({ "province.id": resolvedPostProvinceId }); - if (resolvedPostCityId != null) postLocationOr.push({ "city.id": resolvedPostCityId }); + const postLocationOr = [] + if (resolvedPostProvinceId != null) postLocationOr.push({ 'province.id': resolvedPostProvinceId }) + if (resolvedPostCityId != null) postLocationOr.push({ 'city.id': resolvedPostCityId }) if (userIds.length === 0 && postLocationOr.length === 0) { - return res.status(200).json({ posts: [], totalPages: 0, totalItems: 0 }); + return res.status(200).json({ posts: [], totalPages: 0, totalItems: 0 }) } if (postLocationOr.length > 0) { postFilter.$or = [ ...(userIds.length ? [{ user_id: { $in: userIds } }] : []), - ...postLocationOr, - ]; + ...postLocationOr + ] } else { - postFilter.user_id = { $in: userIds }; + postFilter.user_id = { $in: userIds } } let posts = await PostModel.find(postFilter) .sort({ createdAt: -1 }) - .lean(); + .lean() if (!posts || posts.length === 0) { - return res.status(200).json({ posts: [], totalPages: 0, totalItems: 0 }); + return res.status(200).json({ posts: [], totalPages: 0, totalItems: 0 }) } // پست‌هایی که فقط با تطبیق مکان خودشون (نه مکان نویسنده) برگشتن، ممکنه نویسنده‌شون توی @@ -1469,49 +1464,49 @@ const getPostsWeb = async (req, res, next) => { posts .map(p => String(p.user_id)) .filter(id => !userIds.some(u => String(u) === id)) - ), - ]; + ) + ] if (missingAuthorIds.length) { const extraUsers = await UserModel.find({ _id: { $in: missingAuthorIds } }) .select(userSelectFields) - .lean(); - users = [...users, ...extraUsers]; + .lean() + users = [...users, ...extraUsers] } if (decodedToken?.id) { const blockerIds = new Set( (await getBlockerUserIds(UserModel, decodedToken.id)).map(String) - ); - posts = posts.filter((post) => !blockerIds.has(String(post.user_id))); + ) + posts = posts.filter((post) => !blockerIds.has(String(post.user_id))) if (!posts.length) { - return res.status(200).json({ posts: [], totalPages: 0, totalItems: 0 }); + return res.status(200).json({ posts: [], totalPages: 0, totalItems: 0 }) } } - let likeFilter = {}; - if (decodedToken) likeFilter.userId = decodedToken.id; + const likeFilter = {} + if (decodedToken) likeFilter.userId = decodedToken.id const likes = decodedToken ? await LikeModel.find({ userId: decodedToken.id, postId: { $in: posts.map(p => p._id) } }).lean() - : []; + : [] - const likesMap = {}; - likes.forEach(l => { likesMap[l.postId.toString()] = true; }); + const likesMap = {} + likes.forEach(l => { likesMap[l.postId.toString()] = true }) const comments = await CommentModel.aggregate([ { $match: { post: { $in: posts.map(p => p._id) }, status: commentStatusFilter() } }, - { $group: { _id: "$post", count: { $sum: 1 } } } - ]); - const commentsMap = {}; - comments.forEach(c => { commentsMap[c._id.toString()] = c.count; }); + { $group: { _id: '$post', count: { $sum: 1 } } } + ]) + const commentsMap = {} + comments.forEach(c => { commentsMap[c._id.toString()] = c.count }) posts = posts.map(post => { - const user = users.find(u => u._id.toString() === post.user_id.toString()); + const user = users.find(u => u._id.toString() === post.user_id.toString()) return { ...post, - user_name: user?.user_name || "", - first_name: user?.first_name || "", - last_name: user?.last_name || "", - expertise: user?.expertise || "", + user_name: user?.user_name || '', + first_name: user?.first_name || '', + last_name: user?.last_name || '', + expertise: user?.expertise || '', sub_expertise: user?.sub_expertise || [], // شهر/استان خودِ نویسنده (پروفایل) — بدون تغییر نسبت به قبل province: user?.show_location ? user?.province || {} : {}, @@ -1520,53 +1515,53 @@ const getPostsWeb = async (req, res, next) => { post_province: post.province || null, post_city: post.city || null, show_location: user?.show_location, - user_level: user?.user_level || "", - profile_image: user?.profile_image || "", - is_verified: user?.is_verified || "", - verify_badge: user?.verify_badge || "", + user_level: user?.user_level || '', + profile_image: user?.profile_image || '', + is_verified: user?.is_verified || '', + verify_badge: user?.verify_badge || '', user_score: user?.user_score || 0, rate: user?.rate || 0, likesCount: post.likes ? post.likes.length : 0, is_liked: !!likesMap[post._id.toString()], - commentsCount: commentsMap[post._id.toString()] || 0, - }; - }); + commentsCount: commentsMap[post._id.toString()] || 0 + } + }) - posts = await attachPostEngagementFields(posts, decodedToken?.id); + posts = await attachPostEngagementFields(posts, decodedToken?.id) // در پروفایل کاربر: پست‌های پین‌شده اول (حداکثر ۳) if (_id) { - const pinnedIds = (users[0]?.pinned_post_ids || []).map(String); + const pinnedIds = (users[0]?.pinned_post_ids || []).map(String) if (pinnedIds.length) { - const pinOrder = new Map(pinnedIds.map((id, idx) => [id, idx])); + const pinOrder = new Map(pinnedIds.map((id, idx) => [id, idx])) posts.sort((a, b) => { - const aPin = pinOrder.get(String(a._id)); - const bPin = pinOrder.get(String(b._id)); - if (aPin !== undefined && bPin !== undefined) return aPin - bPin; - if (aPin !== undefined) return -1; - if (bPin !== undefined) return 1; - return new Date(b.createdAt) - new Date(a.createdAt); - }); + const aPin = pinOrder.get(String(a._id)) + const bPin = pinOrder.get(String(b._id)) + if (aPin !== undefined && bPin !== undefined) return aPin - bPin + if (aPin !== undefined) return -1 + if (bPin !== undefined) return 1 + return new Date(b.createdAt) - new Date(a.createdAt) + }) posts = posts.map((p) => ({ ...p, - is_pinned: pinOrder.has(String(p._id)), - })); + is_pinned: pinOrder.has(String(p._id)) + })) } } if (rateFilter) { posts.sort((a, b) => { - if (rateFilter === 'کمترین') return (a.rate || 0) - (b.rate || 0); - if (rateFilter === 'بیشترین') return (b.rate || 0) - (a.rate || 0); - return 0; - }); + if (rateFilter === 'کمترین') return (a.rate || 0) - (b.rate || 0) + if (rateFilter === 'بیشترین') return (b.rate || 0) - (a.rate || 0) + return 0 + }) } const usersById = Object.fromEntries( users.map((u) => [String(u._id), u]) - ); + ) - if (feedMode === "reels" && !_id && posts.length > 1) { + if (feedMode === 'reels' && !_id && posts.length > 1) { const { posts: paginatedPosts, totalItems, isColdStart } = await paginatePersonalizedExplore({ posts, @@ -1577,18 +1572,18 @@ const getPostsWeb = async (req, res, next) => { seedPostId, reelsMode: true, blendFresh: false, - cacheScope: "reels", - }); + cacheScope: 'reels' + }) return res.status(200).json({ posts: paginatedPosts, totalPages: Math.ceil(totalItems / limit) || 1, totalItems, - feedMeta: { isColdStart: !!isColdStart }, - }); + feedMeta: { isColdStart: !!isColdStart } + }) } - if (feedMode === "reels" && _id && seedPostId && posts.length > 1) { + if (feedMode === 'reels' && _id && seedPostId && posts.length > 1) { const { posts: paginatedPosts, totalItems, isColdStart } = await paginatePersonalizedExplore({ posts, @@ -1599,15 +1594,15 @@ const getPostsWeb = async (req, res, next) => { seedPostId, reelsMode: true, blendFresh: false, - cacheScope: `reels-profile:${_id}`, - }); + cacheScope: `reels-profile:${_id}` + }) return res.status(200).json({ posts: paginatedPosts, totalPages: Math.ceil(totalItems / limit) || 1, totalItems, - feedMeta: { isColdStart: !!isColdStart }, - }); + feedMeta: { isColdStart: !!isColdStart } + }) } // پروفایل یک کاربر خاص (_id)، مرتب‌سازی صریح بر اساس امتیاز (rateFilter)، یا درخواست @@ -1618,7 +1613,7 @@ const getPostsWeb = async (req, res, next) => { // صفحه فقط پست‌های تکراریِ کاربرهای قبلی برمی‌گرده و از دید کاربر لیست انگار همون‌جا // متوقف می‌شه) — فقط فید فیلترشده‌ی معمولی (مثلاً فیلتر تخصص/شهر توی خانه) شخصی‌سازی // می‌شه، تا «چیزهایی که کاربر زیاد می‌بینه» همون‌جا هم اثر بذاره - if (!_id && !rateFilter && feedMode !== "list" && posts.length > 1) { + if (!_id && !rateFilter && feedMode !== 'list' && posts.length > 1) { const { posts: paginatedPosts, totalItems, isColdStart } = await paginatePersonalizedExplore({ posts, @@ -1628,134 +1623,133 @@ const getPostsWeb = async (req, res, next) => { userId: decodedToken?.id, reelsMode: false, blendFresh: true, - cacheScope: "home", - }); + cacheScope: 'home' + }) return res.status(200).json({ posts: paginatedPosts, totalPages: Math.ceil(totalItems / limit) || 1, totalItems, - feedMeta: { isColdStart: !!isColdStart }, - }); + feedMeta: { isColdStart: !!isColdStart } + }) } - const startIndex = (page - 1) * limit; - const endIndex = page * limit; - const totalItems = posts.length; - const paginatedPosts = posts.slice(startIndex, endIndex); + const startIndex = (page - 1) * limit + const endIndex = page * limit + const totalItems = posts.length + const paginatedPosts = posts.slice(startIndex, endIndex) return res.status(200).json({ posts: paginatedPosts, totalPages: Math.ceil(totalItems / limit), - totalItems, - }); - + totalItems + }) } catch (error) { - console.error("Error in getPostsWeb:", error.message, error.stack); - return res.status(500).json({ message: "خطای سرور: " + error.message }); + console.error('Error in getPostsWeb:', error.message, error.stack) + return res.status(500).json({ message: 'خطای سرور: ' + error.message }) } -}; +} // همون کلاس کاراکترِ تشخیص هشتگ که فرانت برای چیدن chip های قابل‌لمس روی کپشن استفاده // می‌کنه (captionChips.ts: CAPTION_TOKEN_RE) — تا هشتگ‌های استخراج‌شده اینجا دقیقاً همون // چیزی باشن که کاربر روی کپشن می‌بینه و لمس می‌کنه -const HASHTAG_EXTRACT_RE = /#([\p{L}\d_]+)/gu; +const HASHTAG_EXTRACT_RE = /#([\p{L}\d_]+)/gu // ۱۰ هشتگ برتر بر اساس مجموع بازدید پست‌هایی که توی ۳۰ روز اخیر بازدید داشتن (نه لزوماً // پست‌های ۳۰ روز اخیر — «پربازدید ۳۰ روز جاری» یعنی بازدیدهایی که در این بازه ثبت شدن). // هر بازدید یک پست، به تمام هشتگ‌های یکتای همون کپشن اضافه می‌شه (نه فقط اولی) const getTopHashtags = async (req, res, next) => { try { - const windowDays = 30; - const limit = Math.min(parseInt(req.query.limit, 10) || 10, 20); - const since = new Date(Date.now() - windowDays * 24 * 60 * 60 * 1000); + const windowDays = 30 + const limit = Math.min(parseInt(req.query.limit, 10) || 10, 20) + const since = new Date(Date.now() - windowDays * 24 * 60 * 60 * 1000) const viewAgg = await ExploreInteractionModel.aggregate([ { $match: { - targetType: "post", - action: "view", - createdAt: { $gte: since }, - }, + targetType: 'post', + action: 'view', + createdAt: { $gte: since } + } }, - { $group: { _id: "$targetId", count: { $sum: 1 } } }, - ]); - if (!viewAgg.length) return res.status(200).json({ hashtags: [] }); + { $group: { _id: '$targetId', count: { $sum: 1 } } } + ]) + if (!viewAgg.length) return res.status(200).json({ hashtags: [] }) - const viewsMap = new Map(viewAgg.map((v) => [String(v._id), v.count])); + const viewsMap = new Map(viewAgg.map((v) => [String(v._id), v.count])) const posts = await PostModel.find({ _id: { $in: viewAgg.map((v) => v._id) }, status: postStatusFilter(), is_archived: { $ne: true }, - caption: /#/, + caption: /#/ }) - .select("_id caption") - .lean(); + .select('_id caption') + .lean() - const tagViews = new Map(); + const tagViews = new Map() posts.forEach((post) => { - const viewCount = viewsMap.get(String(post._id)) || 0; - if (!viewCount || !post.caption) return; - const seenTags = new Set(); - const re = new RegExp(HASHTAG_EXTRACT_RE.source, HASHTAG_EXTRACT_RE.flags); - let match; + const viewCount = viewsMap.get(String(post._id)) || 0 + if (!viewCount || !post.caption) return + const seenTags = new Set() + const re = new RegExp(HASHTAG_EXTRACT_RE.source, HASHTAG_EXTRACT_RE.flags) + let match while ((match = re.exec(post.caption)) !== null) { - const tag = match[1]; - if (seenTags.has(tag)) continue; - seenTags.add(tag); - tagViews.set(tag, (tagViews.get(tag) || 0) + viewCount); + const tag = match[1] + if (seenTags.has(tag)) continue + seenTags.add(tag) + tagViews.set(tag, (tagViews.get(tag) || 0) + viewCount) } - }); + }) const hashtags = [...tagViews.entries()] .map(([tag, viewsCount]) => ({ tag, viewsCount })) .sort((a, b) => b.viewsCount - a.viewsCount) - .slice(0, limit); + .slice(0, limit) - return res.status(200).json({ hashtags }); + return res.status(200).json({ hashtags }) } catch (error) { - console.error("Error in getTopHashtags:", error.message, error.stack); - return res.status(500).json({ message: "خطای سرور: " + error.message }); + console.error('Error in getTopHashtags:', error.message, error.stack) + return res.status(500).json({ message: 'خطای سرور: ' + error.message }) } -}; +} const getSingleUser = async (req, res, next) => { try { - const token = req.header("Authorization").split(" ")[1]; - if (!token) return res.status(401).send("Access Denied"); - const user_id = req.query.user_id; + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + const user_id = req.query.user_id // eslint-disable-next-line no-unused-vars - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - const userReqId = decodedToken.id; + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userReqId = decodedToken.id if (!mongoose.Types.ObjectId.isValid(user_id)) { - return res.status(400).json({ message: "شناسه کاربر معتبر نیست" }); + return res.status(400).json({ message: 'شناسه کاربر معتبر نیست' }) } - const user = await UserModel.findById(user_id); + const user = await UserModel.findById(user_id) if (!user) { - return res.status(404).json({ message: "کاربر مورد نظر یافت نشد" }); + return res.status(404).json({ message: 'کاربر مورد نظر یافت نشد' }) } - const shamsiDate = moment(user.created_at).format("jYYYY-jMM-jDD"); - const lastOnline = formatLastOnlineStatus(user.last_online, user.ghost_mode); + const shamsiDate = moment(user.created_at).format('jYYYY-jMM-jDD') + const lastOnline = formatLastOnlineStatus(user.last_online, user.ghost_mode) // بررسی بلاک شدن - const blockedByCurrentUser = viewerBlockedUser(user, userReqId); - const blockedYou = userBlockedViewer(user, userReqId); + const blockedByCurrentUser = viewerBlockedUser(user, userReqId) + const blockedYou = userBlockedViewer(user, userReqId) if (blockedYou) { - return res.status(200).json({ user: blockedProfilePayload(user) }); + return res.status(200).json({ user: blockedProfilePayload(user) }) } // Allow chat const offerExists = await OfferModel.exists({ $or: [ { sender: userReqId, receiver: user_id }, - { sender: user_id, receiver: userReqId }, - ], - }); + { sender: user_id, receiver: userReqId } + ] + }) const response = { - is_self: user_id.toString() === userReqId.toString() ? "true" : "false", + is_self: user_id.toString() === userReqId.toString() ? 'true' : 'false', _id: user._id, last_online: lastOnline, created_at: shamsiDate, @@ -1785,122 +1779,122 @@ const getSingleUser = async (req, res, next) => { is_blocked: blockedByCurrentUser, blocked_you: blockedYou, offer_status: !!offerExists, - monthly_free_offer: user.monthly_free_offer, + monthly_free_offer: user.monthly_free_offer, services: user.services, expertise: user.expertise, - sub_expertise: user.sub_expertise, - }; - return res.status(200).json({ user: response }); + sub_expertise: user.sub_expertise + } + return res.status(200).json({ user: response }) } catch (error) { - next(error); + next(error) } -}; +} const getSingleUserWeb = async (req, res, next) => { try { - let userReqId = null; + let userReqId = null // دریافت توکن و بررسی معتبر بودنش (اگر وجود داشت) - const authHeader = req.header("Authorization"); + const authHeader = req.header('Authorization') if (authHeader) { - const token = authHeader.split(" ")[1]; + const token = authHeader.split(' ')[1] try { - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - userReqId = decodedToken.id; + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + userReqId = decodedToken.id } catch (err) { - console.warn("توکن نامعتبر است، ادامه بدون احراز هویت"); // فقط هشدار، نه ارور + console.warn('توکن نامعتبر است، ادامه بدون احراز هویت') // فقط هشدار، نه ارور } } - let user; - const { user_id, user_name } = req.query; + let user + const { user_id, user_name } = req.query if (user_id) { if (!mongoose.Types.ObjectId.isValid(user_id)) { - return res.status(400).json({ message: "شناسه کاربر معتبر نیست" }); + return res.status(400).json({ message: 'شناسه کاربر معتبر نیست' }) } - user = await UserModel.findById(user_id); + user = await UserModel.findById(user_id) } else if (user_name) { - user = await UserModel.findOne({ user_name }); + user = await UserModel.findOne({ user_name }) } else { return res .status(400) - .json({ message: "لطفاً user_id یا user_name ارسال کنید" }); + .json({ message: 'لطفاً user_id یا user_name ارسال کنید' }) } if (!user) { - return res.status(404).json({ message: "کاربر مورد نظر یافت نشد" }); + return res.status(404).json({ message: 'کاربر مورد نظر یافت نشد' }) } if ( - user.account_status === "deactivated" && + user.account_status === 'deactivated' && String(user._id) !== String(userReqId) ) { - return res.status(404).json({ message: "این حساب کاربری غیرفعال است" }); + return res.status(404).json({ message: 'این حساب کاربری غیرفعال است' }) } - const shamsiDate = moment(user.created_at).format("jYYYY-jMM-jDD"); - const lastOnline = formatLastOnlineStatus(user.last_online, user.ghost_mode); + const shamsiDate = moment(user.created_at).format('jYYYY-jMM-jDD') + const lastOnline = formatLastOnlineStatus(user.last_online, user.ghost_mode) // بررسی بلاک شدن (فقط اگر توکن معتبر داشتیم) - let blockedByCurrentUser = false; - let blockedYou = false; - let offerExists = false; - let isFollowingUser = false; + let blockedByCurrentUser = false + let blockedYou = false + let offerExists = false + let isFollowingUser = false if (userReqId) { - blockedByCurrentUser = viewerBlockedUser(user, userReqId); - blockedYou = userBlockedViewer(user, userReqId); + blockedByCurrentUser = viewerBlockedUser(user, userReqId) + blockedYou = userBlockedViewer(user, userReqId) offerExists = await OfferModel.exists({ $or: [ { sender: userReqId, receiver: user._id }, - { sender: user._id, receiver: userReqId }, - ], - }); + { sender: user._id, receiver: userReqId } + ] + }) if ( user._id.toString() !== userReqId.toString() && !blockedYou ) { isFollowingUser = !!(await UserFollowModel.exists({ followerId: userReqId, - followingId: user._id, - })); + followingId: user._id + })) } } if (blockedYou && userReqId) { - return res.status(200).json({ user: blockedProfilePayload(user) }); + return res.status(200).json({ user: blockedProfilePayload(user) }) } // دریافت تعداد پست‌ها و پروژه‌ها const postsCount = await PostModel.countDocuments({ user_id: user._id, - status: { $in: ["accept", "temp_accept"] }, - }); - const { followersCount, followingCount } = await getFollowCounts(user._id); + status: { $in: ['accept', 'temp_accept'] } + }) + const { followersCount, followingCount } = await getFollowCounts(user._id) const allProjectsCount = await ProjectModel.countDocuments({ - selected_user: user._id, - }); + selected_user: user._id + }) const successfulProjectsCount = await ProjectModel.countDocuments({ selected_user: user._id, - status: "done", - }); + status: 'done' + }) const isSelf = userReqId ? user._id.toString() === userReqId.toString() - : false; + : false const privateContentLocked = - Boolean(user.is_private) && !isSelf && !isFollowingUser; + Boolean(user.is_private) && !isSelf && !isFollowingUser const hasActiveBooking = await BookingConfigModel.exists({ owner: user._id, - status: "active", - }); + status: 'active' + }) const response = { has_active_booking: !!hasActiveBooking, is_self: userReqId ? user._id.toString() === userReqId.toString() - : "false", + : 'false', _id: user._id, last_online: lastOnline, created_at: shamsiDate, @@ -1928,16 +1922,16 @@ const getSingleUserWeb = async (req, res, next) => { // برای بقیه نمایش داده می‌شه — برای خودِ صاحب پروفایل صرف‌نظر از این تیک همیشه برمی‌گرده ...(user.personal_details_public === true || isSelf ? { - marital_status: user.marital_status, - residence_type: user.residence_type, - residence_area: user.residence_area, - vehicle_type: user.vehicle_type, - vehicle_details: user.vehicle_details, - personal_style: user.personal_style, - job: user.job, - income_range: user.income_range, - moral_traits: user.moral_traits, - } + marital_status: user.marital_status, + residence_type: user.residence_type, + residence_area: user.residence_area, + vehicle_type: user.vehicle_type, + vehicle_details: user.vehicle_details, + personal_style: user.personal_style, + job: user.job, + income_range: user.income_range, + moral_traits: user.moral_traits + } : {}), personal_details_public: user.personal_details_public === true, profile_image: user.profile_image, @@ -1957,44 +1951,44 @@ const getSingleUserWeb = async (req, res, next) => { allProjectsCount, successfulProjectsCount, followersCount, - followingCount, - }; + followingCount + } - return res.status(200).json({ user: response }); + return res.status(200).json({ user: response }) } catch (error) { - next(error); + next(error) } -}; +} const getUserContactInfo = async (req, res, next) => { try { - const token = req.header("Authorization").split(" ")[1]; - if (!token) return res.status(401).send("Access Denied"); + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') // eslint-disable-next-line no-unused-vars - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - const user_id = decodedToken.id; + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const user_id = decodedToken.id if (!mongoose.Types.ObjectId.isValid(user_id)) { - return res.status(400).json({ message: "شناسه کاربر معتبر نیست" }); + return res.status(400).json({ message: 'شناسه کاربر معتبر نیست' }) } - const user = await UserModel.findById(user_id); + const user = await UserModel.findById(user_id) if (!user) { - return res.status(404).json({ message: "کاربر مورد نظر یافت نشد" }); + return res.status(404).json({ message: 'کاربر مورد نظر یافت نشد' }) } // اطلاعات تماس در UserModel ذخیره نمی‌شه — همون چیزیه که createAdvertising روی // AdvertisingProfileModel می‌نویسه (وقتی saveInfoForNextAds فعال باشه) - const advertisingProfile = await AdvertisingProfileModel.findOne({ user: user_id }); + const advertisingProfile = await AdvertisingProfileModel.findOne({ user: user_id }) const response = { contactInfo: advertisingProfile?.contactInfo || null, - _id: user._id, - }; - return res.status(200).json({ data: response }); + _id: user._id + } + return res.status(200).json({ data: response }) } catch (error) { - next(error); + next(error) } -}; +} const { applyAutoBlockSuspension, clearAutoBlockSuspension @@ -2002,96 +1996,96 @@ const { const blockUser = async (req, res) => { try { - const { user_to_block } = 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 userReqId = decodedToken.id; + const { user_to_block } = 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 userReqId = decodedToken.id if (userReqId === user_to_block) { - return res.status(400).json({ message: "امکان بلاک خودتان وجود ندارد" }); + return res.status(400).json({ message: 'امکان بلاک خودتان وجود ندارد' }) } - const currentUser = await UserModel.findById(userReqId); - const userToBlock = await UserModel.findById(user_to_block); + const currentUser = await UserModel.findById(userReqId) + const userToBlock = await UserModel.findById(user_to_block) if (!currentUser || !userToBlock) { - return res.status(404).json({ message: "کاربر یافت نشد" }); + return res.status(404).json({ message: 'کاربر یافت نشد' }) } const alreadyBlocked = userToBlock.blocked_by.some((id) => id.equals(currentUser._id) - ); + ) if (alreadyBlocked) { - return res.status(400).json({ message: "این کاربر قبلاً بلاک شده است" }); + return res.status(400).json({ message: 'این کاربر قبلاً بلاک شده است' }) } - userToBlock.blocked_by.push(currentUser._id); - currentUser.blocked_users.push(userToBlock._id); + userToBlock.blocked_by.push(currentUser._id) + currentUser.blocked_users.push(userToBlock._id) - applyAutoBlockSuspension(userToBlock); + applyAutoBlockSuspension(userToBlock) - await currentUser.save(); - await userToBlock.save(); + await currentUser.save() + await userToBlock.save() - res.status(200).json({ message: "کاربر با موفقیت بلاک شد" }); + res.status(200).json({ message: 'کاربر با موفقیت بلاک شد' }) } catch (error) { - console.error("Error blocking user:", error); - res.status(500).json({ error: "Server error" }); + console.error('Error blocking user:', error) + res.status(500).json({ error: 'Server error' }) } -}; +} const unblockUser = async (req, res) => { try { - const { user_to_unblock } = 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 userReqId = decodedToken.id; + const { user_to_unblock } = 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 userReqId = decodedToken.id - const currentUser = await UserModel.findById(userReqId); - const userToUnblock = await UserModel.findById(user_to_unblock); + const currentUser = await UserModel.findById(userReqId) + const userToUnblock = await UserModel.findById(user_to_unblock) if (!currentUser || !userToUnblock) { - return res.status(404).json({ message: "کاربر یافت نشد" }); + return res.status(404).json({ message: 'کاربر یافت نشد' }) } - currentUser.blocked_users.pull(userToUnblock._id); - userToUnblock.blocked_by.pull(currentUser._id); + currentUser.blocked_users.pull(userToUnblock._id) + userToUnblock.blocked_by.pull(currentUser._id) - clearAutoBlockSuspension(userToUnblock); + clearAutoBlockSuspension(userToUnblock) - await currentUser.save(); - await userToUnblock.save(); + await currentUser.save() + await userToUnblock.save() - res.status(200).json({ message: "کاربر با موفقیت آنبلاک شد" }); + res.status(200).json({ message: 'کاربر با موفقیت آنبلاک شد' }) } catch (error) { - console.error("Error unblocking user:", error); - res.status(500).json({ error: "Server error" }); + console.error('Error unblocking user:', error) + res.status(500).json({ error: 'Server error' }) } -}; +} const getComments = async (req, res, next) => { try { - const token = req.header("Authorization").split(" ")[1]; - if (!token) return res.status(401).send("Access Denied"); - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - const user_id = req.query.user_id; - const post_id = req.query.post_id; + 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 user_id = req.query.user_id + const post_id = req.query.post_id if (!mongoose.Types.ObjectId.isValid(user_id)) { - return res.status(400).json({ message: "شناسه کاربر معتبر نیست" }); + return res.status(400).json({ message: 'شناسه کاربر معتبر نیست' }) } - const user = await UserModel.findById(user_id); - const { page = 1, limit = 10 } = req.query; + const user = await UserModel.findById(user_id) + const { page = 1, limit = 10 } = req.query if (!user) { - return res.status(404).json({ message: "کاربر مورد نظر یافت نشد" }); + return res.status(404).json({ message: 'کاربر مورد نظر یافت نشد' }) } - const filter = { user: user_id, status: commentStatusFilter(), parent: null }; + const filter = { user: user_id, status: commentStatusFilter(), parent: null } if (post_id && mongoose.Types.ObjectId.isValid(post_id)) { - filter.post = post_id; + filter.post = post_id } const options = { @@ -2099,165 +2093,167 @@ const getComments = async (req, res, next) => { limit: parseInt(limit), sort: { createdAt: -1 }, populate: [ - { path: "creator", select: "user_name profile_image is_verified verify_badge" }, - { path: "project", select: "project_name" }, - ], - }; + { path: 'creator', select: 'user_name profile_image is_verified verify_badge' }, + { path: 'project', select: 'project_name' } + ] + } - const comments = await CommentModel.paginate(filter, options); + const comments = await CommentModel.paginate(filter, options) - const topIds = comments.docs.map((c) => c._id); + const topIds = comments.docs.map((c) => c._id) const repliesRaw = topIds.length ? await CommentModel.find({ - parent: { $in: topIds }, - status: commentStatusFilter(), - }) - .sort({ createdAt: 1 }) - .populate("creator", "user_name profile_image is_verified verify_badge") - .lean() - : []; + parent: { $in: topIds }, + status: commentStatusFilter() + }) + .sort({ createdAt: 1 }) + .populate('creator', 'user_name profile_image is_verified verify_badge') + .lean() + : [] const repliesByParent = repliesRaw.reduce((acc, reply) => { - const key = String(reply.parent); - if (!acc[key]) acc[key] = []; - acc[key].push(reply); - return acc; - }, {}); + const key = String(reply.parent) + if (!acc[key]) acc[key] = [] + acc[key].push(reply) + return acc + }, {}) const earliestRatedComments = await CommentModel.find({ user: user_id, - rating: { $ne: null, $gt: 0 }, + rating: { $ne: null, $gt: 0 } }) - .select("creator _id createdAt") - .sort({ createdAt: 1 }); + .select('creator _id createdAt') + .sort({ createdAt: 1 }) - const ratedCreatorCommentIds = new Map(); + const ratedCreatorCommentIds = new Map() for (const ratedComment of earliestRatedComments) { - const creatorId = ratedComment.creator.toString(); + const creatorId = ratedComment.creator.toString() if (!ratedCreatorCommentIds.has(creatorId)) { - ratedCreatorCommentIds.set(creatorId, ratedComment._id.toString()); + ratedCreatorCommentIds.set(creatorId, ratedComment._id.toString()) } } const formatOne = (comment, includeRating = true) => { - const creatorId = comment.creator._id.toString(); + const creatorId = comment.creator._id.toString() const isPrimaryRatingComment = includeRating && comment.rating != null && comment.rating > 0 && - ratedCreatorCommentIds.get(creatorId) === comment._id.toString(); + ratedCreatorCommentIds.get(creatorId) === comment._id.toString() return { _id: comment._id, comment: comment.comment, rating: isPrimaryRatingComment ? comment.rating : null, - createdAt: moment(comment.createdAt).format("jYYYY-jMM-jDD HH:mm"), + createdAt: moment(comment.createdAt).format('jYYYY-jMM-jDD HH:mm'), status: comment.status, user: { profile_image: comment.creator.profile_image, user_name: comment.creator.user_name, is_verified: comment.creator.is_verified, verify_badge: comment.creator.verify_badge, - _id: comment.creator._id, + _id: comment.creator._id }, project: comment.project ? comment.project.project_name : null, post_id: comment.post ? comment.post.toString() : null, - parent_id: comment.parent ? String(comment.parent) : null, - }; - }; + parent_id: comment.parent ? String(comment.parent) : null + } + } - const allForLikes = []; + const allForLikes = [] const formattedComments = comments.docs.map((comment) => { - const row = formatOne(comment); + const row = formatOne(comment) const childReplies = (repliesByParent[String(comment._id)] || []).map( (reply) => formatOne({ ...reply, project: null }, false) - ); - allForLikes.push(row, ...childReplies); - return { ...row, replies: childReplies }; - }); + ) + allForLikes.push(row, ...childReplies) + return { ...row, replies: childReplies } + }) const withLikes = await attachCommentLikeFields( allForLikes, decodedToken.id - ); + ) const likesMap = Object.fromEntries( withLikes.map((c) => [String(c._id), c]) - ); + ) const commentsWithMeta = formattedComments.map((c) => ({ ...likesMap[String(c._id)], - replies: (c.replies || []).map((r) => likesMap[String(r._id)] || r), - })); + replies: (c.replies || []).map((r) => likesMap[String(r._id)] || r) + })) - const hasRated = await hasCreatorRatedUser(decodedToken.id, user_id); + const hasRated = await hasCreatorRatedUser(decodedToken.id, user_id) res.status(200).json({ comments: commentsWithMeta, totalPages: comments.totalPages, totalItems: comments.totalDocs, currentPage: comments.page, - has_rated: hasRated, - }); + has_rated: hasRated + }) } catch (error) { - next(error); + next(error) } -}; +} const createUserComment = async (req, res, next) => { try { - const authHeader = req.header("Authorization"); - if (!authHeader) return res.status(401).send("Access Denied"); + const authHeader = req.header('Authorization') + if (!authHeader) return res.status(401).send('Access Denied') - const token = authHeader.split(" ")[1]; - if (!token) return res.status(401).send("Access Denied"); + const token = authHeader.split(' ')[1] + if (!token) return res.status(401).send('Access Denied') - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - const creatorId = decodedToken.id; + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const creatorId = decodedToken.id - const { user_id, comment, rate, post_id, parent_id } = req.body; + const { user_id, comment, rate, post_id, parent_id } = req.body if (!user_id || !comment) { - return res.status(400).send({ message: "All fields are required" }); + return res.status(400).send({ message: 'All fields are required' }) } - let targetUserId = user_id; - let targetPostId = post_id; - let commentFor = post_id ? "post" : "user"; - let parentRef = null; + let targetUserId = user_id + let targetPostId = post_id + let commentFor = post_id ? 'post' : 'user' + let parentRef = null if (parent_id) { if (!mongoose.Types.ObjectId.isValid(parent_id)) { - return res.status(400).json({ message: "شناسه کامنت والد نامعتبر است" }); + return res.status(400).json({ message: 'شناسه کامنت والد نامعتبر است' }) } - const parentComment = await CommentModel.findById(parent_id); + const parentComment = await CommentModel.findById(parent_id) if (!parentComment || !isPublicCommentStatus(parentComment.status)) { - return res.status(404).json({ message: "کامنت والد یافت نشد" }); + return res.status(404).json({ message: 'کامنت والد یافت نشد' }) } - parentRef = parentComment._id; - targetUserId = String(parentComment.user); - targetPostId = parentComment.post ? String(parentComment.post) : undefined; - commentFor = parentComment.comment_for; + parentRef = parentComment._id + targetUserId = String(parentComment.user) + targetPostId = parentComment.post ? String(parentComment.post) : undefined + commentFor = parentComment.comment_for } const { ratingValue, isNewRating, requiresRating, alreadyRated } = parent_id ? { ratingValue: null, isNewRating: false, requiresRating: false, alreadyRated: false } : await resolveRatingForComment({ - creatorId, - targetUserId, - rate, - }); + creatorId, + targetUserId, + rate + }) if (alreadyRated) { return res.status(400).json({ - message: "شما قبلاً به این کاربر امتیاز داده‌اید.", - }); + message: 'شما قبلاً به این کاربر امتیاز داده‌اید.' + }) } if (requiresRating) { return res .status(400) - .send({ message: "Rating must be between 1 and 5" }); + .send({ message: 'Rating must be between 1 and 5' }) } + const creatorUser = await UserModel.findById(creatorId).select('verify_badge').lean() + const newComment = await saveCommentWithModerationStatus( CommentModel, { @@ -2267,36 +2263,37 @@ const createUserComment = async (req, res, next) => { comment, comment_for: commentFor, post: targetPostId || undefined, - parent: parentRef || undefined, + parent: parentRef || undefined }, - "comment" - ); + 'comment', + creatorUser?.verify_badge + ) if (isNewRating) { - await recalculateUserRating(targetUserId); + await recalculateUserRating(targetUserId) } - let ownerId = targetUserId; + let ownerId = targetUserId if (targetPostId && mongoose.Types.ObjectId.isValid(targetPostId)) { - const post = await PostModel.findById(targetPostId).select("user_id").lean(); + const post = await PostModel.findById(targetPostId).select('user_id').lean() if (post?.user_id) { - ownerId = String(post.user_id); + ownerId = String(post.user_id) } } const commentType = targetPostId - ? "post_comment" + ? 'post_comment' : parent_id - ? "post_comment" - : "profile_comment"; + ? 'post_comment' + : 'profile_comment' await createCommentNotification({ io: req.app.get('io'), ownerId, commenterId: creatorId, entityId: targetPostId || targetUserId, - type: commentType, - }); + type: commentType + }) if (isNewRating && ratingValue > 0) { await createRatingNotification({ @@ -2304,209 +2301,209 @@ const createUserComment = async (req, res, next) => { ownerId, raterId: creatorId, entityId: targetPostId || targetUserId, - type: targetPostId ? "post_rating" : "profile_rating", - }); + type: targetPostId ? 'post_rating' : 'profile_rating' + }) } res.status(201).json({ message: - newComment.status === "accepted" || newComment.status === "temp_accept" - ? "نظر با موفقیت ثبت شد." - : "نظر با موفقیت ثبت شد و بعد از تایید، منتشر میشود.", - comment: newComment, - }); + newComment.status === 'accepted' || newComment.status === 'temp_accept' + ? 'نظر با موفقیت ثبت شد.' + : 'نظر با موفقیت ثبت شد و بعد از تایید، منتشر میشود.', + comment: newComment + }) } catch (error) { - if (error?.name === "ValidationError") { - console.error("createUserComment validation:", error.message); + if (error?.name === 'ValidationError') { + console.error('createUserComment validation:', error.message) return res.status(500).json({ - message: "خطا در ثبت نظر. مدل کامنت روی سرور به‌روز نیست.", - }); + message: 'خطا در ثبت نظر. مدل کامنت روی سرور به‌روز نیست.' + }) } - next(error); + next(error) } -}; +} const toggleCommentLike = async (req, res, next) => { try { - const token = req.header("Authorization")?.split(" ")[1]; - if (!token) return res.status(401).send("Access Denied"); + 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 { commentId } = req.body; + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userId = decodedToken.id + const { commentId } = req.body if (!commentId || !mongoose.Types.ObjectId.isValid(commentId)) { - return res.status(400).json({ message: "شناسه کامنت نامعتبر است" }); + return res.status(400).json({ message: 'شناسه کامنت نامعتبر است' }) } - const comment = await CommentModel.findById(commentId).select("_id status"); + const comment = await CommentModel.findById(commentId).select('_id status') if (!comment || !isPublicCommentStatus(comment.status)) { - return res.status(404).json({ message: "کامنت یافت نشد" }); + return res.status(404).json({ message: 'کامنت یافت نشد' }) } - const existing = await CommentLikeModel.findOne({ commentId, userId }); + const existing = await CommentLikeModel.findOne({ commentId, userId }) if (existing) { - await CommentLikeModel.deleteOne({ _id: existing._id }); - const likesCount = await CommentLikeModel.countDocuments({ commentId }); - return res.status(200).json({ is_liked: false, likesCount }); + await CommentLikeModel.deleteOne({ _id: existing._id }) + const likesCount = await CommentLikeModel.countDocuments({ commentId }) + return res.status(200).json({ is_liked: false, likesCount }) } - await CommentLikeModel.create({ commentId, userId }); - const likesCount = await CommentLikeModel.countDocuments({ commentId }); - return res.status(200).json({ is_liked: true, likesCount }); + await CommentLikeModel.create({ commentId, userId }) + const likesCount = await CommentLikeModel.countDocuments({ commentId }) + return res.status(200).json({ is_liked: true, likesCount }) } catch (error) { - next(error); + next(error) } -}; +} const inviteEmployer = async (req, res, next) => { - const token = req.header("Authorization").split(" ")[1]; - if (!token) return res.status(401).send("Access Denied"); - const user_id = req.query.user_id; + const token = req.header('Authorization').split(' ')[1] + if (!token) return res.status(401).send('Access Denied') + const user_id = req.query.user_id // eslint-disable-next-line no-unused-vars - const decodedToken = jwt.verify(token, process.env.APP_SECRET); - const userReqId = decodedToken.id; + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const userReqId = decodedToken.id if (!mongoose.Types.ObjectId.isValid(user_id)) { - return res.status(400).json({ message: "شناسه کاربر معتبر نیست" }); + return res.status(400).json({ message: 'شناسه کاربر معتبر نیست' }) } - const user = await UserModel.findById(user_id); - const creatorUser = await UserModel.findById(userReqId); + const user = await UserModel.findById(user_id) + const creatorUser = await UserModel.findById(userReqId) if (!user) { - return res.status(404).json({ message: "کاربر مورد نظر یافت نشد" }); + return res.status(404).json({ message: 'کاربر مورد نظر یافت نشد' }) } - const today = new Date(); - today.setHours(0, 0, 0, 0); + const today = new Date() + today.setHours(0, 0, 0, 0) const existingNotification = await NotificationModel.findOne({ user_id: user._id, project_post_id: userReqId, - type: "invite", - createdAt: { $gte: today }, - }); + type: 'invite', + createdAt: { $gte: today } + }) if (existingNotification) { return res .status(400) .json({ - message: "شما امروز برای این کارفرما درخواست همکاری ارسال کرده اید.", - type: "today", - }); + message: 'شما امروز برای این کارفرما درخواست همکاری ارسال کرده اید.', + type: 'today' + }) } const notification = new NotificationModel({ user_id: user._id, project_post_id: userReqId, - type: "invite", - title: "دعوت به همکاری", - description: `کاربر ${creatorUser.user_name} شما را برای همکاری دعوت کرده است.`, - }); - await notification.save(); + type: 'invite', + title: 'دعوت به همکاری', + description: `کاربر ${creatorUser.user_name} شما را برای همکاری دعوت کرده است.` + }) + await notification.save() - res.status(201).json({ message: "درخواست همکاری با موفقیت ثبت شد" }); -}; + res.status(201).json({ message: 'درخواست همکاری با موفقیت ثبت شد' }) +} const recordExploreInteraction = async (req, res, next) => { try { - const token = req.header("Authorization")?.split(" ")[1]; - if (!token) return res.status(401).send("Access Denied"); + 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 { targetType, targetId, authorId, contentType, action, dwellMs } = req.body; + const decodedToken = jwt.verify(token, process.env.APP_SECRET) + const { targetType, targetId, authorId, contentType, action, dwellMs } = req.body const viewer = await UserModel.findById(decodedToken.id) .select('ghost_mode show_location province city country') - .lean(); + .lean() if (viewer?.ghost_mode && (action || 'view') === 'profile_visit') { - return res.status(201).json({ message: 'ok' }); + return res.status(201).json({ message: 'ok' }) } if (!targetType || !targetId) { - return res.status(422).json({ message: "اطلاعات ناقص است" }); + return res.status(422).json({ message: 'اطلاعات ناقص است' }) } - const normalizedAction = action || "view"; + const normalizedAction = action || 'view' - if (normalizedAction === "view") { + if (normalizedAction === 'view') { const recentView = await ExploreInteractionModel.findOne({ viewerId: decodedToken.id, targetType, targetId, - action: "view", + action: 'view', $or: [ { created_at: { $gte: new Date(Date.now() - 30 * 60 * 1000) } }, - { createdAt: { $gte: new Date(Date.now() - 30 * 60 * 1000) } }, - ], + { createdAt: { $gte: new Date(Date.now() - 30 * 60 * 1000) } } + ] }) - .select("_id") - .lean(); + .select('_id') + .lean() if (recentView) { - return res.status(201).json({ message: "ok" }); + return res.status(201).json({ message: 'ok' }) } } - const shareLocation = Boolean(viewer?.show_location) && Boolean(viewer?.province) && Boolean(viewer?.city); - const shareCountry = Boolean(viewer?.show_location) && Boolean(viewer?.country); + const shareLocation = Boolean(viewer?.show_location) && Boolean(viewer?.province) && Boolean(viewer?.city) + const shareCountry = Boolean(viewer?.show_location) && Boolean(viewer?.country) await ExploreInteractionModel.create({ viewerId: decodedToken.id, targetType, targetId, authorId: authorId || null, - contentType: contentType || "image", + contentType: contentType || 'image', action: normalizedAction, dwellMs: dwellMs ?? null, viewerCountry: shareCountry ? viewer.country : null, viewerProvince: shareLocation ? viewer.province : null, - viewerCity: shareLocation ? viewer.city : null, - }); + viewerCity: shareLocation ? viewer.city : null + }) - return res.status(201).json({ message: "ok" }); + return res.status(201).json({ message: 'ok' }) } catch (error) { - console.error("recordExploreInteraction:", error); - next(error); + console.error('recordExploreInteraction:', error) + next(error) } -}; +} const getPublicProfilesForSeo = async (req, res, next) => { try { - const page = Math.max(1, parseInt(req.query.page, 10) || 1); - const limit = Math.min(500, Math.max(1, parseInt(req.query.limit, 10) || 200)); - const skip = (page - 1) * limit; + const page = Math.max(1, parseInt(req.query.page, 10) || 1) + const limit = Math.min(500, Math.max(1, parseInt(req.query.limit, 10) || 200)) + const skip = (page - 1) * limit const query = { - account_status: { $ne: "deactivated" }, + account_status: { $ne: 'deactivated' }, is_private: { $ne: true }, is_Register: true, - user_name: { $exists: true, $nin: [null, ""] }, - }; + user_name: { $exists: true, $nin: [null, ''] } + } const [users, total] = await Promise.all([ UserModel.find(query) - .select("user_name updated_at created_at") + .select('user_name updated_at created_at') .sort({ updated_at: -1, created_at: -1 }) .skip(skip) .limit(limit) .lean(), - UserModel.countDocuments(query), - ]); + UserModel.countDocuments(query) + ]) return res.status(200).json({ users: users.map((user) => ({ user_name: user.user_name, - updatedAt: user.updated_at || user.created_at, + updatedAt: user.updated_at || user.created_at })), page, limit, total, - hasMore: skip + users.length < total, - }); + hasMore: skip + users.length < total + }) } catch (error) { - next(error); + next(error) } -}; +} module.exports = { getPublicProfilesForSeo, @@ -2528,5 +2525,5 @@ module.exports = { recordExploreInteraction, getRankedExplorePosts, getPredictedTrendPosts, - getTopHashtags, -}; + getTopHashtags +} diff --git a/controllers/application/users/nearbyUsersController.js b/controllers/application/users/nearbyUsersController.js index 3d457ce..7f799e6 100644 --- a/controllers/application/users/nearbyUsersController.js +++ b/controllers/application/users/nearbyUsersController.js @@ -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 } diff --git a/controllers/application/workroom/workroomController.js b/controllers/application/workroom/workroomController.js index b3872b1..ec78490 100644 --- a/controllers/application/workroom/workroomController.js +++ b/controllers/application/workroom/workroomController.js @@ -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' } } diff --git a/controllers/panel/comments/commentController.js b/controllers/panel/comments/commentController.js index c0548e9..a4b2e6a 100644 --- a/controllers/panel/comments/commentController.js +++ b/controllers/panel/comments/commentController.js @@ -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), // تبدیل محدودیت به عدد صحیح diff --git a/controllers/panel/posts/postController.js b/controllers/panel/posts/postController.js index 34175ba..edf6787 100644 --- a/controllers/panel/posts/postController.js +++ b/controllers/panel/posts/postController.js @@ -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), diff --git a/models/AdvertisingModel.js b/models/AdvertisingModel.js index c1ba0c1..c93c047 100644 --- a/models/AdvertisingModel.js +++ b/models/AdvertisingModel.js @@ -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: { diff --git a/models/ProjectModel.js b/models/ProjectModel.js index e52ff03..4e6749b 100644 --- a/models/ProjectModel.js +++ b/models/ProjectModel.js @@ -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) diff --git a/models/RequestModel.js b/models/RequestModel.js index 31ea192..799379b 100644 --- a/models/RequestModel.js +++ b/models/RequestModel.js @@ -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) diff --git a/models/UserModel.js b/models/UserModel.js index c42a0c5..83274e2 100644 --- a/models/UserModel.js +++ b/models/UserModel.js @@ -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, diff --git a/routes/application/advertising/index.js b/routes/application/advertising/index.js index 4db23cc..7e9daa4 100644 --- a/routes/application/advertising/index.js +++ b/routes/application/advertising/index.js @@ -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 diff --git a/routes/application/orders/index.js b/routes/application/orders/index.js index ebf252f..52762e8 100644 --- a/routes/application/orders/index.js +++ b/routes/application/orders/index.js @@ -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) diff --git a/routes/application/projects/index.js b/routes/application/projects/index.js index 64688d4..353e090 100644 --- a/routes/application/projects/index.js +++ b/routes/application/projects/index.js @@ -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 diff --git a/utils/cache.js b/utils/cache.js index b1ff7ee..60b48eb 100644 --- a/utils/cache.js +++ b/utils/cache.js @@ -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) } diff --git a/utils/commentStatusSave.js b/utils/commentStatusSave.js index c13484c..556f697 100644 --- a/utils/commentStatusSave.js +++ b/utils/commentStatusSave.js @@ -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 } diff --git a/utils/contentModeration.js b/utils/contentModeration.js index 6c76af9..f995c02 100644 --- a/utils/contentModeration.js +++ b/utils/contentModeration.js @@ -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 +} diff --git a/utils/exploreAlgorithm.js b/utils/exploreAlgorithm.js index ed7b08d..10ec5fb 100644 --- a/utils/exploreAlgorithm.js +++ b/utils/exploreAlgorithm.js @@ -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) diff --git a/utils/verifyBadge.js b/utils/verifyBadge.js index 8543a3f..f60464e 100644 --- a/utils/verifyBadge.js +++ b/utils/verifyBadge.js @@ -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 }