diff --git a/serverless/functions/user.yml b/serverless/functions/user.yml index 9ff2ebb..eac6593 100644 --- a/serverless/functions/user.yml +++ b/serverless/functions/user.yml @@ -346,4 +346,19 @@ getNearbyActivities: events: - httpApi: path: /user/activities/get-nearby-activities - method: get \ No newline at end of file + method: get + +addActivityToBucketInterested: + handler: src/modules/user/handlers/activities/addToBucketInterested.handler + memorySize: 384 + package: + patterns: + - 'src/modules/user/handlers/activities/**' + - ${file(./serverless/patterns/base.yml):pattern1} + - ${file(./serverless/patterns/base.yml):pattern2} + - ${file(./serverless/patterns/base.yml):pattern3} + - ${file(./serverless/patterns/base.yml):pattern4} + events: + - httpApi: + path: /user/activities/add-to-bucket-interested + method: post \ No newline at end of file diff --git a/src/modules/user/handlers/activities/addToBucketInterested.ts b/src/modules/user/handlers/activities/addToBucketInterested.ts new file mode 100644 index 0000000..baf980d --- /dev/null +++ b/src/modules/user/handlers/activities/addToBucketInterested.ts @@ -0,0 +1,72 @@ +import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda'; +import { prismaClient } from '../../../../common/database/prisma.lambda.service'; +import { verifyUserToken } from '../../../../common/middlewares/jwt/authForUser'; +import { safeHandler } from '../../../../common/utils/handlers/safeHandler'; +import ApiError from '../../../../common/utils/helper/ApiError'; +import { UserService } from '../../services/user.service'; + +const userService = new UserService(prismaClient); + +export const handler = safeHandler(async ( + event: APIGatewayProxyEvent, + context?: Context +): Promise => { + // Extract token from headers + const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'] + if (!token) { + throw new ApiError(400, 'This is a protected route. Please provide a valid token.'); + } + + // Authenticate user using verifyUserToken + const userInfo = await verifyUserToken(token); + const userId = userInfo.id; + + if (Number.isNaN(userId)) { + throw new ApiError(400, 'User id must be a number'); + } + + const user = await userService.getUserById(userId); + if (!user) { + throw new ApiError(404, 'User not found'); + } + + // Parse request body + let body: { activityXid: number; isBucket: boolean; bucketTypeName: string; }; + + try { + body = event.body ? JSON.parse(event.body) : {}; + } catch (error) { + throw new ApiError(400, 'Invalid JSON in request body'); + } + + const { activityXid, isBucket, bucketTypeName } = body; + + // Validate required fields + if ( + typeof activityXid !== 'number' || + typeof isBucket !== 'boolean' || + !bucketTypeName + ) { + throw new ApiError(400, 'Required fields missing or invalid'); + } + + + // Set the passcode + const counts = await userService.addToBucketInterested(userId, isBucket, bucketTypeName, activityXid); + + return { + statusCode: 200, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + body: JSON.stringify({ + success: true, + message: `Activity added to ${isBucket ? 'bucket' : 'interested'} successfully`, + data: { + bucketCount: counts.bucketCount, + interestedCount: counts.interestedCount, + } + }), + }; +}); \ No newline at end of file diff --git a/src/modules/user/services/user.service.ts b/src/modules/user/services/user.service.ts index 0e70165..41e014b 100644 --- a/src/modules/user/services/user.service.ts +++ b/src/modules/user/services/user.service.ts @@ -709,6 +709,29 @@ export class UserService { }; } + const userBucketInterested = await tx.userBucketInterested.findMany({ + where: { + userXid: userId, + isActive: true, + }, + select: { + activityXid: true, + isBucket: true, + }, + }); + + const userBucketActivityIds = userBucketInterested + .filter(u => u.isBucket) + .map(u => u.activityXid); + + const userInterestedActivityIds = userBucketInterested + .filter(u => !u.isBucket) + .map(u => u.activityXid); + + const allUserExcludedActivityIds = userBucketInterested.map( + u => u.activityXid, + ); + const userConnectionDetails = await tx.connectDetails.findMany({ where: { userXid: userId, isActive: true }, select: { @@ -762,6 +785,11 @@ export class UserService { activityTypeXid: { in: activitiyTypesOfUserInterests.map((at) => at.id), }, + id: { + notIn: allUserExcludedActivityIds.length + ? allUserExcludedActivityIds + : [-1], // prevent empty notIn issue + }, }, skip, take: limit, @@ -842,7 +870,12 @@ export class UserService { // IF user wants the standard 4-step ranking applied TO the most hyped items: const mostHypedActivitiesRaw = await tx.activities.findMany({ where: { - id: { in: mostHypedActivityIds }, + id: { + in: mostHypedActivityIds, + notIn: allUserExcludedActivityIds.length + ? allUserExcludedActivityIds + : [-1], + }, isActive: true, activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED, amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED, @@ -966,6 +999,11 @@ export class UserService { isActive: true, activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED, amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED, + id: { + notIn: allUserExcludedActivityIds.length + ? allUserExcludedActivityIds + : [-1], // prevent empty notIn issue + }, createdAt: { gte: new Date(Date.now() - 31 * 24 * 60 * 60 * 1000) }, }; @@ -984,6 +1022,11 @@ export class UserService { isActive: true, activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED, amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED, + id: { + notIn: allUserExcludedActivityIds.length + ? allUserExcludedActivityIds + : [-1], // prevent empty notIn issue + }, }; if (effectiveCountryXid) { @@ -1010,6 +1053,11 @@ export class UserService { activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED, amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED, deletedAt: null, + id: { + notIn: allUserExcludedActivityIds.length + ? allUserExcludedActivityIds + : [-1], // prevent empty notIn issue + }, }, }); @@ -1034,6 +1082,11 @@ export class UserService { amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED, deletedAt: null, + id: { + notIn: allUserExcludedActivityIds.length + ? allUserExcludedActivityIds + : [-1], // prevent empty notIn issue + }, }, select: { id: true, @@ -1076,6 +1129,11 @@ export class UserService { isActive: true, activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED, amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED, + id: { + notIn: allUserExcludedActivityIds.length + ? allUserExcludedActivityIds + : [-1], // prevent empty notIn issue + }, }; if (effectiveCountryXid) { @@ -1152,6 +1210,8 @@ export class UserService { loggedInNetworkCount: 0, citiesInNetworkCount: 0, rating: 0, + interestedCount: userInterestedActivityIds.length, + bucketCount: userBucketActivityIds.length, pagination: { page, limit, @@ -1237,6 +1297,32 @@ export class UserService { const skip = (page - 1) * limit; + const userBucketInterested = await tx.userBucketInterested.findMany({ + where: { + userXid: userId, + isActive: true, + }, + select: { + activityXid: true, + isBucket: true, + }, + }); + + const bucketActivityIds = userBucketInterested + .filter(u => u.isBucket) + .map(u => u.activityXid); + + const interestedActivityIds = userBucketInterested + .filter(u => !u.isBucket) + .map(u => u.activityXid); + + const excludedActivityIds = userBucketInterested.map( + u => u.activityXid, + ); + + const safeExcludedIds = + excludedActivityIds.length > 0 ? excludedActivityIds : [-1]; + /* ===================================================== CONNECTION INTEREST MAP ===================================================== */ @@ -1270,7 +1356,6 @@ export class UserService { where: { userXid: { in: connectionUserIds }, isActive: true, - isBucket: true, }, _count: { activityXid: true }, }); @@ -1302,6 +1387,9 @@ export class UserService { const otherInterestActivities = await tx.activities.findMany({ where: { isActive: true, + activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED, + amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED, + id: { notIn: safeExcludedIds }, ...excludeUserInterestCondition, }, skip, @@ -1388,7 +1476,16 @@ export class UserService { ).length; const hypedActivities = await tx.activities.findMany({ - where: { id: { in: mostHypedGrouped.map((h) => h.activityXid) } }, + where: { + id: { + in: mostHypedGrouped.map((h) => h.activityXid), + notIn: safeExcludedIds, + }, + activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED, + amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED, + isActive: true, + + }, select: { id: true, activityTitle: true, @@ -1427,7 +1524,10 @@ export class UserService { 5️⃣ NEW ARRIVALS ===================================================== */ const newArrivalsWhere = { + id: { notIn: safeExcludedIds }, isActive: true, + activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED, + amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED, createdAt: { gte: new Date(Date.now() - 31 * 24 * 60 * 60 * 1000) }, ...excludeUserInterestCondition, }; @@ -1457,6 +1557,9 @@ export class UserService { ===================================================== */ const otherStatesWhere: any = { isActive: true, + id: { notIn: safeExcludedIds }, + activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED, + amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED, ...excludeUserInterestCondition, }; if (effectiveCountryXid) @@ -1466,6 +1569,9 @@ export class UserService { const overseasWhere: any = { isActive: true, + id: { notIn: safeExcludedIds }, + activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED, + amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED, ...excludeUserInterestCondition, }; if (effectiveCountryXid) @@ -1513,6 +1619,8 @@ export class UserService { return { pagination: { page, limit }, interests: interestsWithActivities, + interestedCount: interestedActivityIds.length, + bucketCount: bucketActivityIds.length, mostHypedActivities: { page, @@ -3500,4 +3608,59 @@ export class UserService { }); } + async addToBucketInterested( + userXid: number, + isBucket: boolean, + bucketTypeName: string, + activityXid: number + ) { + const activityExists = await this.prisma.activities.findFirst({ + where: { id: activityXid, isActive: true }, + }); + + if (!activityExists) { + throw new ApiError(404, 'Activity not found'); + } + + const existing = await this.prisma.userBucketInterested.findFirst({ + where: { userXid, activityXid }, + }); + + if (existing) { + throw new ApiError(400, 'Activity already added'); + } + + await this.prisma.userBucketInterested.create({ + data: { + userXid, + activityXid, + isBucket, + bucketTypeName, + }, + }); + + // ✅ Get updated counts + const [bucketCount, interestedCount] = await Promise.all([ + this.prisma.userBucketInterested.count({ + where: { + userXid, + isBucket: true, + isActive: true, + }, + }), + this.prisma.userBucketInterested.count({ + where: { + userXid, + isBucket: false, + isActive: true, + }, + }), + ]); + + return { + bucketCount, + interestedCount, + }; + } + } \ No newline at end of file