diff --git a/src/controllers/watchAdsSessionController.ts b/src/controllers/watchAdsSessionController.ts index 1c8376a..d18df3a 100644 --- a/src/controllers/watchAdsSessionController.ts +++ b/src/controllers/watchAdsSessionController.ts @@ -1,44 +1,68 @@ - import { Request, Response } from "express"; - import { WATCH_ADS_SESSION_STATUS } from "../models/enums/watchAds"; - import * as WatchAdsSessionService from "../services/watchAdsSession.service"; - import logger from "../config/loggingConfig"; - import { IUser } from "../types"; - - - // POST /api/v1/watch-ads/session - export const startWatchAdsSession = async (req: Request, res: Response) => { - const authUser = req.currentUser as IUser; - if (!authUser) { - logger.warn("No authenticated user found when trying to start watch-ads session."); - return res.status(401).json({ error: "Unauthorized" }); +import { Request, Response } from "express"; +import { WATCH_ADS_SESSION_STATUS } from "../models/enums/watchAds"; +import * as WatchAdsSessionService from "../services/watchAdsSession.service"; +import logger from "../config/loggingConfig"; +import { IUser } from "../types"; +import { WatchAdsBalance } from "../models/WatchAdsBalance"; + +// POST /api/v1/watch-ads/session +export const startWatchAdsSession = async (req: Request, res: Response) => { + const authUser = req.currentUser as IUser; + if (!authUser) { + logger.warn("No authenticated user found when trying to start watch-ads session."); + return res.status(401).json({ error: "Unauthorized" }); + } + + try { + // Step 1: Check if user already has an active session + const activeSession = await WatchAdsSessionService.findActiveSession(authUser._id); + if (activeSession) { + return res.json(activeSession); } - try { - // Step 1: Check if user already has an active session - const activeSession = await WatchAdsSessionService.findActiveSession(authUser._id); - if (activeSession) { - return res.json(activeSession); - } - - // Step 2: Create a new session - const newSession = await WatchAdsSessionService.createSession(authUser._id, { - status: WATCH_ADS_SESSION_STATUS.Running, - totalSegments: 20, - segmentSecs: 30, - expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), // SESSION_TTL = 24h - }); - - return res.status(201).json(newSession); - - } catch (err: any) { - // Handle race condition where another request created it just now - if (err.code === 11000) { - logger.warn(`Race detected: active WatchAdsSession already exists for user ${authUser._id}`); - const existingSession = await WatchAdsSessionService.findActiveSession(authUser._id); - if (existingSession) return res.json(existingSession); - } - - logger.error("Error starting watch-ads session", err); - return res.status(500).json({ error: "Internal server error" }); + // Step 2: Create a new session + const newSession = await WatchAdsSessionService.createSession(authUser._id, { + status: WATCH_ADS_SESSION_STATUS.Running, + totalSegments: 20, + segmentSecs: 30, + expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), // SESSION_TTL = 24h + }); + + return res.status(201).json(newSession); + + } catch (err: any) { + // Handle race condition where another request created it just now + if (err.code === 11000) { + logger.warn(`Race detected: active WatchAdsSession already exists for user ${authUser._id}`); + const existingSession = await WatchAdsSessionService.findActiveSession(authUser._id); + if (existingSession) return res.json(existingSession); } - }; + + logger.error("Error starting watch-ads session", err); + return res.status(500).json({ error: "Internal server error" }); + } +}; + +// POST /api/v1/watch-ads/session/:id/segment-complete +export const completeSegment = async (req: Request, res: Response) => { + const authUser = req.currentUser as IUser; + if (!authUser) { + return res.status(401).json({ error: "Unauthorized" }); + } + + try { + const { id } = req.params; + const { adId } = req.body; + + const updatedSession = await WatchAdsSessionService.completeSegment(authUser._id, id, adId); + if (!updatedSession) { + return res.status(404).json({ error: "Session not found or expired" }); + } + + return res.json(updatedSession); + + } catch (err: any) { + logger.error("Error completing watch-ads segment", err); + return res.status(500).json({ error: "Internal server error" }); + } +}; diff --git a/src/routes/watchAds.routes.ts b/src/routes/watchAds.routes.ts index d92c39d..044e175 100644 --- a/src/routes/watchAds.routes.ts +++ b/src/routes/watchAds.routes.ts @@ -1,5 +1,5 @@ import { Router } from 'express'; -import { startWatchAdsSession } from '../controllers/watchAdsSessionController'; +import { startWatchAdsSession, completeSegment } from '../controllers/watchAdsSessionController'; import { verifyToken } from '../middlewares/verifyToken'; const router = Router(); @@ -7,4 +7,6 @@ const router = Router(); // start/resume endpoint router.post('/watch-ads/session', verifyToken, startWatchAdsSession); +router.post('/watch-ads/session/:id/segment-complete', verifyToken, completeSegment); + export default router; \ No newline at end of file diff --git a/src/services/watchAdsSession.service.ts b/src/services/watchAdsSession.service.ts index 8edd389..f91c55c 100644 --- a/src/services/watchAdsSession.service.ts +++ b/src/services/watchAdsSession.service.ts @@ -1,6 +1,7 @@ import { Types } from 'mongoose'; import { WatchAdsSession } from '../models/WatchAdsSession'; import { WATCH_ADS_SESSION_STATUS } from '../models/enums/watchAds'; +import { WatchAdsBalance } from '../models/WatchAdsBalance'; type CreateOpts = { status?: string; // should be WATCH_ADS_SESSION_STATUS.Running @@ -28,12 +29,9 @@ export async function createSession(userId: Types.ObjectId, opts: CreateOpts = { status = WATCH_ADS_SESSION_STATUS.Running, totalSegments = opts.totalSegments ?? 20, segmentSecs = opts.segmentSecs ?? 30, - expiresAt = opts.expiresAt ?? new Date( - nowMs + (opts.totalSegments ?? 20) * (opts.segmentSecs ?? 30) * 1000 + 10 * 60 * 1000 - ), + expiresAt = opts.expiresAt ?? new Date(nowMs + 24 * 60 * 60 * 1000), } = opts; - // Validation if (totalSegments <= 0 || segmentSecs <= 0) { throw new Error("Invalid session parameters: totalSegments and segmentSecs must be greater than 0"); @@ -77,7 +75,7 @@ export async function createSession(userId: Types.ObjectId, opts: CreateOpts = { } } - // If we hit here, repeated collisions — just return the active one if any + // If we hit repeated collisions — just return the active one if any return WatchAdsSession.findOne({ userId, status: WATCH_ADS_SESSION_STATUS.Running, @@ -85,3 +83,38 @@ export async function createSession(userId: Types.ObjectId, opts: CreateOpts = { }).lean(); } +export async function completeSegment(userId: Types.ObjectId, sessionId: string, adId: string) { + // 1. Find active session + const session = await WatchAdsSession.findOne({ + _id: sessionId, + userId, + status: WATCH_ADS_SESSION_STATUS.Running, + }); + if (!session) return null; + + // 2. Increment session progress + session.completedSegments += 1; + session.earnedSecs += session.segmentSecs; + + if (session.completedSegments >= session.totalSegments) { + session.status = WATCH_ADS_SESSION_STATUS.Completed; + session.endedAt = new Date(); + } + + await session.save(); + + // 3. Increment or create balance + await WatchAdsBalance.findOneAndUpdate( + { userId }, + { + $inc: { + availableSecs: session.segmentSecs, + lifetimeEarnedSecs: session.segmentSecs, + }, + }, + { upsert: true, new: true } + ); + + // 4. Return updated session + return session.toObject(); +}