HotTRDealsBackend/services/admin.service.js
2026-02-04 06:39:10 +00:00

307 lines
10 KiB
JavaScript

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")
function httpError(statusCode, message) {
const err = new Error(message)
err.statusCode = statusCode
return err
}
function normalizeCategoryPayload(input = {}, fallback = {}) {
const name = input.name !== undefined ? String(input.name || "").trim() : fallback.name
const rawSlug = input.slug !== undefined ? String(input.slug || "").trim() : fallback.slug
const slug = rawSlug ? slugify(rawSlug) : name ? slugify(name) : fallback.slug
const description =
input.description !== undefined ? String(input.description || "").trim() : fallback.description
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 ? String(input.name || "").trim() : fallback.name
const url = input.url !== undefined ? String(input.url || "").trim() : fallback.url
const sellerLogo =
input.sellerLogo !== undefined ? String(input.sellerLogo || "").trim() : fallback.sellerLogo
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: 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 = String(input.domain || "").trim().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 = String(input.domain || "").trim().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,
}