Add shop-chat push notifications and a "send shop to users" endpoint

- shop-chat messages (buyer <-> shop owner) never sent any push notification
  at all — sendShopChatPushNotification (chatHelpers.js) now fires on every
  send, mirroring the existing regular-chat push path; data payload carries
  shop_id/buyer_id/sender_role so clients can route directly to the thread
- New POST /shops/:id/send, mirroring the existing shop-product send
  endpoint — lets a user share a shop itself (not just one product) into a
  regular chat message as a sharedShop payload

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
payacom
2026-09-16 14:05:19 +03:30
parent 2e14db1ccd
commit 1954b2661d
4 changed files with 133 additions and 5 deletions

View File

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

View File

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

View File

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

View File

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