From 08b1ec32b5ac55a82a747512c2fbbda4519882f3 Mon Sep 17 00:00:00 2001 From: Darin Hajou Date: Tue, 23 Sep 2025 18:40:31 +0200 Subject: [PATCH 1/6] Add completeSegment controller endpoint --- src/controllers/watchAdsSessionController.ts | 24 ++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/controllers/watchAdsSessionController.ts b/src/controllers/watchAdsSessionController.ts index 1c8376a4..5e4f6d6c 100644 --- a/src/controllers/watchAdsSessionController.ts +++ b/src/controllers/watchAdsSessionController.ts @@ -42,3 +42,27 @@ 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 updated = await WatchAdsSessionService.completeSegment(authUser._id, id, adId); + if (!updated) { + return res.status(404).json({ error: "Session not found or expired" }); + } + + return res.json(updated); + } catch (err: any) { + logger.error("Error completing watch-ads segment", err); + return res.status(500).json({ error: "Internal server error" }); + } + }; From 6bf1ec64680683ee9ed9ce8921b089a6d26491e6 Mon Sep 17 00:00:00 2001 From: Darin Hajou Date: Thu, 2 Oct 2025 01:46:24 +0200 Subject: [PATCH 2/6] Fix indenting issue --- src/controllers/watchAdsSessionController.ts | 109 +++++++++---------- 1 file changed, 54 insertions(+), 55 deletions(-) diff --git a/src/controllers/watchAdsSessionController.ts b/src/controllers/watchAdsSessionController.ts index 5e4f6d6c..0d1ffc26 100644 --- a/src/controllers/watchAdsSessionController.ts +++ b/src/controllers/watchAdsSessionController.ts @@ -1,68 +1,67 @@ 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 { 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" }); - } - try { - // Step 1: Check if user already has an active session - const activeSession = await WatchAdsSessionService.findActiveSession(authUser._id); - if (activeSession) { - return res.json(activeSession); - } +// 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" }); + } - // 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 - }); + try { + // Step 1: Check if user already has an active session + const activeSession = await WatchAdsSessionService.findActiveSession(authUser._id); + if (activeSession) { + return res.json(activeSession); + } - return res.status(201).json(newSession); + // 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 + }); - } 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); - } + return res.status(201).json(newSession); - logger.error("Error starting watch-ads session", err); - return res.status(500).json({ error: "Internal server error" }); + } 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" }); - } +// 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; + try { + const { id } = req.params; + const { adId } = req.body; - const updated = await WatchAdsSessionService.completeSegment(authUser._id, id, adId); - if (!updated) { - return res.status(404).json({ error: "Session not found or expired" }); - } - - return res.json(updated); - } catch (err: any) { - logger.error("Error completing watch-ads segment", err); - return res.status(500).json({ error: "Internal server error" }); + const updated = await WatchAdsSessionService.completeSegment(authUser._id, id, adId); + if (!updated) { + return res.status(404).json({ error: "Session not found or expired" }); } - }; + + return res.json(updated); + } catch (err: any) { + logger.error("Error completing watch-ads segment", err); + return res.status(500).json({ error: "Internal server error" }); + } +}; From 3eb4dfac69356aa9045f3aa93cc2a0c1deb721f3 Mon Sep 17 00:00:00 2001 From: Darin Hajou Date: Thu, 2 Oct 2025 01:49:38 +0200 Subject: [PATCH 3/6] Add completeSegment service logic with userId, SessionId, adId and balance update --- src/services/watchAdsSession.service.ts | 39 +++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/src/services/watchAdsSession.service.ts b/src/services/watchAdsSession.service.ts index 8edd389a..bc794b15 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 @@ -33,7 +34,6 @@ export async function createSession(userId: Types.ObjectId, opts: CreateOpts = { ), } = opts; - // Validation if (totalSegments <= 0 || segmentSecs <= 0) { throw new Error("Invalid session parameters: totalSegments and segmentSecs must be greater than 0"); @@ -77,7 +77,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 +85,38 @@ export async function createSession(userId: Types.ObjectId, opts: CreateOpts = { }).lean(); } +export async function completeSegment(userId: Types.ObjectId, sessionId: string, adId: string) { + // 1. Find the session + const session = await WatchAdsSession.findOne({ + _id: sessionId, + userId, + status: WATCH_ADS_SESSION_STATUS.Running + }); + if (!session) return null; + + // 2. Update 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. Update balance (simply adds to the count) + await WatchAdsBalance.findOneAndUpdate( + { userId }, + { + $inc: { + availableSecs: session.segmentSecs, + lifetimeEarnedSecs: session.segmentSecs + } + }, + { upsert: true, new: true } + ); + + // 4. Return updated session + return session.toObject(); +} \ No newline at end of file From 324435e4001c2378ad87ddf6ac0a24934fe3aaf5 Mon Sep 17 00:00:00 2001 From: Darin Hajou Date: Sat, 4 Oct 2025 14:01:29 +0200 Subject: [PATCH 4/6] Add /segment-complete route with :id param --- src/routes/watchAds.routes.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/routes/watchAds.routes.ts b/src/routes/watchAds.routes.ts index d92c39d2..044e1757 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 From 027d937c1a3dc78c99af78f2c62f9309649b91ad Mon Sep 17 00:00:00 2001 From: Darin Hajou Date: Tue, 7 Oct 2025 19:09:45 +0200 Subject: [PATCH 5/6] Rename 'updated' to 'updatedSession' in controller --- src/controllers/watchAdsSessionController.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/controllers/watchAdsSessionController.ts b/src/controllers/watchAdsSessionController.ts index 0d1ffc26..d18df3ae 100644 --- a/src/controllers/watchAdsSessionController.ts +++ b/src/controllers/watchAdsSessionController.ts @@ -1,9 +1,9 @@ - import { Request, Response } from "express"; +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) => { @@ -54,12 +54,13 @@ export const completeSegment = async (req: Request, res: Response) => { const { id } = req.params; const { adId } = req.body; - const updated = await WatchAdsSessionService.completeSegment(authUser._id, id, adId); - if (!updated) { + 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(updated); + return res.json(updatedSession); + } catch (err: any) { logger.error("Error completing watch-ads segment", err); return res.status(500).json({ error: "Internal server error" }); From f931f4d3edc2022b9654efea491becc7336a3ba9 Mon Sep 17 00:00:00 2001 From: Darin Hajou Date: Tue, 7 Oct 2025 19:20:50 +0200 Subject: [PATCH 6/6] Change TTL to fixed 24h expiration --- src/services/watchAdsSession.service.ts | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/services/watchAdsSession.service.ts b/src/services/watchAdsSession.service.ts index bc794b15..f91c55c6 100644 --- a/src/services/watchAdsSession.service.ts +++ b/src/services/watchAdsSession.service.ts @@ -29,9 +29,7 @@ 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 @@ -86,15 +84,15 @@ export async function createSession(userId: Types.ObjectId, opts: CreateOpts = { } export async function completeSegment(userId: Types.ObjectId, sessionId: string, adId: string) { - // 1. Find the session - const session = await WatchAdsSession.findOne({ - _id: sessionId, - userId, - status: WATCH_ADS_SESSION_STATUS.Running + // 1. Find active session + const session = await WatchAdsSession.findOne({ + _id: sessionId, + userId, + status: WATCH_ADS_SESSION_STATUS.Running, }); if (!session) return null; - // 2. Update session progress + // 2. Increment session progress session.completedSegments += 1; session.earnedSecs += session.segmentSecs; @@ -105,18 +103,18 @@ export async function completeSegment(userId: Types.ObjectId, sessionId: string, await session.save(); - // 3. Update balance (simply adds to the count) + // 3. Increment or create balance await WatchAdsBalance.findOneAndUpdate( { userId }, { $inc: { availableSecs: session.segmentSecs, - lifetimeEarnedSecs: session.segmentSecs - } + lifetimeEarnedSecs: session.segmentSecs, + }, }, { upsert: true, new: true } ); // 4. Return updated session return session.toObject(); -} \ No newline at end of file +}