const { randomUUID } = require("crypto") const { getRedisClient } = require("./client") const dealAnalyticsDb = require("../../db/dealAnalytics.db") const { ensureMinDealTtl } = require("./dealCache.service") const { DEAL_ANALYTICS_TOTAL_HASH_KEY } = require("./dbSync.service") const DEAL_ANALYTICS_TOTAL_PREFIX = "deals:analytics:total:" function createRedisClient() { return getRedisClient() } function getTotalKey(dealId) { return `${DEAL_ANALYTICS_TOTAL_PREFIX}${dealId}` } function normalizeIds(ids = []) { return Array.from( new Set( (Array.isArray(ids) ? ids : []) .map((id) => Number(id)) .filter((id) => Number.isInteger(id) && id > 0) ) ) } function isValidEventType(type) { const normalized = String(type || "").toUpperCase() return ["IMPRESSION", "VIEW", "CLICK"].includes(normalized) } function aggregateEventIncrements(events = []) { const byDeal = new Map() for (const event of events) { const dealId = Number(event.dealId) if (!Number.isInteger(dealId) || dealId <= 0) continue const type = String(event.type || "").toUpperCase() const entry = byDeal.get(dealId) || { dealId, impressions: 0, views: 0, clicks: 0 } if (type === "IMPRESSION") entry.impressions += 1 else if (type === "VIEW") entry.views += 1 else if (type === "CLICK") entry.clicks += 1 byDeal.set(dealId, entry) } return Array.from(byDeal.values()) } async function seedDealAnalyticsTotals({ dealIds = [] } = {}) { const ids = normalizeIds(dealIds) if (!ids.length) return 0 await dealAnalyticsDb.ensureTotalsForDealIds(ids) const totals = await dealAnalyticsDb.getTotalsByDealIds(ids) const totalsById = new Map(totals.map((t) => [t.dealId, t])) const redis = createRedisClient() try { const pipeline = redis.pipeline() ids.forEach((id) => { const total = totalsById.get(id) || { impressions: 0, views: 0, clicks: 0 } pipeline.hset( getTotalKey(id), "impressions", String(total.impressions || 0), "views", String(total.views || 0), "clicks", String(total.clicks || 0) ) }) await pipeline.exec() return ids.length } finally {} } async function initDealAnalyticsTotal(dealId) { const id = Number(dealId) if (!Number.isInteger(id) || id <= 0) return 0 await dealAnalyticsDb.ensureTotalsForDealIds([id]) await seedDealAnalyticsTotals({ dealIds: [id] }) return 1 } async function queueDealEvents(events = []) { const valid = (Array.isArray(events) ? events : []).filter( (e) => e && Number.isInteger(Number(e.dealId)) && (e.userId || e.ip) && isValidEventType(e.type) ) if (!valid.length) return 0 const increments = aggregateEventIncrements(valid) if (!increments.length) return 0 const redis = createRedisClient() try { await incrementDealAnalyticsTotalsInRedis(increments) const pipeline = redis.pipeline() increments.forEach((entry) => { const field = `dealTotals:${entry.dealId}:${randomUUID()}` pipeline.hset(DEAL_ANALYTICS_TOTAL_HASH_KEY, field, JSON.stringify(entry)) }) await pipeline.exec() return valid.length } catch { try { await dealAnalyticsDb.applyDealTotalsBatch(increments) return valid.length } catch { return 0 } } finally {} } async function queueDealImpressions({ dealIds = [], userId = null, ip = null } = {}) { if (!userId && !ip) return 0 const ids = normalizeIds(dealIds) if (!ids.length) return 0 const events = ids.map((dealId) => ({ dealId, type: "IMPRESSION", userId, ip, })) await Promise.all(ids.map((id) => ensureMinDealTtl(id, { minSeconds: 15 * 60 }))) return queueDealEvents(events) } async function queueDealView({ dealId, userId = null, ip = null } = {}) { if (!userId && !ip) return 0 const id = Number(dealId) if (!Number.isInteger(id) || id <= 0) return 0 await ensureMinDealTtl(id, { minSeconds: 15 * 60 }) return queueDealEvents([ { dealId: id, type: "VIEW", userId, ip, }, ]) } async function queueDealClick({ dealId, userId = null, ip = null } = {}) { if (!userId && !ip) return 0 const id = Number(dealId) if (!Number.isInteger(id) || id <= 0) return 0 await ensureMinDealTtl(id, { minSeconds: 15 * 60 }) return queueDealEvents([ { dealId: id, type: "CLICK", userId, ip, }, ]) } async function incrementDealAnalyticsTotalsInRedis(increments = []) { const data = (Array.isArray(increments) ? increments : []).filter( (item) => item && Number.isInteger(Number(item.dealId)) ) if (!data.length) return 0 const redis = createRedisClient() try { const pipeline = redis.pipeline() data.forEach((item) => { const key = getTotalKey(item.dealId) if (item.impressions) pipeline.hincrby(key, "impressions", Number(item.impressions)) if (item.views) pipeline.hincrby(key, "views", Number(item.views)) if (item.clicks) pipeline.hincrby(key, "clicks", Number(item.clicks)) }) await pipeline.exec() return data.length } catch { return 0 } finally {} } module.exports = { seedDealAnalyticsTotals, initDealAnalyticsTotal, queueDealImpressions, queueDealView, queueDealClick, incrementDealAnalyticsTotalsInRedis, DEAL_ANALYTICS_TOTAL_HASH_KEY, }