diff --git a/serverless/functions/user.yml b/serverless/functions/user.yml index 6fda7ae..f7aff48 100644 --- a/serverless/functions/user.yml +++ b/serverless/functions/user.yml @@ -107,6 +107,21 @@ getLandingPageDetails: path: /user/activities/get-landing-page-details method: get +getSurpriseMePageDetails: + handler: src/modules/user/handlers/activities/surpriseMePage.handler + memorySize: 384 + package: + patterns: + - 'src/modules/user/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/get-surprise-me-page-details + method: get + getActivityDetailsById: handler: src/modules/user/handlers/activities/getByIdActivityDetails.handler memorySize: 384 diff --git a/src/modules/user/handlers/activities/surpriseMePage.ts b/src/modules/user/handlers/activities/surpriseMePage.ts new file mode 100644 index 0000000..1dc96e6 --- /dev/null +++ b/src/modules/user/handlers/activities/surpriseMePage.ts @@ -0,0 +1,61 @@ +import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda'; +import { safeHandler } from '../../../../common/utils/handlers/safeHandler'; +import { prismaClient } from '../../../../common/database/prisma.lambda.service'; +import ApiError from '../../../../common/utils/helper/ApiError'; +import { UserService } from '../../services/user.service'; +import { verifyUserToken } from '../../../../common/middlewares/jwt/authForUser'; + +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.'); + } + + // Verify token and get user info + const userInfo = await verifyUserToken(token); + const userId = Number(userInfo.id); + + if (!userId || isNaN(userId)) { + throw new ApiError(400, 'Invalid user ID'); + } + + const page = Number(event.queryStringParameters?.page ?? 1); + const limit = Number(event.queryStringParameters?.limit ?? 20); + const countryName = event.queryStringParameters?.countryName ?? ''; + const stateName = event.queryStringParameters?.stateName ?? ''; + const cityName = event.queryStringParameters?.cityName ?? ''; + + if (page < 1 || limit < 1) { + throw new ApiError(400, 'Invalid pagination values'); + } + + // Fetch user with their HostHeader stepper info + const result = await userService.getSurpriseMeDetails( + userId, + page, + limit, + countryName, + stateName, + cityName, + ); + + return { + statusCode: 200, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + body: JSON.stringify({ + success: true, + message: 'Data retrieved successfully', + data: result, + }), + }; +}); + diff --git a/src/modules/user/services/user.service.ts b/src/modules/user/services/user.service.ts index 819fb5f..a0811ac 100644 --- a/src/modules/user/services/user.service.ts +++ b/src/modules/user/services/user.service.ts @@ -851,6 +851,335 @@ export class UserService { return data; } + async getSurpriseMeDetails( + userId: number, + page: number, + limit: number, + countryName: string, + stateName: string, + cityName: string + ) { + const data = await this.prisma.$transaction(async (tx) => { + + /* ===================================================== + 1️⃣ USER LOCATION + ===================================================== */ + const userAddressDetails = await tx.userAddressDetails.findFirst({ + where: { userXid: userId }, + select: { + stateXid: true, + cityXid: true, + countryXid: true, + }, + }); + + let effectiveLocation: { + countryXid?: number | null; + stateXid?: number | null; + cityXid?: number | null; + } | null = null; + + if (countryName && stateName && cityName) { + effectiveLocation = await findOrCreateLocation(tx, { + countryName, + stateName, + cityName, + }); + } else if (userAddressDetails) { + effectiveLocation = { + countryXid: userAddressDetails.countryXid, + stateXid: userAddressDetails.stateXid, + cityXid: userAddressDetails.cityXid, + }; + } + + const effectiveCountryXid = effectiveLocation?.countryXid ?? null; + const effectiveStateXid = effectiveLocation?.stateXid ?? null; + + /* ===================================================== + 2️⃣ USER INTERESTS (TO EXCLUDE) + ===================================================== */ + const userInterests = await tx.userInterests.findMany({ + where: { userXid: userId, isActive: true }, + select: { interestXid: true }, + }); + + const userInterestTypeIds = await tx.activityTypes.findMany({ + where: { + interestXid: { in: userInterests.map(i => i.interestXid) }, + isActive: true, + }, + select: { id: true }, + }); + + const excludedActivityTypeIds = userInterestTypeIds.map(a => a.id); + + const excludeUserInterestCondition = + excludedActivityTypeIds.length > 0 + ? { activityTypeXid: { notIn: excludedActivityTypeIds } } + : {}; + + const skip = (page - 1) * limit; + + /* ===================================================== + 3️⃣ OTHER INTERESTS (GROUPED WITH ACTIVITIES) + ===================================================== */ + const otherInterests = await tx.interests.findMany({ + where: { + isActive: true, + id: { notIn: userInterests.map(i => i.interestXid) }, + }, + orderBy: { interestName: 'asc' }, + select: { + id: true, + interestName: true, + interestColor: true, + interestImage: true, + }, + }); + + const otherInterestActivities = await tx.activities.findMany({ + where: { + isActive: true, + ...excludeUserInterestCondition, + }, + skip, + take: limit, + orderBy: { id: 'desc' }, + select: { + id: true, + activityTitle: true, + activityType: { + select: { + interestXid: true, + energyLevel: true, + }, + }, + ActivitiesMedia: { + where: { isActive: true }, + select: { id: true, mediaFileName: true, mediaType: true }, + }, + }, + }); + + const formattedOtherInterestActivities = await Promise.all( + otherInterestActivities.map(async a => ({ + interestXid: a.activityType.interestXid, + activityId: a.id, + activityTitle: a.activityTitle, + energyLevel: a.activityType.energyLevel, + media: await attachMediaWithPresignedUrl(a.ActivitiesMedia), + })) + ); + + const interestsWithActivities = otherInterests.map(interest => ({ + interestId: interest.id, + interestName: interest.interestName, + interestColor: interest.interestColor, + interestImage: interest.interestImage, + page, + limit, + hasMore: formattedOtherInterestActivities.length === limit, + activities: formattedOtherInterestActivities.filter( + a => a.interestXid === interest.id + ).map(({ interestXid, ...rest }) => rest), + })); + + /* ===================================================== + 4️⃣ MOST HYPED + ===================================================== */ + const mostHypedGrouped = await tx.userBucketInterested.groupBy({ + by: ['activityXid'], + where: { + isActive: true, + isBucket: false, + Activities: excludeUserInterestCondition, + }, + _count: { activityXid: true }, + orderBy: { _count: { activityXid: 'desc' } }, + skip, + take: limit, + }); + + const totalHypedCount = ( + await tx.userBucketInterested.groupBy({ + by: ['activityXid'], + where: { + isActive: true, + isBucket: false, + Activities: excludeUserInterestCondition, + }, + }) + ).length; + + const hypedActivities = await tx.activities.findMany({ + where: { id: { in: mostHypedGrouped.map(h => h.activityXid) } }, + select: { + id: true, + activityTitle: true, + activityType: { select: { energyLevel: true } }, + ActivitiesMedia: { + where: { isActive: true }, + select: { id: true, mediaFileName: true, mediaType: true }, + }, + }, + }); + + const mostHypedActivities = await Promise.all( + mostHypedGrouped.map(async g => { + const act = hypedActivities.find(a => a.id === g.activityXid); + if (!act) return null; + return { + activityId: act.id, + activityTitle: act.activityTitle, + hypeCount: g._count.activityXid, + energyLevel: act.activityType.energyLevel, + media: await attachMediaWithPresignedUrl(act.ActivitiesMedia), + }; + }) + ).then(a => a.filter(Boolean)); + + /* ===================================================== + 5️⃣ NEW ARRIVALS + ===================================================== */ + const newArrivalsWhere = { + isActive: true, + createdAt: { gte: new Date(Date.now() - 31 * 24 * 60 * 60 * 1000) }, + ...excludeUserInterestCondition, + }; + + const newArrivalsCount = await tx.activities.count({ + where: newArrivalsWhere, + }); + + const newArrivalsRaw = await tx.activities.findMany({ + where: newArrivalsWhere, + skip, + take: limit, + orderBy: { id: 'desc' }, + select: { + activityTitle: true, + activityType: { select: { energyLevel: true } }, + ActivitiesMedia: { + where: { isActive: true }, + select: { id: true, mediaFileName: true, mediaType: true }, + }, + }, + }); + + /* ===================================================== + 6️⃣ OTHER STATES & OVERSEAS + ===================================================== */ + const otherStatesWhere: any = { + isActive: true, + ...excludeUserInterestCondition, + }; + if (effectiveCountryXid) otherStatesWhere.checkInCountryXid = effectiveCountryXid; + if (effectiveStateXid) otherStatesWhere.checkInStateXid = { not: effectiveStateXid }; + + const overseasWhere: any = { + isActive: true, + ...excludeUserInterestCondition, + }; + if (effectiveCountryXid) overseasWhere.checkInCountryXid = { not: effectiveCountryXid }; + + const [otherStatesCount, overseasCount] = await Promise.all([ + tx.activities.count({ where: otherStatesWhere }), + tx.activities.count({ where: overseasWhere }), + ]); + + const [otherStatesRaw, overseasRaw] = await Promise.all([ + tx.activities.findMany({ + where: otherStatesWhere, + skip, + take: limit, + select: { + activityTitle: true, + activityType: { select: { energyLevel: true } }, + ActivitiesMedia: { + where: { isActive: true }, + select: { id: true, mediaFileName: true, mediaType: true }, + }, + }, + }), + tx.activities.findMany({ + where: overseasWhere, + skip, + take: limit, + select: { + activityTitle: true, + activityType: { select: { energyLevel: true } }, + ActivitiesMedia: { + where: { isActive: true }, + select: { id: true, mediaFileName: true, mediaType: true }, + }, + }, + }), + ]); + + /* ===================================================== + 7️⃣ FINAL RESPONSE + ===================================================== */ + return { + pagination: { page, limit }, + interests: interestsWithActivities, + + mostHypedActivities: { + page, + limit, + totalCount: totalHypedCount, + hasMore: skip + limit < totalHypedCount, + activities: mostHypedActivities, + }, + + newArrivalsActivities: { + page, + limit, + totalCount: newArrivalsCount, + hasMore: skip + limit < newArrivalsCount, + activities: await Promise.all( + newArrivalsRaw.map(async a => ({ + activityTitle: a.activityTitle, + energyLevel: a.activityType.energyLevel, + media: await attachMediaWithPresignedUrl(a.ActivitiesMedia), + })) + ), + }, + + otherStatesActivities: { + page, + limit, + totalCount: otherStatesCount, + hasMore: skip + limit < otherStatesCount, + activities: await Promise.all( + otherStatesRaw.map(async a => ({ + activityTitle: a.activityTitle, + energyLevel: a.activityType.energyLevel, + media: await attachMediaWithPresignedUrl(a.ActivitiesMedia), + })) + ), + }, + + overSeasActivities: { + page, + limit, + totalCount: overseasCount, + hasMore: skip + limit < overseasCount, + activities: await Promise.all( + overseasRaw.map(async a => ({ + activityTitle: a.activityTitle, + energyLevel: a.activityType.energyLevel, + media: await attachMediaWithPresignedUrl(a.ActivitiesMedia), + })) + ), + }, + }; + }); + + return data; + } + + async getActivityDetailsById( userId: number, activityXid: number) {