import { Injectable } from '@nestjs/common'; import { PrismaClient, User, UserAddressDetails } from '@prisma/client'; import * as bcrypt from 'bcryptjs'; import { getPresignedUrl } from '../../../common/middlewares/aws/getPreSignedUrl'; import { ACTIVITY_AM_INTERNAL_STATUS, ACTIVITY_INTERNAL_STATUS } from '../../../common/utils/constants/host.constant'; import ApiError from '../../../common/utils/helper/ApiError'; import { UserPersonalInfoSchema } from '../../../common/utils/validation/user/addPersonalInfo.validation'; import config from '@/config/config'; // function deg2rad(deg) { // return deg * (Math.PI / 180); // } // function getDistanceFromLatLon(userLat1, userLon1, activityLat2, activityLon2) { // const R = 6371; // Earth radius in km // const dLat = deg2rad(activityLat2 - userLat1); // const dLon = deg2rad(activityLon2 - userLon1); // const a = // Math.sin(dLat / 2) * Math.sin(dLat / 2) + // Math.cos(deg2rad(userLat1)) * // Math.cos(deg2rad(activityLat2)) * // Math.sin(dLon / 2) * // Math.sin(dLon / 2); // const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); // return R * c; // } async function findOrCreateLocation( tx: any, { countryName, stateName, cityName, }: { countryName: string; stateName: string; cityName: string; } ) { /* --------------------------- 1️⃣ COUNTRY ----------------------------*/ let country = await tx.countries.findFirst({ where: { countryName: { equals: countryName, mode: 'insensitive', }, isActive: true, }, }); if (!country) { country = await tx.countries.create({ data: { countryName: countryName.trim(), countryCode: countryName.slice(0, 3).toUpperCase(), // optional countryFlag: '', isActive: true, }, }); } /* --------------------------- 2️⃣ STATE ----------------------------*/ let state = await tx.states.findFirst({ where: { stateName: { equals: stateName, mode: 'insensitive', }, countryXid: country.id, isActive: true, }, }); if (!state) { state = await tx.states.create({ data: { stateName: stateName.trim(), countryXid: country.id, isActive: true, }, }); } /* --------------------------- 3️⃣ CITY ----------------------------*/ let city = await tx.cities.findFirst({ where: { cityName: { equals: cityName, mode: 'insensitive', }, stateXid: state.id, isActive: true, }, }); if (!city) { city = await tx.cities.create({ data: { cityName: cityName.trim(), stateXid: state.id, isActive: true, }, }); } return { countryXid: country.id, stateXid: state.id, cityXid: city.id, }; } const attachMediaWithPresignedUrl = async (mediaArr = []) => { return ( await Promise.all( mediaArr.map(async (m) => { if (!m?.mediaFileName) return null; const key = m.mediaFileName.startsWith('http') ? new URL(m.mediaFileName).pathname.replace(/^\/+/, '') : m.mediaFileName; return { id: m.id, mediaType: m.mediaType, mediaFileName: m.mediaFileName, presignedUrl: await getPresignedUrl(bucket, key), }; }) ) ).filter(Boolean); }; const bucket = config.aws.bucketName; /* ===================================================== HELPER: RANK & PAGINATE ACTIVITIES ===================================================== */ async function rankAndPaginateActivities( tx: any, whereClause: any, page: number, limit: number ) { const skip = (page - 1) * limit; // 1️⃣ Fetch Metadata for ALL matching activities for in-memory sorting const allCandidates = await tx.activities.findMany({ where: whereClause, select: { id: true, sustainabilityScore: true, totalScore: true, // Quality Score ItineraryActivities: { select: { ActivityFeedbacks: { select: { activityStars: true }, }, }, }, ActivityVenues: { select: { ActivityPrices: { select: { sellPrice: true }, }, }, }, }, }); const totalCount = allCandidates.length; // 2️⃣ Calculate Metrics & Sort const sortedCandidates = allCandidates.map((act: any) => { // Flatten feedbacks const feedbacks = act.ItineraryActivities.flatMap((ia: any) => ia.ActivityFeedbacks); // Avg Rating const totalStars = feedbacks.reduce((sum: number, f: any) => sum + f.activityStars, 0); const avgRating = feedbacks.length > 0 ? totalStars / feedbacks.length : 0; // Min Price const prices = act.ActivityVenues.flatMap((v: any) => v.ActivityPrices.map((p: any) => p.sellPrice)).filter((p: any) => p !== null) as number[]; const minPrice = prices.length > 0 ? Math.min(...prices) : Infinity; return { id: act.id, avgRating, minPrice, sustainabilityScore: act.sustainabilityScore ?? 0, totalScore: act.totalScore ?? 0, }; }).sort((a: any, b: any) => { // 1. Rating (Highest first) if (b.avgRating !== a.avgRating) return b.avgRating - a.avgRating; // 2. Price (Lowest first) if (a.minPrice !== b.minPrice) return a.minPrice - b.minPrice; // 3. Sustainability Score (Highest first) if (b.sustainabilityScore !== a.sustainabilityScore) return b.sustainabilityScore - a.sustainabilityScore; // 4. Quality Score (Highest first) return b.totalScore - a.totalScore; }); // 3️⃣ Paginate IDs const paginatedCandidates = sortedCandidates.slice(skip, skip + limit); const targetIds = paginatedCandidates.map((c: any) => c.id); // 4️⃣ Fetch Full Details for the page const activitiesUnsorted = await tx.activities.findMany({ where: { id: { in: targetIds } }, select: { id: true, activityTitle: true, activityDurationMins: true, sustainabilityScore: true, activityType: { select: { 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, }, }, }, }); // Re-sort to match the calculated order const activities = targetIds .map((id: number) => activitiesUnsorted.find((a: any) => a.id === id)) .filter(Boolean); // 5️⃣ Format Response const formattedActivities = await Promise.all( activities.map(async (activity: any) => { const prices = activity.ActivityVenues.flatMap((v: any) => v.ActivityPrices.map((p: any) => p.sellPrice)).filter((p: any) => p !== null) as number[]; const cheapestPrice = prices.length > 0 ? Math.min(...prices) : null; return { // interestXid: activity.activityType.interestXid, activityId: activity.id, activityTitle: activity.activityTitle, // activityDurationMins: activity.activityDurationMins, // sustainabilityScore: activity.sustainabilityScore, // cheapestPrice, energyLevel: activity.activityType.energyLevel, media: await attachMediaWithPresignedUrl(activity.ActivitiesMedia), }; }) ); return { page, limit, totalCount, hasMore: skip + limit < totalCount, activities: formattedActivities, }; } @Injectable() export class UserService { constructor(private prisma: PrismaClient) { } async getUserById(userId: number) { return this.prisma.user.findUnique({ where: { id: userId, isActive: true }, }); } async addPersonalInfo(userId: number, data: UserPersonalInfoSchema) { return await this.prisma.$transaction(async (tx) => { const updatedUser = await tx.user.update({ where: { id: userId }, data: { firstName: data.firstName, lastName: data.lastName ?? null, genderName: data.genderName, dateOfBirth: data.dateOfBirth ? new Date(data.dateOfBirth) : null, isProfileUpdated: true, }, }); return updatedUser; }); } async getAllInterestDetails() { const interests = await this.prisma.interests.findMany({ where: { isActive: true }, select: { id: true, interestName: true, interestColor: true, interestImage: true, displayOrder: true } }) for (const interest of interests) { if (interest.interestImage) { const key = interest.interestImage.startsWith('http') ? new URL(interest.interestImage).pathname.replace(/^\/+/, '') : interest.interestImage; (interest as any).presignedUrl = await getPresignedUrl(bucket, key); } else { (interest as any).presignedUrl = null; } } return interests; } async getUserByMobileNumber(mobileNumber: string): Promise { return this.prisma.user.findFirst({ where: { mobileNumber: mobileNumber, isActive: true }, }); } async verifyHostOtp(mobileNumber: string, otp: string): Promise { const user = await this.prisma.user.findFirst({ where: { mobileNumber: mobileNumber, isActive: true }, select: { id: true, mobileNumber: true, UserOtp: { where: { isActive: true, isVerified: false }, orderBy: { createdAt: 'desc' }, take: 1, }, }, }); if (!user) { throw new ApiError(404, 'User not found.'); } const userOtp = user.UserOtp[0]; if (!userOtp) { throw new ApiError(400, 'No OTP found.'); } if (new Date() > userOtp.expiresOn) { throw new ApiError(400, 'OTP has expired.'); } const isMatch = await bcrypt.compare(otp, userOtp.otpCode); if (!isMatch) { throw new ApiError(400, 'Invalid OTP.'); } await this.prisma.userOtp.update({ where: { id: userOtp.id }, data: { isVerified: true, verifiedOn: new Date(), isActive: false, }, }); return true; } async setUserPasscode(userId: number, userPasscode: string): Promise { // Validate passcode format (6 digits) if (!userPasscode || userPasscode.length !== 6 || !/^\d{6}$/.test(userPasscode)) { throw new ApiError(400, 'Passcode must be exactly 6 digits'); } // Hash the passcode const hashedPasscode = await bcrypt.hash(userPasscode, 10); // Update user with passcode const updatedUser = await this.prisma.user.update({ where: { id: userId }, data: { userPasscode: hashedPasscode, }, }); if (!updatedUser) { throw new ApiError(400, 'Failed to set passcode'); } return updatedUser; } async verifyUserPasscode(userId: number, passcode: string): Promise { const user = await this.prisma.user.findUnique({ where: { id: userId, isActive: true }, select: { userPasscode: true }, }); if (!user || !user.userPasscode) { throw new ApiError(404, 'User passcode not found'); } const isMatch = await bcrypt.compare(passcode, user.userPasscode); if (!isMatch) { return false; } return true; } async setUserInterests(userId: number, interest_Xid: number[]): Promise { // Remove existing interests await this.prisma.userInterests.deleteMany({ where: { userXid: userId }, }); // Add new interests const interestRecords = interest_Xid.map((interestId) => ({ userXid: userId, interestXid: interestId, })); await this.prisma.userInterests.createMany({ data: interestRecords, }); } async setUserLocationDetails( userId: number, countryName: string, stateName: string, cityName: string, pinCode: string, latitude?: number, longitude?: number, locationName?: string, locationAddress?: string ): Promise { return this.prisma.$transaction(async (tx) => { // 1️⃣ Country: find or create let country = await tx.countries.findUnique({ where: { countryName }, select: { id: true }, }); if (!country) { country = await tx.countries.create({ data: { countryName, countryCode: countryName.slice(0, 3).toUpperCase(), countryFlag: '', }, select: { id: true }, }); } // 2️⃣ State: find or create (GLOBAL UNIQUE) let state = await tx.states.findFirst({ where: { stateName, countryXid: country.id }, select: { id: true }, }); if (!state) { state = await tx.states.create({ data: { stateName, countryXid: country.id, }, select: { id: true }, }); } // 3️⃣ City: find or create (GLOBAL UNIQUE) let city = await tx.cities.findFirst({ where: { cityName, stateXid: state.id }, select: { id: true }, }); if (!city) { city = await tx.cities.create({ data: { cityName, stateXid: state.id, }, select: { id: true }, }); } return tx.userAddressDetails.create({ data: { user: { connect: { id: userId } }, country: { connect: { id: country.id } }, states: { connect: { id: state.id } }, cities: { connect: { id: city.id } }, address1: locationAddress ?? '', pinCode, locationName: locationName ?? null, locationAddress: locationAddress ?? null, locationLat: latitude ?? null, locationLong: longitude ?? null, }, }); }); } async getLandingPageAllDetails( userId: number, page: number, limit: number, countryName: string, stateName: string, cityName: string, userLat: string, userLong: string ) { 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, } }) let effectiveLocation: { countryXid?: number | null; stateXid?: number | null; cityXid?: number | null; } | null = null; const hasRequestLocation = countryName && stateName && cityName; if (hasRequestLocation) { // ✅ Create/find ONLY if request location is sent effectiveLocation = await findOrCreateLocation(tx, { countryName: countryName!, stateName: stateName!, cityName: cityName!, }); } else if (userAddressDetails) { // ✅ Fallback to user’s saved address 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: [], otherStatesActivities: null, overSeasActivities: null, }; } const activitiyTypesOfUserInterests = await tx.activityTypes.findMany({ where: { interestXid: { in: userInterests.map(ui => ui.interestXid) }, isActive: true }, select: { id: true } }) if (!activitiyTypesOfUserInterests.length) { return { userAddressDetails, interests: [], otherStatesActivities: null, overSeasActivities: null, }; } const skip = (page - 1) * limit; /* ===================================================== 1️⃣ FETCH ALL CANDIDATES FOR INTERESTS (SIMPLE SORT) ===================================================== */ // Reverted to simple ID based sorting for Interest-based activities const activities = await tx.activities.findMany({ where: { isActive: true, activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED, amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED, activityTypeXid: { in: activitiyTypesOfUserInterests.map(at => at.id), }, }, skip, take: limit, orderBy: { id: 'desc' }, select: { id: true, activityTitle: true, activityDurationMins: true, sustainabilityScore: true, checkInLat: true, checkInLong: true, activityType: { select: { 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 mostHypedTotalCount = await tx.userBucketInterested.groupBy({ by: ['activityXid'], where: { isActive: true, isBucket: false, }, }); /* ===================================================== 2️⃣ MOST HYPED ACTIVITIES (RANKED) ===================================================== */ const mostHypedGrouped = await tx.userBucketInterested.groupBy({ by: ['activityXid'], where: { isActive: true, isBucket: false }, _count: { activityXid: true, }, orderBy: { _count: { activityXid: 'desc', }, }, skip, take: limit, }); const totalHypedActivities = mostHypedTotalCount.length; const mostHypedActivityIds = mostHypedGrouped.map(a => a.activityXid); // Fetch metadata for ranking only for the top hyped ones (optimization: double sorting might be needed if we want to sort BY rating WITHIN the hyped list, but usually Hyped = Count. // IF user wants the standard 4-step ranking applied TO the most hyped items: const mostHypedActivitiesRaw = await tx.activities.findMany({ where: { id: { in: mostHypedActivityIds }, 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, }, }, // Fetch ranking metadata 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, // Keep original fields for final output avgRating, minPrice, sustainabilityScore: act.sustainabilityScore ?? 0, totalScore: act.totalScore ?? 0, hypeCount: mostHypedGrouped.find(g => g.activityXid === act.id)?._count.activityXid ?? 0 }; }).sort((a, b) => { // 1. Rating (Highest first) if (b.avgRating !== a.avgRating) return b.avgRating - a.avgRating; // 2. Price (Lowest first) if (a.minPrice !== b.minPrice) return a.minPrice - b.minPrice; // 3. Sustainability Score if (b.sustainabilityScore !== a.sustainabilityScore) return b.sustainabilityScore - a.sustainabilityScore; // 4. Quality Score return b.totalScore - a.totalScore; }); const mostHypedActivities = await Promise.all( mostHypedSorted.map(async activity => ({ activityId: activity.id, activityTitle: activity.activityTitle, hypeCount: activity.hypeCount, energyLevel: activity.activityType.energyLevel, media: await attachMediaWithPresignedUrl(activity.ActivitiesMedia), })) ); const formattedMostHypedActivities = { page, limit, totalCount: totalHypedActivities, hasMore: skip + limit < totalHypedActivities, activities: mostHypedActivities, }; /* ===================================================== 3️⃣ NEW ARRIVALS (RANKED) ===================================================== */ const newArrivalsWhere = { 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) } }; const formattedNewArrivalsActivities = await rankAndPaginateActivities(tx, newArrivalsWhere, page, limit); /* ===================================================== 4️⃣ OTHER STATES ACTIVITIES (RANKED) ===================================================== */ const otherStatesWhere: any = { isActive: true, activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED, amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED, }; if (effectiveCountryXid) { otherStatesWhere.checkInCountryXid = effectiveCountryXid; } if (effectiveStateXid) { otherStatesWhere.checkInStateXid = { not: effectiveStateXid }; } const formattedOtherStatesActivities = await rankAndPaginateActivities(tx, otherStatesWhere, page, limit); /* ===================================================== 5️⃣ OVERSEAS ACTIVITIES (RANKED) ===================================================== */ const overseasWhere: any = { isActive: true, activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED, amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED, }; if (effectiveCountryXid) { overseasWhere.checkInCountryXid = { not: effectiveCountryXid }; } const formattedOverSeasActivities = await rankAndPaginateActivities(tx, overseasWhere, page, limit); 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; return { interestXid: activity.activityType.interestXid, activityId: activity.id, activityTitle: activity.activityTitle, activityDurationMins: activity.activityDurationMins, sustainabilityScore: activity.sustainabilityScore, cheapestPrice, energyLevel: activity.activityType.energyLevel, media: await attachMediaWithPresignedUrl(activity.ActivitiesMedia), }; }) ); const interestsWithActivities = [...userInterests] .sort((a, b) => a.interest.interestName.localeCompare(b.interest.interestName) ) .map(ui => ({ interestId: ui.interest.id, interestName: ui.interest.interestName, interestColor: ui.interest.interestColor, interestImage: ui.interest.interestImage, displayOrder: ui.interest.displayOrder, page, limit, hasMore: formattedActivities.length === limit, activities: formattedActivities .filter(a => a.interestXid === ui.interestXid) .map(({ interestXid, ...rest }) => rest), })); return { userAddressDetails, experiencesLogged: 25, citiesDiscovered: 10, loggedInNetworkCount: 0, citiesInNetworkCount: 0, pagination: { page, limit, }, interests: interestsWithActivities, otherStatesActivities: formattedOtherStatesActivities, overSeasActivities: formattedOverSeasActivities, newArrivalsActivities: formattedNewArrivalsActivities, mostHypedActivities: formattedMostHypedActivities, }; }) 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) { return await this.prisma.$transaction(async (tx) => { const activity = await tx.activities.findUnique({ where: { id: activityXid, isActive: true, activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED, amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED }, select: { id: true, activityTitle: true, activityDurationMins: true, sustainabilityScore: true, checkInLat: true, checkInLong: true, activityRefNumber: true, checkInAddress: true, checkOutAddress: true, checkOutLat: true, checkOutLong: true, activityDescription: true, foodAvailable: true, foodIsChargeable: true, alcoholAvailable: true, trainerAvailable: true, trainerIsChargeable: true, pickUpDropAvailable: true, pickUpDropIsChargeable: true, inActivityAvailable: true, inActivityIsChargeable: true, isLateCheckingAllowed: true, equipmentAvailable: true, equipmentIsChargeable: true, cancellationAvailable: true, cancellationAllowedBeforeMins: true, activityType: { select: { interestXid: true, // ✅ VERY IMPORTANT activityTypeName: true, energyLevel: { select: { id: true, energyLevelName: true, energyColor: true, energyIcon: true, }, }, }, }, ActivityOtherDetails: { where: { isActive: true }, select: { id: true, exclusiveNotes: true, dosNotes: true, dontsNotes: true, tipsNotes: true, termsAndCondition: true } }, ActivityEligibility: { where: { isActive: true }, select: { id: true, isAgeRestriction: true, ageRestrictionName: true, ageEntered: true, ageIn: true, minAge: true, maxAge: true, isWeightRestriction: true, weightRestrictionName: true, weightEntered: true, weightIn: true, minWeight: true, maxWeight: true, isHeightRestriction: true, heightRestrictionName: true, heightEntered: true, heightIn: true, minHeight: true, maxHeight: true } }, ActivityTrainers: { where: { isActive: true }, select: { id: true, totalAmount: true } }, ActivityAllowedEntry: { where: { isActive: true }, select: { id: true, allowedEntryTypeXid: true, allowedEntryType: { select: { id: true, allowedEntryTypeName: true } } } }, ActivityFoodCost: { where: { isActive: true }, select: { id: true, totalAmount: true } }, activityFoodTypes: { where: { isActive: true }, select: { id: true, foodTypeXid: true, foodType: { select: { id: true, foodTypeName: true } } } }, ActivityEquipments: { where: { isActive: true }, select: { id: true, equipmentName: true, isEquipmentChargeable: true, equipmentTotalPrice: true } }, ActivityNavigationModes: { where: { isActive: true }, select: { id: true, navigationModeXid: true, navigationMode: { select: { id: true, navigationModeName: true, navigationModeIcon: true } }, isInActivityChargeable: true, navigationModesTotalPrice: true } }, ActivityAmenities: { where: { isActive: true }, select: { id: true, amenitiesXid: true, amenities: { select: { id: true, amenitiesName: true } } } }, ActivityPickUpDetails: { where: { isActive: true }, select: { id: true, isPickUp: true, locationLat: true, locationLong: true, locationAddress: true, transportTotalPrice: true } }, activityCuisines: { where: { isActive: true }, select: { id: true, foodCuisineXid: true, foodCuisine: { select: { id: true, cuisineName: true } } } }, ActivityVenues: { where: { isActive: true }, select: { id: true, venueName: true, venueLabel: true, venueCapacity: true, availableSeats: true, isMinPeopleReqMandatory: true, minPeopleRequired: true, minReqfullfilledBeforeMins: true, venueDescription: true, ActivityPrices: { select: { id: true, sellPrice: true, }, }, }, }, ActivitiesMedia: { where: { isActive: true }, select: { id: true, mediaFileName: true, mediaType: true, }, }, }, }) const interestedCount = await tx.userBucketInterested.count({ where: { activityXid, isActive: true, }, }) return { activity, interestedCount, rating: 0, // ⭐ Placeholder, implement rating logic as needed distance: 0 } }) } }