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

334 lines
9.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const dealService = require("./deal.service")
const dealAiReviewDB = require("../db/dealAiReview.db")
const categoryDB = require("../db/category.db")
const { findCategoryById, listCategories } = categoryDB
const { findSeller } = require("../db/seller.db")
const {
getOrCacheDealForModeration,
updateDealInRedis,
} = require("./redis/dealCache.service")
const { queueDealUpdate, queueNotificationCreate } = require("./redis/dbSync.service")
const { publishNotification } = require("./redis/notificationPubsub.service")
const { getSellerById } = require("./redis/sellerCache.service")
const { attachTagsToDeal, normalizeTags } = require("./tag.service")
function normalizeDealForModResponse(deal) {
if (!deal) return deal
const images = Array.isArray(deal.images)
? deal.images.map((img, idx) => ({
id: img.id ?? 0,
imageUrl: img.imageUrl,
order: img.order ?? idx,
}))
: []
return {
...deal,
images,
myVote: deal.myVote ?? 0,
_count: { comments: Number(deal.commentCount ?? 0) },
}
}
async function enrichDealSeller(deal) {
if (!deal || deal.seller || !deal.sellerId) return deal
const seller = await getSellerById(Number(deal.sellerId))
if (!seller) return deal
return { ...deal, seller }
}
async function getPendingDeals({ page = 1, limit = 10, filters = {}, viewer = null } = {}) {
return dealService.getDeals({
preset: "RAW",
q: filters?.q,
page,
limit,
viewer,
scope: "MOD",
baseWhere: { status: "PENDING" },
filters,
useRedisSearch: true,
})
}
async function updateDealStatus(dealId, nextStatus) {
const id = Number(dealId)
if (!Number.isInteger(id) || id <= 0) {
const err = new Error("INVALID_DEAL_ID")
err.statusCode = 400
throw err
}
const { deal } = await getOrCacheDealForModeration(id)
if (!deal) {
const err = new Error("DEAL_NOT_FOUND")
err.statusCode = 404
throw err
}
if (deal.status === nextStatus) return { id: deal.id, status: deal.status }
const updatedAt = new Date()
await updateDealInRedis(id, { status: nextStatus }, { updatedAt })
queueDealUpdate({
dealId: id,
data: { status: nextStatus },
updatedAt: updatedAt.toISOString(),
}).catch((err) => console.error("DB sync deal status update failed:", err?.message || err))
return { id: deal.id, status: nextStatus }
}
async function approveDeal(dealId) {
const id = Number(dealId)
if (!Number.isInteger(id) || id <= 0) {
const err = new Error("INVALID_DEAL_ID")
err.statusCode = 400
throw err
}
const { deal } = await getOrCacheDealForModeration(id)
if (!deal) {
const err = new Error("DEAL_NOT_FOUND")
err.statusCode = 404
throw err
}
const aiReviewFromDb = await dealAiReviewDB.findDealAiReviewByDealId(id, {
select: { bestCategoryId: true, tags: true },
})
let categoryId = Number(deal.categoryId || 0)
if (!categoryId) {
const aiCategoryId = Number(deal.aiReview?.bestCategoryId || 0)
if (aiCategoryId > 0) {
categoryId = aiCategoryId
} else {
const fallbackId = aiReviewFromDb?.bestCategoryId ?? 0
if (!Number.isInteger(fallbackId) || fallbackId <= 0) {
const err = new Error("CATEGORY_REQUIRED")
err.statusCode = 400
throw err
}
categoryId = fallbackId
}
}
const aiTags = Array.isArray(deal.aiReview?.tags)
? deal.aiReview.tags
: Array.isArray(aiReviewFromDb?.tags)
? aiReviewFromDb.tags
: []
const normalizedTags = normalizeTags(aiTags)
if (deal.status === "ACTIVE" && categoryId === Number(deal.categoryId || 0) && !normalizedTags.length) {
return { id: deal.id, status: deal.status }
}
const tagResult = normalizedTags.length
? await attachTagsToDeal(id, normalizedTags)
: { tags: [] }
const updatedAt = new Date()
const redisPatch = { status: "ACTIVE", categoryId }
if (tagResult.tags?.length) {
redisPatch.tags = tagResult.tags
}
await updateDealInRedis(id, redisPatch, { updatedAt })
queueDealUpdate({
dealId: id,
data: { status: "ACTIVE", categoryId },
updatedAt: updatedAt.toISOString(),
}).catch((err) => console.error("DB sync deal approve failed:", err?.message || err))
if (Number.isInteger(Number(deal.userId)) && Number(deal.userId) > 0) {
const payload = {
userId: Number(deal.userId),
message: "Fırsatın onaylandı!",
type: "MODERATION",
createdAt: updatedAt.toISOString(),
}
queueNotificationCreate(payload).catch((err) =>
console.error("DB sync approval notification failed:", err?.message || err)
)
publishNotification(payload).catch((err) =>
console.error("Approval notification publish failed:", err?.message || err)
)
}
return { id: deal.id, status: "ACTIVE" }
}
async function rejectDeal(dealId) {
return updateDealStatus(dealId, "REJECTED")
}
async function expireDeal(dealId) {
return updateDealStatus(dealId, "EXPIRED")
}
async function unexpireDeal(dealId) {
return updateDealStatus(dealId, "ACTIVE")
}
async function getDealDetailForMod(dealId, viewer = null) {
const deal = await dealService.getDealById(dealId, viewer)
if (!deal) {
const err = new Error("DEAL_NOT_FOUND")
err.statusCode = 404
throw err
}
const aiReview = await dealAiReviewDB.findDealAiReviewByDealId(Number(dealId), {
select: {
dealId: true,
bestCategoryId: true,
tags: true,
needsReview: true,
hasIssue: true,
issueType: true,
issueReason: true,
createdAt: true,
},
})
const categoryBreadcrumb = aiReview
? await categoryDB.getCategoryBreadcrumb(aiReview.bestCategoryId, { includeUndefined: false })
: []
return {
deal,
aiReview: aiReview ? { ...aiReview, categoryBreadcrumb } : null,
}
}
async function updateDealForMod(dealId, input = {}, viewer = null) {
const id = Number(dealId)
if (!Number.isInteger(id) || id <= 0) {
const err = new Error("INVALID_DEAL_ID")
err.statusCode = 400
throw err
}
const { deal: existing } = await getOrCacheDealForModeration(id)
if (!existing) {
const err = new Error("DEAL_NOT_FOUND")
err.statusCode = 404
throw err
}
if (input.sellerId !== undefined && input.customSeller !== undefined) {
const err = new Error("SELLER_CONFLICT")
err.statusCode = 400
throw err
}
const data = {}
if (input.title !== undefined) data.title = input.title
if (input.description !== undefined) data.description = input.description ?? null
if (input.url !== undefined) data.url = input.url ?? null
if (input.price !== undefined) data.price = input.price ?? null
if (input.originalPrice !== undefined) data.originalPrice = input.originalPrice ?? null
if (input.shippingPrice !== undefined) data.shippingPrice = input.shippingPrice ?? null
if (input.couponCode !== undefined) data.couponCode = input.couponCode ?? null
if (input.location !== undefined) data.location = input.location ?? null
if (input.discountValue !== undefined) data.discountValue = input.discountValue ?? null
if (input.discountType !== undefined) {
const normalized =
typeof input.discountType === "string" ? input.discountType.toUpperCase() : null
if (normalized && !["PERCENT", "AMOUNT"].includes(normalized)) {
const err = new Error("INVALID_DISCOUNT_TYPE")
err.statusCode = 400
throw err
}
data.discountType = normalized
}
if (input.saleType !== undefined) {
const normalized = typeof input.saleType === "string" ? input.saleType.toUpperCase() : null
if (normalized && !["ONLINE", "OFFLINE", "CODE"].includes(normalized)) {
const err = new Error("INVALID_SALE_TYPE")
err.statusCode = 400
throw err
}
data.saletype = normalized
}
if (input.sellerId !== undefined) {
const sellerId = Number(input.sellerId)
if (!Number.isInteger(sellerId) || sellerId <= 0) {
const err = new Error("INVALID_SELLER_ID")
err.statusCode = 400
throw err
}
const seller = await findSeller({ id: sellerId }, { select: { id: true } })
if (!seller) {
const err = new Error("SELLER_NOT_FOUND")
err.statusCode = 404
throw err
}
data.sellerId = sellerId
data.customSeller = null
}
if (input.customSeller !== undefined) {
const normalized =
typeof input.customSeller === "string" ? input.customSeller.trim() : null
data.customSeller = normalized || null
if (data.customSeller) data.sellerId = null
}
if (input.categoryId !== undefined) {
const categoryId = Number(input.categoryId)
if (!Number.isInteger(categoryId) || categoryId < 0) {
const err = new Error("INVALID_CATEGORY_ID")
err.statusCode = 400
throw err
}
if (categoryId > 0) {
const category = await findCategoryById(categoryId, { select: { id: true } })
if (!category) {
const err = new Error("CATEGORY_NOT_FOUND")
err.statusCode = 404
throw err
}
}
data.categoryId = categoryId
}
if (!Object.keys(data).length) {
const enriched = await enrichDealSeller(existing)
return normalizeDealForModResponse(enriched)
}
const updatedAt = new Date()
const updated = await updateDealInRedis(id, data, { updatedAt })
queueDealUpdate({
dealId: id,
data,
updatedAt: updatedAt.toISOString(),
}).catch((err) => console.error("DB sync deal update failed:", err?.message || err))
const normalized = updated || existing
const enriched = await enrichDealSeller(normalized)
return normalizeDealForModResponse(enriched)
}
async function listAllCategories() {
return listCategories({
select: { id: true, name: true, parentId: true },
orderBy: { id: "asc" },
})
}
module.exports = {
getPendingDeals,
approveDeal,
rejectDeal,
expireDeal,
unexpireDeal,
getDealDetailForMod,
updateDealForMod,
listAllCategories,
}