// routes/deals.js const express = require("express") const router = express.Router() const requireAuth = require("../middleware/requireAuth") const requireNotRestricted = require("../middleware/requireNotRestricted") const optionalAuth = require("../middleware/optionalAuth") const { upload } = require("../middleware/upload.middleware") const { validate } = require("../middleware/validate.middleware") const { endpoints } = require("@shared/contracts") const requireApiKey = require("../middleware/requireApiKey") const userDB = require("../db/user.db") const { getDeals, getDealById, createDeal, getDealEngagement, getDealSuggestions, getBestWidgetDeals, } = require("../services/deal.service") const dealSaveService = require("../services/dealSave.service") const dealReportService = require("../services/dealReport.service") const { mapCreateDealRequestToDealCreateData } = require("../adapters/requests/dealCreate.adapter") const { mapDealToDealDetailResponse } = require("../adapters/responses/dealDetail.adapter") const { mapDealToDealCardResponse, mapPaginatedDealsToDealCardResponse } = require("../adapters/responses/dealCard.adapter") const { getClientIp } = require("../utils/requestInfo") const { queueDealImpressions, queueDealView, queueDealClick, } = require("../services/redis/dealAnalytics.service") const { getOrCacheDeal } = require("../services/redis/dealCache.service") const { enqueueAuditFromRequest, buildAuditMeta } = require("../services/audit.service") const { AUDIT_ACTIONS } = require("../services/auditActions") const { deals, users } = endpoints function parsePage(value) { const num = Number(value) if (!Number.isInteger(num) || num < 1) return 1 return num } const listQueryValidator = validate(deals.dealsListRequestSchema, "query", "validatedDealListQuery") const buildViewer = (req) => req.auth ? { userId: req.auth.userId, role: req.auth.role } : null function createListHandler(preset) { return async (req, res) => { try { const viewer = buildViewer(req) const { q, page, limit, hotListId, trendingListId } = req.validatedDealListQuery const payload = await getDeals({ preset, q, page, limit, viewer, filters: req.query, hotListId, trendingListId, }) const response = deals.dealsListResponseSchema.parse( mapPaginatedDealsToDealCardResponse(payload) ) const dealIds = payload?.results?.map((deal) => deal.id) || [] queueDealImpressions({ dealIds, userId: req.auth?.userId ?? null, ip: getClientIp(req), }).catch((err) => console.error("Deal impression queue failed:", err?.message || err)) res.json(response) } catch (err) { console.error(err) const status = err.statusCode || 500 res.status(status).json({ error: err.message || "Sunucu hatasi" }) } } } // Public deals of a user (viewer optional; self profile => "MY" else "USER_PUBLIC") router.get( "/users/:userName/deals", optionalAuth, validate(users.userProfileRequestSchema, "params", "validatedUserProfile"), listQueryValidator, async (req, res) => { try { const { userName } = req.validatedUserProfile const targetUser = await userDB.findUser( { username: userName }, { select: { id: true } } ) if (!targetUser) return res.status(404).json({ error: "Kullanici bulunamadi" }) const { q, page, limit, hotListId, trendingListId } = req.validatedDealListQuery const viewer = buildViewer(req) const isSelfProfile = viewer?.userId === targetUser.id const preset = isSelfProfile ? "MY" : "USER_PUBLIC" const payload = await getDeals({ preset, q, page, limit, targetUserId: targetUser.id, viewer, filters: req.query, hotListId, trendingListId, }) const response = deals.dealsListResponseSchema.parse( mapPaginatedDealsToDealCardResponse(payload) ) const dealIds = payload?.results?.map((deal) => deal.id) || [] queueDealImpressions({ dealIds, userId: req.auth?.userId ?? null, ip: getClientIp(req), }).catch((err) => console.error("Deal impression queue failed:", err?.message || err)) res.json(response) } catch (err) { console.error(err) const status = err.statusCode || 500 res.status(status).json({ error: err.message || "Sunucu hatasi" }) } } ) // My deals (auth required) router.get( "/me/deals", requireAuth, listQueryValidator, createListHandler("MY") ) router.get("/new", requireApiKey, optionalAuth, listQueryValidator, createListHandler("NEW")) router.get("/hot", requireApiKey, optionalAuth, listQueryValidator, createListHandler("HOT")) router.get("/trending", requireApiKey, optionalAuth, listQueryValidator, createListHandler("TRENDING")) router.get("/search/suggest", optionalAuth, async (req, res) => { try { const q = String(req.query.q || "").trim() const limit = Number(req.query.limit || 8) const payload = await getDealSuggestions({ q, limit, viewer: buildViewer(req) }) const response = mapPaginatedDealsToDealCardResponse({ page: 1, total: payload.results.length, totalPages: 1, results: payload.results, }) res.json(response.results) } catch (err) { console.error(err) res.status(500).json({ error: "Sunucu hatasi" }) } }) // Resolve deal URL (SSR uses api key; user token optional) router.post("/url", requireApiKey, optionalAuth, async (req, res) => { try { const dealId = Number(req.body?.dealId) if (!Number.isInteger(dealId) || dealId <= 0) { return res.status(400).json({ error: "dealId invalid" }) } const deal = await getOrCacheDeal(dealId, { ttlSeconds: 15 * 60 }) if (!deal) return res.status(404).json({ error: "Deal bulunamadi" }) if (deal.status === "PENDING" || deal.status === "REJECTED") { const isOwner = req.auth?.userId && Number(deal.userId) === Number(req.auth.userId) const isMod = req.auth?.role === "MOD" || req.auth?.role === "ADMIN" if (!isOwner && !isMod) return res.status(404).json({ error: "Deal bulunamadi" }) } const userId = req.auth?.userId ?? null const ip = getClientIp(req) queueDealClick({ dealId, userId, ip }).catch((err) => console.error("Deal click queue failed:", err?.message || err) ) res.json({ url: deal.url ?? null }) } catch (err) { console.error(err) res.status(500).json({ error: "Sunucu hatasi" }) } }) // Report deal (auth required) router.post("/:id/report", requireAuth, async (req, res) => { try { const id = Number(req.params.id) const reason = req.body?.reason const note = req.body?.note const result = await dealReportService.createDealReport({ dealId: id, userId: req.auth.userId, reason, note, }) enqueueAuditFromRequest( req, AUDIT_ACTIONS.DEAL.REPORT_CREATE, buildAuditMeta({ entityType: "DEAL", entityId: id, extra: { reason: reason ?? null }, }) ) res.json(result) } catch (err) { console.error(err) const status = err.statusCode || 500 res.status(status).json({ error: err.message || "Report basarisiz" }) } }) // Saved deals (auth required) router.post("/:id/save", requireAuth, async (req, res) => { try { const id = Number(req.params.id) const result = await dealSaveService.saveDealForUser({ userId: req.auth.userId, dealId: id, }) enqueueAuditFromRequest( req, AUDIT_ACTIONS.DEAL.SAVE, buildAuditMeta({ entityType: "DEAL", entityId: id, }) ) res.json(result) } catch (err) { console.error(err) const status = err.statusCode || 500 res.status(status).json({ error: err.message || "Kaydetme basarisiz" }) } }) router.delete("/:id/save", requireAuth, async (req, res) => { try { const id = Number(req.params.id) await dealSaveService.removeSavedDealForUser({ userId: req.auth.userId, dealId: id, }) enqueueAuditFromRequest( req, AUDIT_ACTIONS.DEAL.UNSAVE, buildAuditMeta({ entityType: "DEAL", entityId: id, }) ) res.sendStatus(200) } catch (err) { console.error(err) const status = err.statusCode || 500 res.status(status).json({ error: err.message || "Silme basarisiz" }) } }) router.get("/saved", requireAuth, async (req, res) => { try { const page = parsePage(req.query.page) const payload = await dealSaveService.listSavedDeals({ userId: req.auth.userId, page, }) const response = deals.dealsListResponseSchema.parse( mapPaginatedDealsToDealCardResponse(payload) ) res.json(response) } catch (err) { console.error(err) const status = err.statusCode || 500 res.status(status).json({ error: err.message || "Kaydedilenler alinamadi" }) } }) // Best deals widget: hot day/week/year top 5 router.get("/widgets/best", requireApiKey, optionalAuth, async (req, res) => { try { const viewer = buildViewer(req) const limitRaw = Number(req.query.limit ?? 5) const limit = Number.isFinite(limitRaw) ? Math.max(1, Math.min(20, limitRaw)) : 5 const payload = await getBestWidgetDeals({ viewer, limit }) const hotDay = payload.hotDay.map(mapDealToDealCardResponse) const hotWeek = payload.hotWeek.map(mapDealToDealCardResponse) const hotMonth = payload.hotMonth.map(mapDealToDealCardResponse) const dealIds = [...payload.hotDay, ...payload.hotWeek, ...payload.hotMonth].map((d) => d.id) if (dealIds.length) { queueDealImpressions({ dealIds, userId: req.auth?.userId ?? null, ip: getClientIp(req), }).catch((err) => console.error("Deal impression queue failed:", err?.message || err)) } res.json({ hotDay, hotWeek, hotMonth }) } catch (err) { console.error(err) const status = err.statusCode || 500 res.status(status).json({ error: err.message || "Sunucu hatasi" }) } }) router.get( "/search", optionalAuth, listQueryValidator, async (req, res) => { try { const { q, page, limit, hotListId, trendingListId } = req.validatedDealListQuery if (!q || !q.trim()) { return res.json({ results: [], total: 0, totalPages: 0, page }) } const payload = await getDeals({ preset: "NEW", q, page, limit, viewer: buildViewer(req), filters: req.query, baseWhere: { status: "ACTIVE" }, hotListId, trendingListId, useRedisSearch: true, }) const response = deals.dealsListResponseSchema.parse( mapPaginatedDealsToDealCardResponse(payload) ) const dealIds = payload?.results?.map((deal) => deal.id) || [] queueDealImpressions({ dealIds, userId: req.auth?.userId ?? null, ip: getClientIp(req), }).catch((err) => console.error("Deal impression queue failed:", err?.message || err)) res.json(response) } catch (err) { console.error(err) const status = err.statusCode || 500 res.status(status).json({ error: err.message || "Sunucu hatasi" }) } } ) // TOP deals (daily/weekly/monthly) - viewer optional router.get("/top", optionalAuth, async (req, res) => { try { const viewer = buildViewer(req) const range = String(req.query.range || "day") const limitRaw = Number(req.query.limit ?? 6) const limit = Number.isFinite(limitRaw) ? Math.max(1, Math.min(20, limitRaw)) : 6 let preset = "HOT_DAY" if (range === "week") preset = "HOT_WEEK" else if (range === "month") preset = "HOT_MONTH" else if (range !== "day") return res.status(400).json({ error: "range invalid" }) const payload = await getDeals({ preset, q: null, page: 1, limit, viewer, filters: req.query, }) const response = deals.dealsListResponseSchema.parse( mapPaginatedDealsToDealCardResponse(payload) ) const dealIds = payload?.results?.map((deal) => deal.id) || [] queueDealImpressions({ dealIds, userId: req.auth?.userId ?? null, ip: getClientIp(req), }).catch((err) => console.error("Deal impression queue failed:", err?.message || err)) // frontend DealCard[] bekliyor res.json(response.results) } catch (err) { console.error(err) const status = err.statusCode || 500 res.status(status).json({ error: err.message || "Sunucu hatasi" }) } }) router.post( "/engagement", requireAuth, validate(deals.dealEngagementRequestSchema, "body", "validatedEngagement"), async (req, res) => { try { const { ids } = req.validatedEngagement const viewer = buildViewer(req) const engagement = await getDealEngagement(ids, viewer) res.json(deals.dealEngagementResponseSchema.parse(engagement)) } catch (err) { const status = err.statusCode || 500 res.status(status).json({ error: err.message || "Sunucu hatasi" }) } } ) router.get( "/:id", optionalAuth, validate(deals.dealDetailRequestSchema, "params", "validatedDealId"), async (req, res) => { try { const { id } = req.validatedDealId const deal = await getDealById(id, buildViewer(req)) if (!deal) return res.status(404).json({ error: "Deal bulunamadi" }) queueDealView({ dealId: deal.id, userId: req.auth?.userId ?? null, ip: getClientIp(req), }).catch((err) => console.error("Deal view queue failed:", err?.message || err)) const mapped = mapDealToDealDetailResponse(deal) res.json(deals.dealDetailResponseSchema.parse(mapped)) } catch (err) { console.error(err) res.status(500).json({ error: "Sunucu hatasi" }) } } ) // Create deal (auth required) router.post( "/", requireAuth, requireNotRestricted({ checkSuspend: true }), upload.array("images", 5), validate(deals.dealCreateRequestSchema, "body", "validatedDealPayload"), async (req, res) => { try { const userId = req.auth.userId const dealCreateData = mapCreateDealRequestToDealCreateData( req.validatedDealPayload, userId ) const deal = await createDeal(dealCreateData, req.files || []) const mapped = mapDealToDealDetailResponse(deal) enqueueAuditFromRequest( req, AUDIT_ACTIONS.DEAL.CREATE, buildAuditMeta({ entityType: "DEAL", entityId: deal.id, after: { title: deal.title }, }) ) res.json(deals.dealCreateResponseSchema.parse(mapped)) } catch (err) { console.error(err) res.status(500).json({ error: "Sunucu hatasi" }) } } ) module.exports = router