338 lines
9.9 KiB
JavaScript
338 lines
9.9 KiB
JavaScript
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))
|
||
|
||
let normalized = updated || existing
|
||
if (!normalized?.user) {
|
||
const refreshed = await getOrCacheDealForModeration(id)
|
||
if (refreshed?.deal) normalized = refreshed.deal
|
||
}
|
||
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,
|
||
}
|