Fix shop product bugs: partial catalog search, premature product activation, verify badge population

- searchCatalog: use $regex partial match (like user search) instead of
  MongoDB $text full-word search, so typing part of a product name works
- createShopProduct: new listings always start pending_shop_approval instead
  of auto-activating when the shop itself is already active, so incomplete
  wizard drafts no longer leak into the public discover feed before the
  owner finishes and it's approved
- Populate owner.verify_badge on shop responses (getShopPublic, getShopById,
  getDiscoverFeed, getShopProductById, getCatalogListings) so the app can
  show a verified/license badge next to shop names

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
payacom
2026-09-15 12:32:26 +03:30
parent c302aea177
commit ddd70143cf
2 changed files with 24 additions and 13 deletions

View File

@@ -58,12 +58,13 @@ const searchCatalog = async (req, res, next) => {
return res.json({ products: [] })
}
const products = await ProductCatalogModel.find(
{ $text: { $search: q } },
{ score: { $meta: 'textScore' } }
)
.sort({ score: { $meta: 'textScore' } })
.limit(20)
// قبلاً $text (جستجوی متنیِ کاملِ مونگو) بود که فقط با کلمه‌ی کامل/ریشه‌شده مچ می‌شد،
// نه با بخشی از یه کلمه — با اینکه کاربر دقیقاً خواسته بود «قسمتی از نام کالا رو
// وارد کنی» (عینِ جستجوی کاربر که $regex استفاده می‌کنه، نه $text)
const escaped = q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
const products = await ProductCatalogModel.find({
name: { $regex: escaped, $options: 'i' }
}).limit(20)
return res.json({ products })
} catch (error) {
@@ -169,7 +170,12 @@ const createShopProduct = async (req, res, next) => {
category: clonedFields.category || null,
sub_category: clonedFields.sub_category || null,
sub_sub_category: clonedFields.sub_sub_category || null,
status: isCloned ? 'pending_shop_approval' : (shop.status === 'active' ? 'active' : 'pending_shop_approval')
// قبلاً اگه خودِ فروشگاه active بود، کالای تازه‌ساخته (همین قدمِ اول، فقط نام — بدون
// واریانت/تصویر/دسته‌بندی) بلافاصله status:'active' می‌گرفت و توی فیدِ دیسکاور عمومی
// نمایش داده می‌شد، با اینکه هنوز کاربر ویزارد رو تموم نکرده بود (کالاهای نیمه‌کاره/
// آزمایشی مثل عنوانِ خالی یا فقط یه عدد). الان همیشه pending می‌مونه تا قدمِ آخرِ
// ویزارد (NewProductVariantsScreen.tsx submit) صریحاً status:'active' رو ست کنه
status: 'pending_shop_approval'
})
return res.json({ message: 'کالا با موفقیت ثبت شد', listing, catalogProduct })
@@ -312,9 +318,10 @@ const getShopProducts = async (req, res, next) => {
const getShopProductById = async (req, res, next) => {
try {
const listing = await ShopProductListingModel.findById(req.params.id).populate(
'catalogProduct shop'
)
const listing = await ShopProductListingModel.findById(req.params.id).populate([
{ path: 'catalogProduct' },
{ path: 'shop', populate: { path: 'owner', select: 'verify_badge' } }
])
if (!listing) return productError(res, 404, 'کالا یافت نشد')
return res.json({ listing })
} catch (error) {
@@ -337,7 +344,10 @@ const getCatalogListings = async (req, res, next) => {
const listings = await ShopProductListingModel.find({
catalogProduct: catalogId,
status: 'active'
}).populate('shop catalogProduct')
}).populate([
{ path: 'shop', populate: { path: 'owner', select: 'verify_badge' } },
{ path: 'catalogProduct' }
])
const visible = listings.filter((listing) => listing.shop?.status === 'active')
visible.sort((a, b) => cheapestVariantPrice(a) - cheapestVariantPrice(b))
@@ -449,7 +459,7 @@ const getDiscoverFeed = async (req, res, next) => {
...validTaggedPosts.map((p) => String(p.linked_shop_product_id.shop))
])
]
const shops = await ShopModel.find({ _id: { $in: shopIds } })
const shops = await ShopModel.find({ _id: { $in: shopIds } }).populate('owner', 'verify_badge')
const shopMap = new Map(shops.map((s) => [String(s._id), s]))
const catalogIds = [...new Set(docs.map((d) => String(d.catalogProduct)).filter(Boolean))]

View File

@@ -176,6 +176,7 @@ const getShopById = async (req, res, next) => {
const { shop, error } = await findOwnedShop(req.params.id, req.user._id)
if (error === 'NOT_FOUND') return shopError(res, 404, 'فروشگاه یافت نشد')
if (error === 'FORBIDDEN') return shopError(res, 403, 'شما مالک این فروشگاه نیستید')
await shop.populate('owner', 'verify_badge')
return res.json({ shop })
} catch (error) {
next(error)
@@ -202,7 +203,7 @@ const updateShopBasic = async (req, res, next) => {
/** Public shop profile: visible to any visitor once active; owner can preview it before that. */
const getShopPublic = async (req, res, next) => {
try {
const shop = await ShopModel.findById(req.params.id).populate('owner', 'user_name first_name last_name profile_image')
const shop = await ShopModel.findById(req.params.id).populate('owner', 'user_name first_name last_name profile_image verify_badge')
if (!shop) return shopError(res, 404, 'فروشگاه یافت نشد')
const currentUserId = getOptionalUserId(req)