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 } } } }, votes: { select: { userId: true, voteType: true } }, savedBy: { select: { userId: true, createdAt: 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 } ) 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)) } 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, }