HotTRDealsBackend/services/dealSave.service.js
2026-02-07 22:42:02 +00:00

207 lines
6.2 KiB
JavaScript

const dealDB = require("../db/deal.db")
const dealSaveDB = require("../db/dealSave.db")
const { getDealsByIdsFromRedis } = require("./redis/hotDealList.service")
const {
ensureUserCache,
getUserSavedIdsFromRedis,
addUserSavedDeal,
removeUserSavedDeal,
setUserSavedDeals,
} = require("./redis/userCache.service")
const { mapDealToRedisJson } = require("./redis/dealIndexing.service")
const { getOrCacheDeal, updateDealSavesInRedis, setDealInRedis } = require("./redis/dealCache.service")
const { queueDealSaveUpdate } = require("./redis/dbSync.service")
const PAGE_LIMIT = 20
const ALLOWED_STATUSES = new Set(["ACTIVE", "EXPIRED"])
function assertPositiveInt(value, name) {
const num = Number(value)
if (!Number.isInteger(num) || num <= 0) throw new Error(`Gecersiz ${name}.`)
return num
}
function normalizePage(value) {
const num = Number(value)
if (!Number.isInteger(num) || num < 1) return 1
return num
}
const DEAL_CACHE_INCLUDE = {
user: { select: { id: true, username: true, avatarUrl: true } },
images: { orderBy: { order: "asc" }, select: { id: true, imageUrl: true, order: true } },
dealTags: { include: { tag: { select: { id: true, slug: true, name: true } } } },
comments: {
orderBy: { createdAt: "desc" },
include: {
user: { select: { id: true, username: true, avatarUrl: true } },
likes: { select: { userId: true } },
},
},
aiReview: {
select: {
bestCategoryId: true,
needsReview: true,
hasIssue: true,
issueType: true,
issueReason: true,
},
},
}
async function saveDealForUser({ userId, dealId }) {
const uid = assertPositiveInt(userId, "userId")
const did = assertPositiveInt(dealId, "dealId")
const deal = await getOrCacheDeal(did, { ttlSeconds: 15 * 60 })
.catch(() => null)
if (!deal) {
const err = new Error("Deal bulunamadi.")
err.statusCode = 404
throw err
}
if (!ALLOWED_STATUSES.has(String(deal.status))) {
const err = new Error("Bu deal kaydedilemez.")
err.statusCode = 400
throw err
}
await updateDealSavesInRedis({
dealId: did,
userId: uid,
action: "SAVE",
createdAt: new Date().toISOString(),
minSeconds: 15 * 60,
})
await addUserSavedDeal(uid, did, { ttlSeconds: 60 * 60 })
queueDealSaveUpdate({
dealId: did,
userId: uid,
action: "SAVE",
createdAt: new Date().toISOString(),
}).catch((err) => console.error("DB sync dealSave queue failed:", err?.message || err))
return { saved: true }
}
async function removeSavedDealForUser({ userId, dealId }) {
const uid = assertPositiveInt(userId, "userId")
const did = assertPositiveInt(dealId, "dealId")
await updateDealSavesInRedis({
dealId: did,
userId: uid,
action: "UNSAVE",
createdAt: new Date().toISOString(),
minSeconds: 15 * 60,
})
await removeUserSavedDeal(uid, did, { ttlSeconds: 60 * 60 })
queueDealSaveUpdate({
dealId: did,
userId: uid,
action: "UNSAVE",
createdAt: new Date().toISOString(),
}).catch((err) => console.error("DB sync dealSave queue failed:", err?.message || err))
return { removed: true }
}
async function listSavedDeals({ userId, page = 1 }) {
const uid = assertPositiveInt(userId, "userId")
const safePage = normalizePage(page)
const skip = (safePage - 1) * PAGE_LIMIT
await ensureUserCache(uid, { ttlSeconds: 60 * 60 })
const redisCache = await getUserSavedIdsFromRedis(uid)
const redisJsonIds = redisCache?.jsonIds || []
const savedSet = redisCache?.savedSet || new Set()
const unsavedSet = redisCache?.unsavedSet || new Set()
const where = {
userId: uid,
deal: { status: { in: Array.from(ALLOWED_STATUSES) } },
}
const [total, saves] = await Promise.all([
dealSaveDB.countDealSavesByUser(uid, { where }),
dealSaveDB.findDealSavesByUser(
uid,
{
skip,
take: PAGE_LIMIT,
orderBy: { createdAt: "desc" },
where,
}
),
])
const dbDealIds = saves.map((s) => Number(s.dealId)).filter((id) => Number.isInteger(id) && id > 0)
const baseDb = dbDealIds.filter((id) => !unsavedSet.has(id))
const extraSaved = Array.from(savedSet).filter((id) => !unsavedSet.has(id))
let mergedIds = []
if (redisJsonIds.length) {
const filteredJson = redisJsonIds.filter((id) => !unsavedSet.has(id))
const jsonSet = new Set(filteredJson)
const prependSaved = extraSaved.filter((id) => !jsonSet.has(id))
mergedIds = [...prependSaved, ...filteredJson]
baseDb.forEach((id) => {
if (!jsonSet.has(id)) mergedIds.push(id)
})
} else {
const baseSet = new Set(baseDb)
const prependSaved = extraSaved.filter((id) => !baseSet.has(id))
mergedIds = [...prependSaved, ...baseDb]
}
await setUserSavedDeals(uid, mergedIds, { ttlSeconds: 60 * 60 })
const pageIds = mergedIds.slice(skip, skip + PAGE_LIMIT)
const cachedDeals = await getDealsByIdsFromRedis(pageIds, uid)
const cachedMap = new Map(cachedDeals.map((d) => [Number(d.id), d]))
const missingIds = pageIds.filter((id) => !cachedMap.has(id))
if (missingIds.length) {
const missingDeals = await dealDB.findDeals(
{ id: { in: missingIds }, status: { in: Array.from(ALLOWED_STATUSES) } },
{ include: DEAL_CACHE_INCLUDE }
)
const fallbackMap = new Map()
missingDeals.forEach((deal) => {
const payload = mapDealToRedisJson(deal)
const myVote = 0
fallbackMap.set(Number(deal.id), {
...payload,
user: deal.user ?? null,
seller: deal.seller ?? null,
myVote,
isSaved: true,
})
})
await Promise.all(
missingDeals.map((deal) => {
const payload = mapDealToRedisJson(deal)
return setDealInRedis(deal.id, payload, { ttlSeconds: 15 * 60 })
})
)
const hydrated = await getDealsByIdsFromRedis(missingIds, uid)
hydrated.forEach((d) => cachedMap.set(Number(d.id), d))
if (!hydrated.length && fallbackMap.size) {
fallbackMap.forEach((value, key) => cachedMap.set(key, value))
}
}
const results = pageIds.map((id) => cachedMap.get(id)).filter(Boolean)
return {
page: safePage,
total: mergedIds.length,
totalPages: mergedIds.length ? Math.ceil(mergedIds.length / PAGE_LIMIT) : 0,
results,
}
}
module.exports = {
saveDealForUser,
removeSavedDealForUser,
listSavedDeals,
}