diff --git a/controllers/application/shop-chat/shopChatController.js b/controllers/application/shop-chat/shopChatController.js index 7b9d158..6287d02 100644 --- a/controllers/application/shop-chat/shopChatController.js +++ b/controllers/application/shop-chat/shopChatController.js @@ -7,6 +7,7 @@ const ShopModel = require('../../../models/ShopModel') const ShopChatMessageModel = require('../../../models/ShopChatMessageModel') const UserModel = require('../../../models/UserModel') const { optimizeImageFile } = require('../../../utils/imageOptimize') +const { sendShopChatPushNotification } = require('../../../helpers/chatHelpers') let optimizeVideoFaststart = async () => {} try { ({ optimizeVideoFaststart } = require('../../../utils/postVideoProcessing')) @@ -200,16 +201,27 @@ const sendShopChatMessage = async (req, res, next) => { const { content } = req.body if (!content || !content.trim()) return chatError(res, 422, 'متن پیام الزامی است') + const senderRole = isOwner ? 'shop' : 'buyer' const message = await ShopChatMessageModel.create({ shop: shop._id, buyer: buyerId, - senderRole: isOwner ? 'shop' : 'buyer', + senderRole, senderId: req.user._id, content: content.trim(), readByShop: isOwner, readByBuyer: !isOwner }) + sendShopChatPushNotification({ + shop, + senderId: req.user._id, + senderRole, + buyerId, + receiverId: isOwner ? buyerId : shop.owner + }).catch((err) => { + console.error('[shopChatController] push notification failed:', err.message) + }) + return res.status(201).json({ message }) } catch (error) { next(error) @@ -251,10 +263,11 @@ const sendShopChatFileMessage = async (req, res, next) => { } const fileUrl = `/shop-chat/${uniqueFileName}` + const senderRole = isOwner ? 'shop' : 'buyer' const message = await ShopChatMessageModel.create({ shop: shop._id, buyer: buyerId, - senderRole: isOwner ? 'shop' : 'buyer', + senderRole, senderId: req.user._id, content: content && content.trim() ? content.trim() : null, file: fileUrl, @@ -263,6 +276,16 @@ const sendShopChatFileMessage = async (req, res, next) => { readByBuyer: !isOwner }) + sendShopChatPushNotification({ + shop, + senderId: req.user._id, + senderRole, + buyerId, + receiverId: isOwner ? buyerId : shop.owner + }).catch((err) => { + console.error('[shopChatController] push notification failed:', err.message) + }) + return res.status(201).json({ message }) } catch (error) { next(error) diff --git a/controllers/application/shops/shopController.js b/controllers/application/shops/shopController.js index 08df933..e33f017 100644 --- a/controllers/application/shops/shopController.js +++ b/controllers/application/shops/shopController.js @@ -1,4 +1,5 @@ /* eslint-disable camelcase */ +const mongoose = require('mongoose') const fs = require('fs-extra') const path = require('path') const jwt = require('jsonwebtoken') @@ -7,8 +8,12 @@ const ShopProductListingModel = require('../../../models/ShopProductListingModel const ShopRatingModel = require('../../../models/ShopRatingModel') const ShopFollowModel = require('../../../models/ShopFollowModel') const OrderModel = require('../../../models/OrderModel') +const MessageModel = require('../../../models/MessageModel') +const UserModel = require('../../../models/UserModel') const { maskPhoneNumbers } = require('../../../utils/phoneNumberMasking') const { optimizeImageFile } = require('../../../utils/imageOptimize') +const { viewerIsBlockedBy } = require('../../../utils/blockVisibility') +const { enrichMessage, emitChatEvents } = require('../../../helpers/chatHelpers') const MS_DAY = 24 * 60 * 60 * 1000 const ANALYTICS_RANGE_DAYS = { '7d': 7, '30d': 30, '90d': 90, all: null } @@ -400,6 +405,76 @@ const submitShop = async (req, res, next) => { } } +/** عینِ sendProductToUsers توی shopProductController.js — پیام‌های چتِ معمولی + * (MessageModel) با یه پیامِ sharedShop برای هرکدوم از گیرنده‌ها؛ برای ارسالِ خودِ + * فروشگاه (نه یه کالای خاص) از دکمه‌ی «ارسال» کنارِ اشتراک‌گذاری */ +const sendShopToUsers = async (req, res, next) => { + try { + const senderId = req.user._id + const { receiverIds } = req.body + + if (!Array.isArray(receiverIds) || !receiverIds.length) { + return shopError(res, 400, 'حداقل یک گیرنده انتخاب کنید') + } + + const uniqueReceivers = [ + ...new Set( + receiverIds + .map(String) + .filter((id) => mongoose.Types.ObjectId.isValid(id) && id !== String(senderId)) + ) + ].slice(0, 50) + + if (!uniqueReceivers.length) { + return shopError(res, 400, 'گیرنده معتبر یافت نشد') + } + + const shop = await ShopModel.findById(req.params.id) + if (!shop) return shopError(res, 404, 'فروشگاه یافت نشد') + + const sharedShop = { + shopId: String(shop._id), + name: shop.name, + logo: shop.logo || '', + url: `https://modstagram.com/shops/profile/${shop._id}` + } + + const content = JSON.stringify({ sharedShop }) + const io = req.app.get('io') + + let sentCount = 0 + for (const receiverId of uniqueReceivers) { + const receiver = await UserModel.findById(receiverId).select('blocked_users').lean() + if (!receiver) continue + if (viewerIsBlockedBy(receiver, senderId)) continue + + const newMessage = new MessageModel({ + senderId, + receiverId, + content + }) + await newMessage.save() + + if (io) { + const formatted = await enrichMessage(newMessage) + emitChatEvents(io, formatted, senderId, receiverId) + } + sentCount += 1 + } + + if (!sentCount) { + return shopError(res, 400, 'ارسال به هیچ گیرنده‌ای انجام نشد') + } + + return res.status(200).json({ + message: `فروشگاه به ${sentCount} نفر ارسال شد`, + sentCount + }) + } catch (error) { + next(error) + } +} + module.exports = { createDraftShop, getMyShops, @@ -412,5 +487,6 @@ module.exports = { updateShopLocation, updateShopContact, toggleShopFollow, - submitShop + submitShop, + sendShopToUsers } diff --git a/helpers/chatHelpers.js b/helpers/chatHelpers.js index f70ca84..860a30a 100644 --- a/helpers/chatHelpers.js +++ b/helpers/chatHelpers.js @@ -105,6 +105,32 @@ async function sendChatPushNotification(senderId, receiverId) { }) } +// چتِ فروشگاه (خریدار <-> مالکِ فروشگاه) قبلاً هیچ پوشی نمی‌فرستاد — کاملاً جدا از +// sendChatPushNotification بالا چون فرستنده می‌تونه «فروشگاه» باشه، نه یه کاربرِ ساده، +// پس تایتل/عکسِ نوتیف باید از روی نقشِ فرستنده (shop یا buyer) انتخاب بشه +async function sendShopChatPushNotification({ shop, senderId, senderRole, receiverId, buyerId }) { + let title + let image + if (senderRole === 'shop') { + title = shop?.name || 'فروشگاه' + image = buildStorageUrl(shop?.logo) || undefined + } else { + const sender = await UserModel.findById(senderId).select('first_name last_name user_name profile_image').lean() + if (!sender) return + title = `${sender.first_name || ''} ${sender.last_name || ''}`.trim() || sender.user_name || 'کاربر' + image = buildStorageUrl(sender.profile_image) || undefined + } + await sendPushToUser(receiverId, { + title, + description: 'یک پیام جدید برای شما ارسال کرد', + type: 'shop_chat_message', + // buyer_id همیشه لازمه تا مالکِ فروشگاه (که می‌تونه چند نخِ گفتگو با خریدارهای مختلف + // داشته باشه) بدونه دقیقاً کدوم نخ رو باز کنه؛ برای خودِ خریدار هم همون id خودشه + data: { shop_id: String(shop?._id || ''), sender_role: senderRole, buyer_id: String(buyerId || '') }, + image + }) +} + function parseForwardedContent(content) { if (!content) return { body: '', forwardedFrom: null } @@ -138,5 +164,6 @@ module.exports = { formatTimeOnly, enrichMessage, emitChatEvents, - parseForwardedContent + parseForwardedContent, + sendShopChatPushNotification } diff --git a/routes/application/shops/index.js b/routes/application/shops/index.js index bdc4bea..f98e752 100644 --- a/routes/application/shops/index.js +++ b/routes/application/shops/index.js @@ -11,7 +11,8 @@ const { updateShopLocation, updateShopContact, toggleShopFollow, - submitShop + submitShop, + sendShopToUsers } = require('../../../controllers/application/shops/shopController') const { getCart, @@ -48,5 +49,6 @@ router.patch('/:id/location', [blockCheck], updateShopLocation) router.patch('/:id/contact', [blockCheck], updateShopContact) router.post('/:id/follow', [blockCheck], toggleShopFollow) router.post('/:id/submit', [blockCheck], submitShop) +router.post('/:id/send', [blockCheck], sendShopToUsers) module.exports = router