// services/seller/sellerService.js const { findSeller, findSellers } = require("../db/seller.db") const dealService = require("./deal.service") const { listSellersFromRedis, setSellerInRedis, setSellersInRedis } = require("./redis/sellerCache.service") const { getRecentDealIdsBySeller, getSellerDealIndexCount } = require("./redis/sellerDealIndex.service") const { getDealsByIdsFromRedis } = require("./redis/hotDealList.service") const DEFAULT_LIMIT = 10 const MAX_LIMIT = 50 function normalizeSellerName(value) { return String(value || "").trim() } function normalizeSeller(seller = {}) { const id = Number(seller.id) if (!Number.isInteger(id) || id <= 0) return null return { id, name: String(seller.name || "").trim(), url: seller.url ?? null, sellerLogo: seller.sellerLogo ?? null, isActive: seller.isActive !== undefined ? Boolean(seller.isActive) : true, } } async function listSellersCached() { let sellers = await listSellersFromRedis() if (sellers.length) return sellers sellers = await findSellers( {}, { select: { id: true, name: true, url: true, sellerLogo: true, isActive: true }, orderBy: { name: "asc" } } ) if (sellers.length) { await setSellersInRedis(sellers) } return sellers } function clampPagination({ page, limit }) { const rawPage = Number(page) const rawLimit = Number(limit) const normalizedPage = Number.isFinite(rawPage) ? Math.max(1, Math.floor(rawPage)) : 1 let normalizedLimit = Number.isFinite(rawLimit) ? Math.max(1, Math.floor(rawLimit)) : DEFAULT_LIMIT normalizedLimit = Math.min(MAX_LIMIT, normalizedLimit) const skip = (normalizedPage - 1) * normalizedLimit return { page: normalizedPage, limit: normalizedLimit, skip } } function normalizeDealCardFromRedis(deal = {}) { return { ...deal, id: Number(deal.id), score: Number.isFinite(deal.score) ? deal.score : 0, commentCount: Number.isFinite(deal.commentCount) ? deal.commentCount : 0, price: deal.price ?? null, originalPrice: deal.originalPrice ?? null, shippingPrice: deal.shippingPrice ?? null, discountValue: deal.discountValue ?? null, } } function hasSellerFilters(filters = {}) { if (!filters || typeof filters !== "object") return false const keys = [ "status", "categoryId", "categoryIds", "saleType", "affiliateType", "minPrice", "maxPrice", "priceMin", "priceMax", "minScore", "maxScore", "sortBy", "sortDir", "createdAfter", "createdBefore", "from", "to", "hasImage", ] return keys.some((key) => { const value = filters[key] return value !== undefined && value !== null && String(value).trim() !== "" }) } async function getSellerByName(name) { const normalized = normalizeSellerName(name) if (!normalized) { const err = new Error("SELLER_NAME_REQUIRED") err.statusCode = 400 throw err } const sellers = await listSellersCached() const lower = normalized.toLowerCase() const cached = sellers .map(normalizeSeller) .filter(Boolean) .find((seller) => seller.name.toLowerCase() === lower) if (cached) { return { id: cached.id, name: cached.name, url: cached.url, sellerLogo: cached.sellerLogo } } const seller = await findSeller( { name: { equals: normalized, mode: "insensitive" } }, { select: { id: true, name: true, url: true, sellerLogo: true, isActive: true } } ) if (seller) { await setSellerInRedis(seller) return { id: seller.id, name: seller.name, url: seller.url, sellerLogo: seller.sellerLogo } } return null } async function getDealsBySellerName(name, { page = 1, limit = 10, filters = {}, viewer = null, scope = "USER" } = {}) { const seller = await getSellerByName(name) if (!seller) { const err = new Error("SELLER_NOT_FOUND") err.statusCode = 404 throw err } const searchTerm = String(filters?.q || "").trim() const useSellerIndex = !searchTerm && !hasSellerFilters(filters) if (useSellerIndex) { const pagination = clampPagination({ page, limit }) const [total, ids] = await Promise.all([ getSellerDealIndexCount(seller.id), getRecentDealIdsBySeller({ sellerId: seller.id, offset: pagination.skip, limit: pagination.limit, }), ]) if (!total) { return { seller, payload: { page: pagination.page, total: 0, totalPages: 0, results: [], }, } } if (!ids.length) { return { seller, payload: { page: pagination.page, total, totalPages: Math.ceil(total / pagination.limit), results: [], }, } } const viewerId = viewer?.userId ? Number(viewer.userId) : null const deals = await getDealsByIdsFromRedis(ids, viewerId) if (deals.length === ids.length) { const activeDeals = deals.filter((deal) => String(deal?.status || "").toUpperCase() === "ACTIVE") if (activeDeals.length === ids.length) { return { seller, payload: { page: pagination.page, total, totalPages: Math.ceil(total / pagination.limit), results: activeDeals.map(normalizeDealCardFromRedis), }, } } } } const payload = await dealService.getDeals({ preset: "NEW", q: searchTerm || undefined, page, limit, viewer, scope, baseWhere: { sellerId: seller.id, status: "ACTIVE" }, filters, useRedisSearch: true, }) return { seller, payload } } async function getActiveSellers() { const sellers = await listSellersCached() return sellers .map(normalizeSeller) .filter((seller) => seller && seller.isActive) .sort((a, b) => a.name.localeCompare(b.name)) .map((seller) => ({ name: seller.name, sellerLogo: seller.sellerLogo, })) } module.exports = { getSellerByName, getDealsBySellerName, getActiveSellers, }