const categoryDb = require("../db/category.db") const sellerDb = require("../db/seller.db") const { slugify } = require("../utils/slugify") const { listCategoriesFromRedis, setCategoryInRedis, setCategoriesInRedis, getCategoryById, } = require("./redis/categoryCache.service") const { listSellersFromRedis, setSellerInRedis, setSellersInRedis, getSellerById, setSellerDomainInRedis, } = require("./redis/sellerCache.service") const { queueCategoryUpsert, queueSellerUpsert, queueSellerDomainUpsert, } = require("./redis/dbSync.service") const { ensureCategoryIdCounter, generateCategoryId } = require("./redis/categoryId.service") const { ensureSellerIdCounter, generateSellerId } = require("./redis/sellerId.service") const { sanitizeOptionalPlainText } = require("../utils/inputSanitizer") const { normalizeMediaPath } = require("../utils/mediaPath") function httpError(statusCode, message) { const err = new Error(message) err.statusCode = statusCode return err } function normalizeCategoryPayload(input = {}, fallback = {}) { const name = input.name !== undefined ? sanitizeOptionalPlainText(input.name, { maxLength: 120 }) || "" : sanitizeOptionalPlainText(fallback.name, { maxLength: 120 }) || "" const rawSlug = input.slug !== undefined ? sanitizeOptionalPlainText(input.slug, { maxLength: 160 }) || "" : sanitizeOptionalPlainText(fallback.slug, { maxLength: 160 }) || "" const slug = rawSlug ? slugify(rawSlug) : name ? slugify(name) : fallback.slug const description = input.description !== undefined ? sanitizeOptionalPlainText(input.description, { maxLength: 300 }) || "" : sanitizeOptionalPlainText(fallback.description, { maxLength: 300 }) || "" const parentId = input.parentId !== undefined && input.parentId !== null ? Number(input.parentId) : input.parentId === null ? null : fallback.parentId ?? null const isActive = input.isActive !== undefined ? Boolean(input.isActive) : Boolean(fallback.isActive ?? true) return { name, slug, description, parentId, isActive } } async function ensureCategoryParent(parentId) { if (parentId === null || parentId === undefined) return null const pid = Number(parentId) if (!Number.isInteger(pid) || pid < 0) throw httpError(400, "INVALID_PARENT_ID") if (pid === 0) return 0 const cached = await getCategoryById(pid) if (cached) return pid const fromDb = await categoryDb.findCategoryById(pid, { select: { id: true, name: true, slug: true, parentId: true, isActive: true, description: true } }) if (!fromDb) throw httpError(404, "CATEGORY_PARENT_NOT_FOUND") await setCategoryInRedis(fromDb) return pid } async function listCategoriesCached() { let categories = await listCategoriesFromRedis() if (!categories.length) { categories = await categoryDb.listCategories({ select: { id: true, name: true, slug: true, parentId: true, isActive: true, description: true }, orderBy: { id: "asc" }, }) if (categories.length) await setCategoriesInRedis(categories) } return categories .map((cat) => ({ id: cat.id, name: cat.name, slug: cat.slug, parentId: cat.parentId ?? null, isActive: cat.isActive !== undefined ? Boolean(cat.isActive) : true, description: cat.description ?? "", })) .sort((a, b) => a.id - b.id) } async function createCategory(input = {}) { const payload = normalizeCategoryPayload(input) if (!payload.name) throw httpError(400, "CATEGORY_NAME_REQUIRED") if (!payload.slug) throw httpError(400, "CATEGORY_SLUG_REQUIRED") const categories = await listCategoriesCached() const duplicate = categories.find((c) => c.slug === payload.slug) if (duplicate) throw httpError(400, "CATEGORY_SLUG_EXISTS") await ensureCategoryIdCounter() const id = await generateCategoryId() const parentId = await ensureCategoryParent(payload.parentId ?? null) const category = { id, name: payload.name, slug: payload.slug, parentId: parentId ?? null, isActive: payload.isActive, description: payload.description ?? "", } await setCategoryInRedis(category) queueCategoryUpsert({ categoryId: id, data: { name: category.name, slug: category.slug, parentId: category.parentId, isActive: category.isActive, description: category.description ?? "", }, updatedAt: new Date().toISOString(), }).catch((err) => console.error("DB sync category create failed:", err?.message || err)) return category } async function updateCategory(categoryId, input = {}) { const id = Number(categoryId) if (!Number.isInteger(id) || id < 0) throw httpError(400, "INVALID_CATEGORY_ID") const cached = await getCategoryById(id) const existing = cached || (await categoryDb.findCategoryById(id, { select: { id: true, name: true, slug: true, parentId: true, isActive: true, description: true }, })) if (!existing) throw httpError(404, "CATEGORY_NOT_FOUND") const payload = normalizeCategoryPayload(input, existing) if (!payload.name) throw httpError(400, "CATEGORY_NAME_REQUIRED") if (!payload.slug) throw httpError(400, "CATEGORY_SLUG_REQUIRED") if (payload.parentId !== null && Number(payload.parentId) === id) { throw httpError(400, "INVALID_PARENT_ID") } const categories = await listCategoriesCached() const duplicate = categories.find((c) => c.slug === payload.slug && Number(c.id) !== id) if (duplicate) throw httpError(400, "CATEGORY_SLUG_EXISTS") const parentId = await ensureCategoryParent(payload.parentId ?? null) const category = { id, name: payload.name, slug: payload.slug, parentId: parentId ?? null, isActive: payload.isActive, description: payload.description ?? "", } await setCategoryInRedis(category) queueCategoryUpsert({ categoryId: id, data: { name: category.name, slug: category.slug, parentId: category.parentId, isActive: category.isActive, description: category.description ?? "", }, updatedAt: new Date().toISOString(), }).catch((err) => console.error("DB sync category update failed:", err?.message || err)) return category } function normalizeSellerPayload(input = {}, fallback = {}) { const name = input.name !== undefined ? sanitizeOptionalPlainText(input.name, { maxLength: 120 }) || "" : sanitizeOptionalPlainText(fallback.name, { maxLength: 120 }) || "" const url = input.url !== undefined ? sanitizeOptionalPlainText(input.url, { maxLength: 500 }) || "" : sanitizeOptionalPlainText(fallback.url, { maxLength: 500 }) || "" const sellerLogo = input.sellerLogo !== undefined ? normalizeMediaPath(sanitizeOptionalPlainText(input.sellerLogo, { maxLength: 500 }) || "") || "" : normalizeMediaPath(sanitizeOptionalPlainText(fallback.sellerLogo, { maxLength: 500 }) || "") || "" const isActive = input.isActive !== undefined ? Boolean(input.isActive) : Boolean(fallback.isActive ?? true) return { name, url: url ?? "", sellerLogo: sellerLogo ?? "", isActive } } async function listSellersCached() { let sellers = await listSellersFromRedis() if (!sellers.length) { sellers = await sellerDb.findSellers({}, { select: { id: true, name: true, url: true, sellerLogo: true, isActive: true }, orderBy: { name: "asc" }, }) if (sellers.length) await setSellersInRedis(sellers) } return sellers.map((seller) => ({ id: seller.id, name: seller.name, url: seller.url ?? "", sellerLogo: normalizeMediaPath(seller.sellerLogo) ?? "", isActive: seller.isActive !== undefined ? Boolean(seller.isActive) : true, })) } async function createSeller(input = {}, { createdById } = {}) { const payload = normalizeSellerPayload(input) if (!payload.name) throw httpError(400, "SELLER_NAME_REQUIRED") const creatorId = Number(createdById) if (!Number.isInteger(creatorId) || creatorId <= 0) throw httpError(400, "CREATED_BY_REQUIRED") const sellers = await listSellersCached() const duplicate = sellers.find((s) => s.name.toLowerCase() === payload.name.toLowerCase()) if (duplicate) throw httpError(400, "SELLER_NAME_EXISTS") await ensureSellerIdCounter() const id = await generateSellerId() const seller = { id, name: payload.name, url: payload.url ?? "", sellerLogo: payload.sellerLogo ?? "", isActive: payload.isActive, } await setSellerInRedis(seller) queueSellerUpsert({ sellerId: id, data: { name: seller.name, url: seller.url ?? "", sellerLogo: seller.sellerLogo ?? "", isActive: seller.isActive, createdById: creatorId, }, updatedAt: new Date().toISOString(), }).catch((err) => console.error("DB sync seller create failed:", err?.message || err)) if (input.domain) { const domain = (sanitizeOptionalPlainText(input.domain, { maxLength: 255 }) || "").toLowerCase() if (domain) { await setSellerDomainInRedis(domain, id) queueSellerDomainUpsert({ sellerId: id, domain, createdById: creatorId }).catch((err) => console.error("DB sync seller domain failed:", err?.message || err) ) } } return seller } async function updateSeller(sellerId, input = {}, { createdById } = {}) { const id = Number(sellerId) if (!Number.isInteger(id) || id <= 0) throw httpError(400, "INVALID_SELLER_ID") const creatorId = Number(createdById) if (!Number.isInteger(creatorId) || creatorId <= 0) throw httpError(400, "CREATED_BY_REQUIRED") const cached = await getSellerById(id) const existing = cached || (await sellerDb.findSeller({ id }, { select: { id: true, name: true, url: true, sellerLogo: true, isActive: true } })) if (!existing) throw httpError(404, "SELLER_NOT_FOUND") const payload = normalizeSellerPayload(input, existing) if (!payload.name) throw httpError(400, "SELLER_NAME_REQUIRED") const sellers = await listSellersCached() const duplicate = sellers.find( (s) => s.name.toLowerCase() === payload.name.toLowerCase() && Number(s.id) !== id ) if (duplicate) throw httpError(400, "SELLER_NAME_EXISTS") const seller = { id, name: payload.name, url: payload.url ?? "", sellerLogo: payload.sellerLogo ?? "", isActive: payload.isActive, } await setSellerInRedis(seller) queueSellerUpsert({ sellerId: id, data: { name: seller.name, url: seller.url ?? "", sellerLogo: seller.sellerLogo ?? "", isActive: seller.isActive, }, updatedAt: new Date().toISOString(), }).catch((err) => console.error("DB sync seller update failed:", err?.message || err)) if (input.domain) { const domain = (sanitizeOptionalPlainText(input.domain, { maxLength: 255 }) || "").toLowerCase() if (domain) { await setSellerDomainInRedis(domain, id) queueSellerDomainUpsert({ sellerId: id, domain, createdById: creatorId }).catch((err) => console.error("DB sync seller domain failed:", err?.message || err) ) } } return seller } module.exports = { listCategoriesCached, createCategory, updateCategory, listSellersCached, createSeller, updateSeller, }