diff --git a/serverless/functions/user.yml b/serverless/functions/user.yml index 3989e57..a2c56fd 100644 --- a/serverless/functions/user.yml +++ b/serverless/functions/user.yml @@ -376,4 +376,19 @@ removeActivityFromBucketInterested: events: - httpApi: path: /user/activities/remove-from-bucket-interested - method: post \ No newline at end of file + method: post + +getFilteredLandingPageAllDetails: + handler: src/modules/user/handlers/activities/filteredLandingPageAllDetails.handler + memorySize: 512 + package: + patterns: + - 'src/modules/user/**' + - ${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-filtered-landing-page-details + method: get \ No newline at end of file diff --git a/src/modules/user/handlers/activities/filteredLandingPageAllDetails.handler.ts b/src/modules/user/handlers/activities/filteredLandingPageAllDetails.handler.ts new file mode 100644 index 0000000..9ab9fec --- /dev/null +++ b/src/modules/user/handlers/activities/filteredLandingPageAllDetails.handler.ts @@ -0,0 +1,74 @@ +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 { FilteredLandingPageService } from '../../services/filteredLandingPage.service'; + +const filteredLandingPageService = new FilteredLandingPageService(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 ?? ''; + const userLat = event.queryStringParameters?.userLat ?? ''; + const userLong = event.queryStringParameters?.userLong ?? ''; + + let activityTypeXids: number[] | undefined; + if (event.queryStringParameters?.activityTypeXids) { + try { + activityTypeXids = JSON.parse(event.queryStringParameters.activityTypeXids); + } catch (error) { + // Handle invalid JSON if needed + } + } + + if (page < 1 || limit < 1) { + throw new ApiError(400, 'Invalid pagination values'); + } + + // Fetch filtered landing page details + const result = await filteredLandingPageService.getFilteredLandingPageAllDetails( + userId, + page, + limit, + countryName, + stateName, + cityName, + userLat, + userLong, + activityTypeXids + ); + + return { + statusCode: 200, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + body: JSON.stringify({ + success: true, + message: 'Filtered landing page data retrieved successfully', + data: result, + }), + }; +}); diff --git a/src/modules/user/services/filteredLandingPage.service.ts b/src/modules/user/services/filteredLandingPage.service.ts new file mode 100644 index 0000000..a5d71e6 --- /dev/null +++ b/src/modules/user/services/filteredLandingPage.service.ts @@ -0,0 +1,949 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaClient } from '@prisma/client'; +import { getPresignedUrl } from '../../../common/middlewares/aws/getPreSignedUrl'; +import { + ACTIVITY_AM_INTERNAL_STATUS, + ACTIVITY_INTERNAL_STATUS, +} from '../../../common/utils/constants/host.constant'; +import config from '../../../config/config'; + +const bucket = config.aws.bucketName; + +@Injectable() +export class FilteredLandingPageService { + constructor(private readonly prisma: PrismaClient) {} + + normalizeName = (name: string): string => { + return name + .toLowerCase() + .replace(/[^a-z0-9\s]/g, '') + .replace(/\s+/g, ' ') + .trim(); + }; + + attachPresignedUrl = async (key: string | null): Promise => { + if (!key) return null; + try { + return await getPresignedUrl(bucket, key); + } catch (error) { + console.error(`Failed to generate presigned URL for key: ${key}`, error); + return null; + } + }; + + findOrCreateLocation = async ( + countryName: string, + stateName: string, + cityName: string, + tx: any, + ) => { + const normalizedCountry = this.normalizeName(countryName); + const normalizedState = this.normalizeName(stateName); + const normalizedCity = this.normalizeName(cityName); + + let country = await tx.countries.findFirst({ + where: { + countryName: { contains: normalizedCountry, mode: 'insensitive' }, + }, + }); + + if (!country) { + country = await tx.countries.create({ + data: { + countryName: countryName.trim(), + isActive: true, + createdAt: new Date(), + updatedAt: new Date(), + }, + }); + } + + let state = await tx.states.findFirst({ + where: { + countryXid: country.id, + stateName: { contains: normalizedState, mode: 'insensitive' }, + }, + }); + + if (!state) { + state = await tx.states.create({ + data: { + countryXid: country.id, + stateName: stateName.trim(), + isActive: true, + createdAt: new Date(), + updatedAt: new Date(), + }, + }); + } + + let city = await tx.cities.findFirst({ + where: { + stateXid: state.id, + cityName: { contains: normalizedCity, mode: 'insensitive' }, + }, + }); + + if (!city) { + city = await tx.cities.create({ + data: { + stateXid: state.id, + cityName: cityName.trim(), + isActive: true, + createdAt: new Date(), + updatedAt: new Date(), + }, + }); + } + + return { + countryXid: country.id, + stateXid: state.id, + cityXid: city.id, + }; + }; + + attachMediaWithPresignedUrl = async (mediaArr = []) => { + return ( + await Promise.all( + mediaArr.map(async (m) => { + return { + ...m, + presignedUrl: await this.attachPresignedUrl(m.mediaFileName), + }; + }), + ) + ); + }; + + calculateDistance = ( + lat1: number | null, + lon1: number | null, + lat2: number | null, + lon2: number | null, + ) => { + if (!lat1 || !lon1 || !lat2 || !lon2) return null; + + const R = 6371; // km + const dLat = ((lat2 - lat1) * Math.PI) / 180; + const dLon = ((lon2 - lon1) * Math.PI) / 180; + + const a = + Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos((lat1 * Math.PI) / 180) * + Math.cos((lat2 * Math.PI) / 180) * + Math.sin(dLon / 2) * + Math.sin(dLon / 2); + + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + return R * c; + }; + + async rankAndPaginateActivities( + tx: any, + whereClause: any, + page: number, + limit: number, + connectionInterestMap: Map, + ) { + const skip = (page - 1) * limit; + + // Get total count + const totalCount = await tx.activities.count({ where: whereClause }); + + // Fetch activities with ranking metadata + const activities = await tx.activities.findMany({ + where: whereClause, + skip, + take: limit, + select: { + id: true, + activityTitle: true, + sustainabilityScore: true, + totalScore: true, + activityType: { + select: { + interestXid: true, + energyLevel: { + select: { + id: true, + energyLevelName: true, + energyColor: true, + energyIcon: true, + }, + }, + }, + }, + ActivitiesMedia: { + where: { isActive: true }, + select: { + id: true, + mediaFileName: true, + mediaType: true, + }, + }, + // Fetch ranking metadata + ItineraryActivities: { + select: { + ActivityFeedbacks: { + select: { activityStars: true }, + }, + }, + }, + ActivityVenues: { + select: { + ActivityPrices: { + select: { sellPrice: true }, + }, + }, + }, + }, + }); + + // Sort and format + const sortedActivities = activities + .map((act) => { + const feedbacks = act.ItineraryActivities.flatMap( + (ia) => ia.ActivityFeedbacks, + ); + const totalStars = feedbacks.reduce( + (sum, f) => sum + f.activityStars, + 0, + ); + const avgRating = + feedbacks.length > 0 ? totalStars / feedbacks.length : 0; + const prices = act.ActivityVenues.flatMap((v) => + v.ActivityPrices.map((p) => p.sellPrice), + ).filter((p) => p !== null) as number[]; + const minPrice = prices.length > 0 ? Math.min(...prices) : Infinity; + + return { + ...act, + avgRating, + minPrice, + sustainabilityScore: act.sustainabilityScore ?? 0, + totalScore: act.totalScore ?? 0, + }; + }) + .sort((a, b) => { + if (b.avgRating !== a.avgRating) return b.avgRating - a.avgRating; + if (a.minPrice !== b.minPrice) return a.minPrice - b.minPrice; + if (b.sustainabilityScore !== a.sustainabilityScore) + return b.sustainabilityScore - a.sustainabilityScore; + return b.totalScore - a.totalScore; + }); + + const formattedActivities = await Promise.all( + sortedActivities.map(async (activity) => ({ + interestXid: activity.activityType.interestXid, + activityId: activity.id, + connectionInterestedCount: + connectionInterestMap.get(activity.id) ?? 0, + activityTitle: activity.activityTitle, + sustainabilityScore: activity.sustainabilityScore, + cheapestPrice: activity.minPrice === Infinity ? null : activity.minPrice, + distance: 0, + rating: activity.avgRating, + energyLevel: activity.activityType.energyLevel + ? { + ...activity.activityType.energyLevel, + presignedUrl: await this.attachPresignedUrl( + activity.activityType.energyLevel.energyIcon, + ), + } + : null, + media: await this.attachMediaWithPresignedUrl(activity.ActivitiesMedia), + })), + ); + + return { + page, + limit, + totalCount, + hasMore: skip + limit < totalCount, + activities: formattedActivities, + }; + } + + async getFilteredLandingPageAllDetails( + userId: number, + page: number, + limit: number, + countryName: string, + stateName: string, + cityName: string, + userLat: string, + userLong: string, + activityTypeXids?: number[], + ) { + const data = await this.prisma.$transaction(async (tx) => { + const userAddressDetails = await tx.userAddressDetails.findFirst({ + where: { userXid: userId }, + select: { + id: true, + address1: true, + address2: true, + pinCode: true, + locationName: true, + stateXid: true, + cityXid: true, + countryXid: true, + locationLat: true, + locationLong: true, + }, + }); + + const userLatitude = userAddressDetails?.locationLat ?? null; + const userLongitude = userAddressDetails?.locationLong ?? null; + + let effectiveLocation: { + countryXid?: number | null; + stateXid?: number | null; + cityXid?: number | null; + } | null = null; + + const hasRequestLocation = countryName && stateName && cityName; + + if (hasRequestLocation) { + effectiveLocation = await this.findOrCreateLocation( + countryName!, + stateName!, + cityName!, + tx, + ); + } else if (userAddressDetails) { + effectiveLocation = { + countryXid: userAddressDetails.countryXid, + stateXid: userAddressDetails.stateXid, + cityXid: userAddressDetails.cityXid, + }; + } + + const effectiveCountryXid = effectiveLocation?.countryXid ?? null; + const effectiveStateXid = effectiveLocation?.stateXid ?? null; + + const userInterests = await tx.userInterests.findMany({ + where: { userXid: userId, isActive: true }, + select: { + id: true, + interestXid: true, + interest: { + select: { + id: true, + interestName: true, + interestColor: true, + interestImage: true, + displayOrder: true, + }, + }, + }, + }); + + if (!userInterests.length) { + return { + userAddressDetails, + interests: [], + activityTypes: [], + otherStatesActivities: null, + overSeasActivities: null, + }; + } + + // Get all activity types for user interests, filtered by selected activity types if provided + const activityTypeWhere: any = { + interestXid: { in: userInterests.map((ui) => ui.interestXid) }, + isActive: true, + }; + + if (activityTypeXids && activityTypeXids.length > 0) { + activityTypeWhere.id = { in: activityTypeXids }; + } + + const activityTypesWithInterests = await tx.activityTypes.findMany({ + where: activityTypeWhere, + select: { + id: true, + activityTypeName: true, + interestXid: true, + interests: { + select: { + id: true, + interestName: true, + interestColor: true, + interestImage: true, + displayOrder: true, + }, + }, + }, + }); + + if (!activityTypesWithInterests.length) { + return { + userAddressDetails, + interests: [], + activityTypes: [], + otherStatesActivities: null, + overSeasActivities: null, + }; + } + + 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: { + id: true, + schoolCompanyXid: true, + } + }) + + const otherConnectionUsers = await tx.connectDetails.findMany({ + where: { userXid: { notIn: [userId] }, isActive: true, schoolCompanyXid: { in: userConnectionDetails.map((u) => u.schoolCompanyXid) } }, + select: { + id: true, + userXid: true, + } + }) + + const connectionUserIds = + otherConnectionUsers.length > 0 + ? otherConnectionUsers.map(u => u.userXid) + : [-1]; + + const connectionInterestByActivity = await tx.userBucketInterested.groupBy({ + by: ['activityXid'], + where: { + userXid: { in: connectionUserIds }, + isActive: true, + }, + _count: { + activityXid: true, + }, + }); + + const connectionInterestMap = new Map( + connectionInterestByActivity.map(item => [ + item.activityXid, + item._count.activityXid, + ]) + ); + + const skip = (page - 1) * limit; + + // Group activity types by interest + const activityTypesByInterest = activityTypesWithInterests.reduce((acc, at) => { + if (!acc[at.interestXid]) { + acc[at.interestXid] = { + interest: at.interests, + activityTypes: [], + }; + } + acc[at.interestXid].activityTypes.push({ + activityTypeId: at.id, + activityTypeName: at.activityTypeName, + }); + return acc; + }, {} as any); + + // Fetch activities for each activity type + const activitiesByActivityType = await Promise.all( + activityTypesWithInterests.map(async (activityType) => { + const activities = await tx.activities.findMany({ + where: { + isActive: true, + activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED, + amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED, + activityTypeXid: activityType.id, + id: { + notIn: allUserExcludedActivityIds.length + ? allUserExcludedActivityIds + : [-1], + }, + }, + skip, + take: limit, + orderBy: { id: 'desc' }, + select: { + id: true, + activityTitle: true, + activityDurationMins: true, + sustainabilityScore: true, + checkInLat: true, + checkInLong: true, + activityType: { + select: { + id: true, + activityTypeName: true, + interestXid: true, + energyLevel: { + select: { + id: true, + energyLevelName: true, + energyColor: true, + energyIcon: true, + }, + }, + }, + }, + ActivityVenues: { + select: { + ActivityPrices: { + select: { + sellPrice: true, + }, + }, + }, + }, + ActivitiesMedia: { + where: { isActive: true }, + select: { + id: true, + mediaFileName: true, + mediaType: true, + }, + }, + }, + }); + + const formattedActivities = await Promise.all( + activities.map(async (activity) => { + const cheapestPrice = + activity.ActivityVenues.flatMap((v) => v.ActivityPrices) + .map((p) => p.sellPrice) + .filter(Boolean) + .sort((a, b) => a - b)[0] ?? null; + + const distance = this.calculateDistance( + userLatitude, + userLongitude, + activity.checkInLat, + activity.checkInLong, + ); + + return { + activityId: activity.id, + connectionInterestedCount: + connectionInterestMap.get(activity.id) ?? 0, + activityTitle: activity.activityTitle, + activityDurationMins: activity.activityDurationMins, + sustainabilityScore: activity.sustainabilityScore, + cheapestPrice, + distance, + rating: 0, + energyLevel: activity.activityType.energyLevel + ? { + ...activity.activityType.energyLevel, + presignedUrl: await this.attachPresignedUrl( + activity.activityType.energyLevel.energyIcon, + ), + } + : null, + media: await this.attachMediaWithPresignedUrl(activity.ActivitiesMedia), + }; + }), + ); + + return { + activityTypeId: activityType.id, + activityTypeName: activityType.activityTypeName, + interestXid: activityType.interestXid, + activities: formattedActivities, + pagination: { + page, + limit, + hasMore: formattedActivities.length === limit, + }, + }; + }), + ); + + // Group by interests for the final structure + const interestsWithActivityTypes = await Promise.all(Object.values(activityTypesByInterest).map( + async (interestGroup: any) => ({ + interestId: interestGroup.interest.id, + interestName: interestGroup.interest.interestName, + interestColor: interestGroup.interest.interestColor, + interestImage: interestGroup.interest.interestImage, + interestImagePresignedUrl: await this.attachPresignedUrl(interestGroup.interest.interestImage), + displayOrder: interestGroup.interest.displayOrder, + activityTypes: interestGroup.activityTypes.map((at: any) => { + const activityTypeData = activitiesByActivityType.find( + (adata) => adata.activityTypeId === at.activityTypeId + ); + return { + ...at, + activities: activityTypeData?.activities || [], + pagination: activityTypeData?.pagination || { page, limit, hasMore: false }, + }; + }), + }), + )); + + // Most Hyped Activities with filtering + const mostHypedGrouped = await tx.userBucketInterested.groupBy({ + by: ['activityXid'], + where: { + isActive: true, + isBucket: false, + }, + _count: { + activityXid: true, + }, + orderBy: { + _count: { + activityXid: 'desc', + }, + }, + }); + + // Filter most hyped activities by activity type if provided + let filteredMostHypedActivityIds = mostHypedGrouped.map((a) => a.activityXid); + + if (activityTypeXids && activityTypeXids.length > 0) { + const activitiesWithTypes = await tx.activities.findMany({ + where: { + id: { in: filteredMostHypedActivityIds }, + activityTypeXid: { in: activityTypeXids }, + }, + select: { id: true }, + }); + filteredMostHypedActivityIds = activitiesWithTypes.map(a => a.id); + } + + const finalMostHypedGrouped = mostHypedGrouped + .filter(group => filteredMostHypedActivityIds.includes(group.activityXid)) + .slice(skip, skip + limit); + + const totalHypedActivities = filteredMostHypedActivityIds.length; + const mostHypedActivityIds = finalMostHypedGrouped.map((a) => a.activityXid); + + const mostHypedActivitiesRaw = await tx.activities.findMany({ + where: { + id: { + in: mostHypedActivityIds, + notIn: allUserExcludedActivityIds.length + ? allUserExcludedActivityIds + : [-1], + }, + isActive: true, + activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED, + amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED, + }, + select: { + id: true, + activityTitle: true, + sustainabilityScore: true, + totalScore: true, + activityType: { + select: { + energyLevel: { + select: { + id: true, + energyLevelName: true, + energyColor: true, + energyIcon: true, + }, + }, + }, + }, + ActivitiesMedia: { + where: { isActive: true }, + select: { + id: true, + mediaFileName: true, + mediaType: true, + }, + }, + ItineraryActivities: { + select: { + ActivityFeedbacks: { + select: { activityStars: true }, + }, + }, + }, + ActivityVenues: { + select: { + ActivityPrices: { + select: { sellPrice: true }, + }, + }, + }, + }, + }); + + // Sort Most Hyped by the 4 criteria + const mostHypedSorted = mostHypedActivitiesRaw + .map((act) => { + const feedbacks = act.ItineraryActivities.flatMap( + (ia) => ia.ActivityFeedbacks, + ); + const totalStars = feedbacks.reduce( + (sum, f) => sum + f.activityStars, + 0, + ); + const avgRating = + feedbacks.length > 0 ? totalStars / feedbacks.length : 0; + const prices = act.ActivityVenues.flatMap((v) => + v.ActivityPrices.map((p) => p.sellPrice), + ).filter((p) => p !== null) as number[]; + const minPrice = prices.length > 0 ? Math.min(...prices) : Infinity; + + return { + ...act, + avgRating, + minPrice, + sustainabilityScore: act.sustainabilityScore ?? 0, + totalScore: act.totalScore ?? 0, + hypeCount: + finalMostHypedGrouped.find((g) => g.activityXid === act.id)?._count + .activityXid ?? 0, + }; + }) + .sort((a, b) => { + if (b.avgRating !== a.avgRating) return b.avgRating - a.avgRating; + if (a.minPrice !== b.minPrice) return a.minPrice - b.minPrice; + if (b.sustainabilityScore !== a.sustainabilityScore) + return b.sustainabilityScore - a.sustainabilityScore; + return b.totalScore - a.totalScore; + }); + + const mostHypedActivities = await Promise.all( + mostHypedSorted.map(async (activity) => ({ + activityId: activity.id, + activityTitle: activity.activityTitle, + connectionInterestedCount: + connectionInterestMap.get(activity.id) ?? 0, + hypeCount: activity.hypeCount, + distance: 0, + rating: 0, + energyLevel: activity.activityType.energyLevel + ? { + ...activity.activityType.energyLevel, + presignedUrl: await this.attachPresignedUrl( + activity.activityType.energyLevel.energyIcon, + ), + } + : null, + media: await this.attachMediaWithPresignedUrl(activity.ActivitiesMedia), + })), + ); + + const formattedMostHypedActivities = { + page, + limit, + totalCount: totalHypedActivities, + hasMore: skip + limit < totalHypedActivities, + activities: mostHypedActivities, + }; + + // New Arrivals with filtering + const newArrivalsWhere: any = { + isActive: true, + activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED, + amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED, + id: { + notIn: allUserExcludedActivityIds.length + ? allUserExcludedActivityIds + : [-1], + }, + createdAt: { gte: new Date(Date.now() - 31 * 24 * 60 * 60 * 1000) }, + }; + + if (activityTypeXids && activityTypeXids.length > 0) { + newArrivalsWhere.activityTypeXid = { in: activityTypeXids }; + } + + const formattedNewArrivalsActivities = await this.rankAndPaginateActivities( + tx, + newArrivalsWhere, + page, + limit, + connectionInterestMap + ); + + // Other States Activities with filtering + const otherStatesWhere: any = { + isActive: true, + activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED, + amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED, + id: { + notIn: allUserExcludedActivityIds.length + ? allUserExcludedActivityIds + : [-1], + }, + }; + + if (effectiveCountryXid) { + otherStatesWhere.checkInCountryXid = effectiveCountryXid; + } + if (effectiveStateXid) { + otherStatesWhere.checkInStateXid = { not: effectiveStateXid }; + } + if (activityTypeXids && activityTypeXids.length > 0) { + otherStatesWhere.activityTypeXid = { in: activityTypeXids }; + } + + const formattedOtherStatesActivities = await this.rankAndPaginateActivities( + tx, + otherStatesWhere, + page, + limit, + connectionInterestMap + ); + + // Random Activities with filtering + const totalActiveCount = await tx.activities.count({ + where: { + isActive: true, + activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED, + amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED, + deletedAt: null, + id: { + notIn: allUserExcludedActivityIds.length + ? allUserExcludedActivityIds + : [-1], + }, + ...(activityTypeXids && activityTypeXids.length > 0 && { + activityTypeXid: { in: activityTypeXids }, + }), + }, + }); + + let randomActivities: any[] = []; + + if (totalActiveCount > 0) { + const takeCount = Math.min(5, totalActiveCount); + + const randomOffsets = new Set(); + while (randomOffsets.size < takeCount) { + randomOffsets.add(Math.floor(Math.random() * totalActiveCount)); + } + + const randomFetched = await Promise.all( + Array.from(randomOffsets).map((offset) => + tx.activities.findFirst({ + skip: offset, + where: { + isActive: true, + activityInternalStatus: + ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED, + amInternalStatus: + ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED, + deletedAt: null, + id: { + notIn: allUserExcludedActivityIds.length + ? allUserExcludedActivityIds + : [-1], + }, + ...(activityTypeXids && activityTypeXids.length > 0 && { + activityTypeXid: { in: activityTypeXids }, + }), + }, + select: { + id: true, + activityTitle: true, + ActivitiesMedia: { + where: { isActive: true, isCoverImage: true }, + orderBy: { displayOrder: 'asc' }, + take: 1, + select: { + mediaFileName: true, + }, + }, + }, + }), + ), + ); + + randomActivities = await Promise.all( + randomFetched + .filter(Boolean) + .map(async (activity) => { + const cover = activity!.ActivitiesMedia?.[0]; + + return { + activityId: activity!.id, + activityTitle: activity!.activityTitle, + coverImage: cover?.mediaFileName ?? null, + coverImagePresignedUrl: cover?.mediaFileName + ? await this.attachPresignedUrl(cover.mediaFileName) + : null, + }; + }), + ); + } + + // Overseas Activities with filtering + const overseasWhere: any = { + isActive: true, + activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED, + amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED, + id: { + notIn: allUserExcludedActivityIds.length + ? allUserExcludedActivityIds + : [-1], + }, + }; + + if (effectiveCountryXid) { + overseasWhere.checkInCountryXid = { not: effectiveCountryXid }; + } + if (activityTypeXids && activityTypeXids.length > 0) { + overseasWhere.activityTypeXid = { in: activityTypeXids }; + } + + const formattedOverSeasActivities = await this.rankAndPaginateActivities( + tx, + overseasWhere, + page, + limit, + connectionInterestMap + ); + + return { + userAddressDetails, + experiencesLogged: 0, + citiesDiscovered: 0, + loggedInNetworkCount: 0, + citiesInNetworkCount: 0, + rating: 0, + interestedCount: userInterestedActivityIds.length, + bucketCount: userBucketActivityIds.length, + pagination: { + page, + limit, + }, + randomActivities, + interests: interestsWithActivityTypes, + activityTypes: activitiesByActivityType, + otherStatesActivities: formattedOtherStatesActivities, + overSeasActivities: formattedOverSeasActivities, + newArrivalsActivities: formattedNewArrivalsActivities, + mostHypedActivities: formattedMostHypedActivities, + }; + }); + + return data; + } +}