2026-01-16 17:50:30 +05:30
import { Injectable } from '@nestjs/common' ;
2026-02-02 17:09:42 +05:30
import { PrismaClient , User , UserAddressDetails } from '@prisma/client' ;
2026-01-23 17:56:46 +05:30
import * as bcrypt from 'bcryptjs' ;
2026-02-04 15:32:11 +05:30
import { getPresignedUrl } from '../../../common/middlewares/aws/getPreSignedUrl' ;
2026-02-19 15:55:18 +05:30
import {
ACTIVITY_AM_INTERNAL_STATUS ,
ACTIVITY_INTERNAL_STATUS ,
} from '../../../common/utils/constants/host.constant' ;
2026-02-09 15:50:56 +05:30
import ApiError from '../../../common/utils/helper/ApiError' ;
import { UserPersonalInfoSchema } from '../../../common/utils/validation/user/addPersonalInfo.validation' ;
2026-02-19 16:25:43 +05:30
import { AddSchoolCompanyDetailDTO } from '../dto/addSchoolCompanyDetail.dto' ;
2026-02-04 15:32:11 +05:30
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;
// }
2026-03-05 18:57:58 +05:30
const 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 Number ( ( R * c ) . toFixed ( 2 ) ) ;
} ;
2026-02-19 17:11:29 +05:30
const normalizeName = ( name : string ) = >
name . trim ( ) . toLowerCase ( ) . replace ( /\s+/g , " " ) ;
2026-02-17 11:48:06 +05:30
const attachPresignedUrl = async ( file : string | null | undefined ) = > {
2026-02-19 15:55:18 +05:30
if ( ! file ) return null ;
2026-02-17 11:48:06 +05:30
2026-02-19 15:55:18 +05:30
const key = file . startsWith ( 'http' )
? new URL ( file ) . pathname . replace ( /^\/+/ , '' )
: file ;
2026-02-17 11:48:06 +05:30
2026-02-19 15:55:18 +05:30
return await getPresignedUrl ( bucket , key ) ;
2026-02-17 11:48:06 +05:30
} ;
2026-02-06 15:55:20 +05:30
async function findOrCreateLocation (
2026-02-19 15:55:18 +05:30
tx : any ,
{
countryName ,
stateName ,
cityName ,
} : {
countryName : string ;
stateName : string ;
cityName : string ;
} ,
2026-02-06 15:55:20 +05:30
) {
2026-02-19 15:55:18 +05:30
/ * - - - - - - - - - - - - - - - - - - - - - - - - - - -
2026-02-06 15:55:20 +05:30
1 ️ ⃣ COUNTRY
-- -- -- -- -- -- -- -- -- -- -- -- -- -- * /
2026-02-19 15:55:18 +05:30
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 ,
} ,
2026-02-06 15:55:20 +05:30
} ) ;
2026-02-19 15:55:18 +05:30
}
2026-02-06 15:55:20 +05:30
2026-02-19 15:55:18 +05:30
/ * - - - - - - - - - - - - - - - - - - - - - - - - - - -
2026-02-06 15:55:20 +05:30
2 ️ ⃣ STATE
-- -- -- -- -- -- -- -- -- -- -- -- -- -- * /
2026-02-19 15:55:18 +05:30
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 ,
} ,
2026-02-06 15:55:20 +05:30
} ) ;
2026-02-19 15:55:18 +05:30
}
2026-02-06 15:55:20 +05:30
2026-02-19 15:55:18 +05:30
/ * - - - - - - - - - - - - - - - - - - - - - - - - - - -
2026-02-06 15:55:20 +05:30
3 ️ ⃣ CITY
-- -- -- -- -- -- -- -- -- -- -- -- -- -- * /
2026-02-19 15:55:18 +05:30
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 ,
} ,
2026-02-06 15:55:20 +05:30
} ) ;
2026-02-19 15:55:18 +05:30
}
2026-02-06 15:55:20 +05:30
2026-02-19 15:55:18 +05:30
return {
countryXid : country.id ,
stateXid : state.id ,
cityXid : city.id ,
} ;
2026-02-06 15:55:20 +05:30
}
2026-02-05 16:07:43 +05:30
const attachMediaWithPresignedUrl = async ( mediaArr = [ ] ) = > {
2026-02-19 15:55:18 +05:30
return (
await Promise . all (
mediaArr . map ( async ( m ) = > {
if ( ! m ? . mediaFileName ) return null ;
2026-02-05 16:07:43 +05:30
2026-02-19 15:55:18 +05:30
const key = m . mediaFileName . startsWith ( 'http' )
? new URL ( m . mediaFileName ) . pathname . replace ( /^\/+/ , '' )
: m . mediaFileName ;
2026-02-05 16:07:43 +05:30
2026-02-19 15:55:18 +05:30
return {
id : m.id ,
mediaType : m.mediaType ,
mediaFileName : m.mediaFileName ,
presignedUrl : await getPresignedUrl ( bucket , key ) ,
} ;
} ) ,
)
) . filter ( Boolean ) ;
} ;
2026-02-04 15:32:11 +05:30
2026-02-23 20:00:50 +05:30
function deg2rad ( deg : number ) : number {
return deg * ( Math . PI / 180 ) ;
}
function getDistanceFromLatLon (
userLat1 : number ,
userLon1 : number ,
activityLat2 : number ,
activityLon2 : number ,
) : number {
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 ;
}
2026-02-04 15:32:11 +05:30
const bucket = config . aws . bucketName ;
2026-02-11 18:49:28 +05:30
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
HELPER : RANK & PAGINATE ACTIVITIES
=== === === === === === === === === === === === === === === === === == * /
async function rankAndPaginateActivities (
2026-02-19 15:55:18 +05:30
tx : any ,
whereClause : any ,
page : number ,
limit : number ,
2026-02-27 17:24:39 +05:30
connectionInterestMap
2026-02-11 18:49:28 +05:30
) {
2026-02-19 15:55:18 +05:30
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 : {
2026-02-11 18:49:28 +05:30
select : {
2026-02-19 15:55:18 +05:30
ActivityFeedbacks : {
select : { activityStars : true } ,
} ,
2026-02-11 18:49:28 +05:30
} ,
2026-02-19 15:55:18 +05:30
} ,
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 ;
2026-02-11 18:49:28 +05:30
} ) ;
2026-02-19 15:55:18 +05:30
// 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 : {
2026-02-11 18:49:28 +05:30
select : {
2026-02-19 15:55:18 +05:30
interestXid : true ,
energyLevel : {
select : {
id : true ,
energyLevelName : true ,
energyColor : true ,
energyIcon : true ,
2026-02-11 18:49:28 +05:30
} ,
2026-02-19 15:55:18 +05:30
} ,
2026-02-11 18:49:28 +05:30
} ,
2026-02-19 15:55:18 +05:30
} ,
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 ,
2026-02-28 11:54:32 +05:30
distance : 0 ,
rating : 0 ,
2026-02-19 15:55:18 +05:30
// activityDurationMins: activity.activityDurationMins,
// sustainabilityScore: activity.sustainabilityScore,
// cheapestPrice,
2026-02-27 17:24:39 +05:30
connectionInterestedCount :
connectionInterestMap . get ( activity . id ) ? ? 0 ,
2026-02-19 15:55:18 +05:30
energyLevel : activity.activityType.energyLevel
? {
2026-02-19 20:05:33 +05:30
. . . activity . activityType . energyLevel ,
presignedUrl : await attachPresignedUrl (
activity . activityType . energyLevel . energyIcon ,
) ,
}
2026-02-19 15:55:18 +05:30
: null ,
media : await attachMediaWithPresignedUrl ( activity . ActivitiesMedia ) ,
} ;
} ) ,
) ;
return {
page ,
limit ,
totalCount ,
hasMore : skip + limit < totalCount ,
activities : formattedActivities ,
} ;
2026-02-11 18:49:28 +05:30
}
2026-01-16 17:50:30 +05:30
@Injectable ( )
export class UserService {
2026-02-19 20:05:33 +05:30
constructor ( private prisma : PrismaClient ) { }
2026-01-16 17:50:30 +05:30
2026-02-19 15:55:18 +05:30
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 ,
} ,
} ) ;
2026-01-16 17:50:30 +05:30
2026-02-19 15:55:18 +05:30
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 ,
} ,
} ) ;
2026-01-16 17:50:30 +05:30
2026-03-09 18:49:40 +05:30
const totalActivityCount = await this . prisma . activities . count ( {
where : {
isActive : true ,
activityInternalStatus : ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED ,
amInternalStatus : ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED ,
}
} )
2026-02-19 15:55:18 +05:30
for ( const interest of interests ) {
if ( interest . interestImage ) {
const key = interest . interestImage . startsWith ( 'http' )
? new URL ( interest . interestImage ) . pathname . replace ( /^\/+/ , '' )
: interest . interestImage ;
2026-02-01 10:02:35 +05:30
2026-02-19 15:55:18 +05:30
( interest as any ) . presignedUrl = await getPresignedUrl ( bucket , key ) ;
} else {
( interest as any ) . presignedUrl = null ;
}
}
2026-02-05 16:07:43 +05:30
2026-03-09 18:49:40 +05:30
return { interests , totalActivityCount } ;
2026-02-19 15:55:18 +05:30
}
2026-02-05 16:07:43 +05:30
2026-02-19 15:55:18 +05:30
async getUserByMobileNumber ( mobileNumber : string ) : Promise < User | null > {
return this . prisma . user . findFirst ( {
where : { mobileNumber : mobileNumber , isActive : true } ,
} ) ;
}
async verifyHostOtp ( mobileNumber : string , otp : string ) : Promise < boolean > {
2026-02-25 15:56:18 +05:30
const trimmedOtp = ( otp || '' ) . toString ( ) . trim ( ) ;
2026-02-19 15:55:18 +05:30
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 ,
} ,
} ,
} ) ;
2026-02-05 16:07:43 +05:30
2026-02-19 15:55:18 +05:30
if ( ! user ) {
throw new ApiError ( 404 , 'User not found.' ) ;
2026-01-23 17:56:46 +05:30
}
2026-02-19 15:55:18 +05:30
const userOtp = user . UserOtp [ 0 ] ;
2026-02-01 10:02:35 +05:30
2026-02-19 15:55:18 +05:30
if ( ! userOtp ) {
throw new ApiError ( 400 , 'No OTP found.' ) ;
2026-01-23 17:56:46 +05:30
}
2026-02-19 15:55:18 +05:30
if ( new Date ( ) > userOtp . expiresOn ) {
throw new ApiError ( 400 , 'OTP has expired.' ) ;
}
2026-01-23 17:56:46 +05:30
2026-02-25 15:56:18 +05:30
const isMatch = await bcrypt . compare ( trimmedOtp , userOtp . otpCode ) ;
2026-01-23 17:56:46 +05:30
2026-02-19 15:55:18 +05:30
if ( ! isMatch ) {
throw new ApiError ( 400 , 'Invalid OTP.' ) ;
}
2026-01-23 17:56:46 +05:30
2026-02-19 15:55:18 +05:30
await this . prisma . userOtp . update ( {
where : { id : userOtp.id } ,
data : {
isVerified : true ,
verifiedOn : new Date ( ) ,
isActive : false ,
} ,
} ) ;
2026-01-23 17:56:46 +05:30
2026-02-19 15:55:18 +05:30
return true ;
}
2026-01-23 17:56:46 +05:30
2026-02-19 15:55:18 +05:30
async setUserPasscode ( userId : number , userPasscode : string ) : Promise < User > {
// Validate passcode format (6 digits)
if (
! userPasscode ||
userPasscode . length !== 6 ||
! /^\d{6}$/ . test ( userPasscode )
) {
throw new ApiError ( 400 , 'Passcode must be exactly 6 digits' ) ;
}
2026-01-23 17:56:46 +05:30
2026-02-19 15:55:18 +05:30
// Hash the passcode
const hashedPasscode = await bcrypt . hash ( userPasscode , 10 ) ;
2026-01-23 17:56:46 +05:30
2026-02-19 15:55:18 +05:30
// Update user with passcode
const updatedUser = await this . prisma . user . update ( {
where : { id : userId } ,
data : {
userPasscode : hashedPasscode ,
} ,
} ) ;
2026-01-23 17:56:46 +05:30
2026-02-19 15:55:18 +05:30
if ( ! updatedUser ) {
throw new ApiError ( 400 , 'Failed to set passcode' ) ;
2026-01-23 17:56:46 +05:30
}
2026-01-30 15:30:45 +05:30
2026-02-19 15:55:18 +05:30
return updatedUser ;
}
2026-01-30 15:30:45 +05:30
2026-02-19 15:55:18 +05:30
async verifyUserPasscode ( userId : number , passcode : string ) : Promise < boolean > {
const user = await this . prisma . user . findUnique ( {
where : { id : userId , isActive : true } ,
select : { userPasscode : true } ,
} ) ;
2026-01-30 15:30:45 +05:30
2026-02-19 15:55:18 +05:30
if ( ! user || ! user . userPasscode ) {
throw new ApiError ( 404 , 'User passcode not found' ) ;
2026-01-30 15:30:45 +05:30
}
2026-02-02 14:50:23 +05:30
2026-02-19 15:55:18 +05:30
const isMatch = await bcrypt . compare ( passcode , user . userPasscode ) ;
2026-02-10 15:14:11 +05:30
2026-02-19 15:55:18 +05:30
if ( ! isMatch ) {
return false ;
}
2026-02-10 15:14:11 +05:30
2026-02-19 15:55:18 +05:30
return true ;
}
2026-02-10 15:14:11 +05:30
2026-02-19 15:55:18 +05:30
async setUserInterests (
userId : number ,
interest_Xid : number [ ] ,
) : Promise < void > {
// Remove existing interests
await this . prisma . userInterests . deleteMany ( {
where : { userXid : userId } ,
} ) ;
2026-02-10 15:14:11 +05:30
2026-02-19 15:55:18 +05:30
// Add new interests
const interestRecords = interest_Xid . map ( ( interestId ) = > ( {
userXid : userId ,
interestXid : interestId ,
} ) ) ;
2026-02-10 15:14:11 +05:30
2026-02-19 15:55:18 +05:30
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 < UserAddressDetails > {
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 } ,
2026-02-02 14:50:23 +05:30
} ) ;
2026-02-19 15:55:18 +05:30
}
2026-02-02 14:50:23 +05:30
2026-02-19 15:55:18 +05:30
// 2️ ⃣ State: find or create (GLOBAL UNIQUE)
let state = await tx . states . findFirst ( {
where : { stateName , countryXid : country.id } ,
select : { id : true } ,
} ) ;
2026-02-02 14:50:23 +05:30
2026-02-19 15:55:18 +05:30
if ( ! state ) {
state = await tx . states . create ( {
data : {
stateName ,
countryXid : country.id ,
} ,
select : { id : true } ,
2026-02-02 14:50:23 +05:30
} ) ;
2026-02-19 15:55:18 +05:30
}
2026-02-02 14:50:23 +05:30
2026-02-19 15:55:18 +05:30
// 3️ ⃣ City: find or create (GLOBAL UNIQUE)
let city = await tx . cities . findFirst ( {
where : { cityName , stateXid : state.id } ,
select : { id : true } ,
} ) ;
2026-02-04 15:32:11 +05:30
2026-02-19 15:55:18 +05:30
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 ,
} ,
} ) ;
} ) ;
}
2026-02-04 15:32:11 +05:30
2026-02-19 15:55:18 +05:30
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 ,
} ,
} ) ;
2026-03-05 18:57:58 +05:30
const userLatitude = userAddressDetails ? . locationLat ? ? null ;
const userLongitude = userAddressDetails ? . locationLong ? ? null ;
2026-02-19 15:55:18 +05:30
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 ! ,
2026-02-02 17:09:42 +05:30
} ) ;
2026-02-19 15:55:18 +05:30
} else if ( userAddressDetails ) {
// ✅ Fallback to user’ s saved address
effectiveLocation = {
countryXid : userAddressDetails.countryXid ,
stateXid : userAddressDetails.stateXid ,
cityXid : userAddressDetails.cityXid ,
} ;
}
2026-02-04 15:32:11 +05:30
2026-02-19 15:55:18 +05:30
const effectiveCountryXid = effectiveLocation ? . countryXid ? ? null ;
const effectiveStateXid = effectiveLocation ? . stateXid ? ? null ;
2026-02-06 15:55:20 +05:30
2026-02-19 15:55:18 +05:30
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 ,
} ,
} ,
} ,
} ) ;
2026-02-06 15:55:20 +05:30
2026-02-19 15:55:18 +05:30
if ( ! userInterests . length ) {
return {
userAddressDetails ,
interests : [ ] ,
otherStatesActivities : null ,
overSeasActivities : null ,
} ;
}
2026-02-05 16:07:43 +05:30
2026-02-19 15:55:18 +05:30
const activitiyTypesOfUserInterests = await tx . activityTypes . findMany ( {
where : {
interestXid : { in : userInterests . map ( ( ui ) = > ui . interestXid ) } ,
isActive : true ,
} ,
select : {
id : true ,
} ,
} ) ;
2026-02-04 15:32:11 +05:30
2026-02-19 15:55:18 +05:30
if ( ! activitiyTypesOfUserInterests . length ) {
return {
userAddressDetails ,
interests : [ ] ,
otherStatesActivities : null ,
overSeasActivities : null ,
} ;
}
2026-02-04 15:32:11 +05:30
2026-03-06 19:59:31 +05:30
const userInterestActivityTypeIds =
activitiyTypesOfUserInterests . map ( ( a ) = > a . id ) ;
2026-02-28 13:11:26 +05:30
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 ,
) ;
2026-03-10 21:30:43 +05:30
const latestUserActivity = await tx . userBucketInterested . findFirst ( {
where : {
userXid : userId ,
isActive : true ,
} ,
orderBy : {
createdAt : 'desc' ,
} ,
select : {
activityXid : true ,
} ,
} ) ;
let latestCoverImage : string | null = null ;
let latestCoverImagePresignedUrl : string | null = null ;
if ( latestUserActivity ) {
const latestActivityImage = await tx . activities . findFirst ( {
where : {
id : latestUserActivity.activityXid ,
isActive : true ,
activityInternalStatus : ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED ,
amInternalStatus : ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED ,
} ,
select : {
ActivitiesMedia : {
where : {
isCoverImage : true ,
isActive : true ,
} ,
select : {
mediaFileName : true ,
} ,
take : 1 ,
} ,
} ,
} ) ;
latestCoverImage =
latestActivityImage ? . ActivitiesMedia ? . [ 0 ] ? . mediaFileName ? ? null ;
latestCoverImagePresignedUrl = latestCoverImage
? await attachPresignedUrl ( latestCoverImage )
: null ;
}
2026-02-27 17:24:39 +05:30
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 ] ; // impossible user id
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 ,
] )
) ;
2026-02-19 15:55:18 +05:30
const skip = ( page - 1 ) * limit ;
2026-02-04 15:32:11 +05:30
2026-02-19 15:55:18 +05:30
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
2026-02-11 18:49:28 +05:30
1 ️ ⃣ FETCH ALL CANDIDATES FOR INTERESTS ( SIMPLE SORT )
=== === === === === === === === === === === === === === === === === == * /
2026-02-19 15:55:18 +05:30
// 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 : {
2026-03-06 19:59:31 +05:30
in : userInterestActivityTypeIds
2026-02-19 15:55:18 +05:30
} ,
2026-02-28 13:11:26 +05:30
id : {
notIn : allUserExcludedActivityIds.length
? allUserExcludedActivityIds
: [ - 1 ] , // prevent empty notIn issue
} ,
2026-02-19 15:55:18 +05:30
} ,
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 ,
2026-02-04 15:32:11 +05:30
} ,
2026-02-19 15:55:18 +05:30
} ,
} ,
} ,
ActivityVenues : {
select : {
ActivityPrices : {
2026-02-04 15:32:11 +05:30
select : {
2026-02-19 15:55:18 +05:30
sellPrice : true ,
2026-02-04 15:32:11 +05:30
} ,
2026-02-19 15:55:18 +05:30
} ,
} ,
} ,
ActivitiesMedia : {
where : { isActive : true } ,
select : {
id : true ,
mediaFileName : true ,
mediaType : true ,
} ,
} ,
} ,
} ) ;
2026-02-04 15:32:11 +05:30
2026-02-19 15:55:18 +05:30
const mostHypedTotalCount = await tx . userBucketInterested . groupBy ( {
by : [ 'activityXid' ] ,
where : {
isActive : true ,
isBucket : false ,
2026-03-06 19:59:31 +05:30
activityXid : {
notIn : allUserExcludedActivityIds.length
? allUserExcludedActivityIds
: [ - 1 ] ,
} ,
Activities : {
activityTypeXid : { in : userInterestActivityTypeIds } ,
isActive : true ,
activityInternalStatus : ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED ,
amInternalStatus : ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED ,
}
2026-02-19 15:55:18 +05:30
} ,
} ) ;
2026-02-05 16:07:43 +05:30
2026-02-19 15:55:18 +05:30
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
2026-02-11 18:49:28 +05:30
2 ️ ⃣ MOST HYPED ACTIVITIES ( RANKED )
=== === === === === === === === === === === === === === === === === == * /
2026-02-19 15:55:18 +05:30
const mostHypedGrouped = await tx . userBucketInterested . groupBy ( {
by : [ 'activityXid' ] ,
where : {
isActive : true ,
isBucket : false ,
2026-03-06 19:59:31 +05:30
activityXid : {
notIn : allUserExcludedActivityIds.length ? allUserExcludedActivityIds : [ - 1 ] ,
} ,
2026-02-19 15:55:18 +05:30
} ,
_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 : {
2026-02-28 13:11:26 +05:30
id : {
in : mostHypedActivityIds ,
notIn : allUserExcludedActivityIds.length
? allUserExcludedActivityIds
: [ - 1 ] ,
} ,
2026-03-06 19:59:31 +05:30
activityTypeXid : { in : userInterestActivityTypeIds } ,
2026-02-19 15:55:18 +05:30
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 : {
2026-02-05 16:07:43 +05:30
select : {
2026-02-19 15:55:18 +05:30
id : true ,
energyLevelName : true ,
energyColor : true ,
energyIcon : true ,
2026-02-05 16:07:43 +05:30
} ,
2026-02-19 15:55:18 +05:30
} ,
} ,
} ,
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 ;
} ) ;
2026-02-06 15:55:20 +05:30
2026-02-19 15:55:18 +05:30
const mostHypedActivities = await Promise . all (
mostHypedSorted . map ( async ( activity ) = > ( {
activityId : activity.id ,
activityTitle : activity.activityTitle ,
2026-02-27 17:24:39 +05:30
connectionInterestedCount :
connectionInterestMap . get ( activity . id ) ? ? 0 ,
2026-02-19 15:55:18 +05:30
hypeCount : activity.hypeCount ,
2026-02-28 11:54:32 +05:30
distance : 0 ,
rating : 0 ,
2026-02-19 15:55:18 +05:30
energyLevel : activity.activityType.energyLevel
? {
2026-02-19 20:05:33 +05:30
. . . activity . activityType . energyLevel ,
presignedUrl : await attachPresignedUrl (
activity . activityType . energyLevel . energyIcon ,
) ,
}
2026-02-19 15:55:18 +05:30
: null ,
media : await attachMediaWithPresignedUrl ( activity . ActivitiesMedia ) ,
} ) ) ,
) ;
const formattedMostHypedActivities = {
page ,
limit ,
totalCount : totalHypedActivities ,
hasMore : skip + limit < totalHypedActivities ,
activities : mostHypedActivities ,
} ;
2026-02-06 15:55:20 +05:30
2026-02-19 15:55:18 +05:30
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
2026-02-11 18:49:28 +05:30
3 ️ ⃣ NEW ARRIVALS ( RANKED )
=== === === === === === === === === === === === === === === === === == * /
2026-02-19 15:55:18 +05:30
const newArrivalsWhere = {
isActive : true ,
activityInternalStatus : ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED ,
amInternalStatus : ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED ,
2026-03-06 19:59:31 +05:30
activityTypeXid : { in : userInterestActivityTypeIds } ,
2026-02-28 13:11:26 +05:30
id : {
notIn : allUserExcludedActivityIds.length
? allUserExcludedActivityIds
2026-03-06 19:59:31 +05:30
: [ - 1 ] ,
2026-02-28 13:11:26 +05:30
} ,
2026-02-19 15:55:18 +05:30
createdAt : { gte : new Date ( Date . now ( ) - 31 * 24 * 60 * 60 * 1000 ) } ,
} ;
const formattedNewArrivalsActivities = await rankAndPaginateActivities (
tx ,
newArrivalsWhere ,
page ,
limit ,
2026-02-27 17:24:39 +05:30
connectionInterestMap
2026-02-19 15:55:18 +05:30
) ;
2026-02-04 15:32:11 +05:30
2026-02-19 15:55:18 +05:30
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
2026-02-11 18:49:28 +05:30
4 ️ ⃣ OTHER STATES ACTIVITIES ( RANKED )
=== === === === === === === === === === === === === === === === === == * /
2026-02-19 15:55:18 +05:30
const otherStatesWhere : any = {
isActive : true ,
activityInternalStatus : ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED ,
amInternalStatus : ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED ,
2026-03-06 19:59:31 +05:30
activityTypeXid : { in : userInterestActivityTypeIds } ,
2026-02-28 13:11:26 +05:30
id : {
notIn : allUserExcludedActivityIds.length
? allUserExcludedActivityIds
: [ - 1 ] , // prevent empty notIn issue
} ,
2026-02-19 15:55:18 +05:30
} ;
if ( effectiveCountryXid ) {
otherStatesWhere . checkInCountryXid = effectiveCountryXid ;
}
if ( effectiveStateXid ) {
otherStatesWhere . checkInStateXid = { not : effectiveStateXid } ;
}
const formattedOtherStatesActivities = await rankAndPaginateActivities (
tx ,
otherStatesWhere ,
page ,
limit ,
2026-02-27 17:24:39 +05:30
connectionInterestMap
2026-02-19 15:55:18 +05:30
) ;
2026-02-27 17:24:39 +05:30
// =====================================================
// 6️ ⃣ RANDOM ACTIVITIES (5 ONLY - SIMPLE)
// =====================================================
2026-03-25 13:34:12 +05:30
let randomActivities : any [ ] = [ ] ;
const eligibleRandomActivityIds = await tx . activities . findMany ( {
2026-02-27 17:24:39 +05:30
where : {
isActive : true ,
activityInternalStatus : ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED ,
amInternalStatus : ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED ,
deletedAt : null ,
2026-02-28 13:11:26 +05:30
id : {
notIn : allUserExcludedActivityIds.length
? allUserExcludedActivityIds
2026-03-25 13:34:12 +05:30
: [ - 1 ] ,
2026-02-28 13:11:26 +05:30
} ,
2026-03-25 13:34:12 +05:30
ActivitiesMedia : {
some : {
isActive : true ,
isCoverImage : true ,
} ,
} ,
} ,
select : {
id : true ,
2026-02-27 17:24:39 +05:30
} ,
} ) ;
2026-03-25 13:34:12 +05:30
if ( eligibleRandomActivityIds . length > 0 ) {
const takeCount = Math . min ( 5 , eligibleRandomActivityIds . length ) ;
const selectedIds = eligibleRandomActivityIds
. sort ( ( ) = > Math . random ( ) - 0.5 )
. slice ( 0 , takeCount )
. map ( ( activity ) = > activity . id ) ;
2026-02-27 17:24:39 +05:30
2026-03-25 13:34:12 +05:30
const randomFetched = await tx . activities . findMany ( {
where : {
id : { in : selectedIds } ,
} ,
select : {
id : true ,
activityTitle : true ,
ActivitiesMedia : {
where : { isActive : true , isCoverImage : true } ,
orderBy : { displayOrder : 'asc' } ,
take : 1 ,
2026-02-27 17:24:39 +05:30
select : {
2026-03-25 13:34:12 +05:30
mediaFileName : true ,
2026-02-27 17:24:39 +05:30
} ,
2026-03-25 13:34:12 +05:30
} ,
} ,
} ) ;
2026-02-27 17:24:39 +05:30
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 attachPresignedUrl ( cover . mediaFileName )
: null ,
} ;
} ) ,
) ;
}
2026-02-06 15:55:20 +05:30
2026-02-19 15:55:18 +05:30
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
2026-02-11 18:49:28 +05:30
5 ️ ⃣ OVERSEAS ACTIVITIES ( RANKED )
2026-02-06 15:55:20 +05:30
=== === === === === === === === === === === === === === === === === == * /
2026-02-19 15:55:18 +05:30
const overseasWhere : any = {
isActive : true ,
activityInternalStatus : ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED ,
amInternalStatus : ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED ,
2026-03-06 19:59:31 +05:30
activityTypeXid : { in : userInterestActivityTypeIds } ,
2026-02-28 13:11:26 +05:30
id : {
notIn : allUserExcludedActivityIds.length
? allUserExcludedActivityIds
: [ - 1 ] , // prevent empty notIn issue
} ,
2026-02-19 15:55:18 +05:30
} ;
if ( effectiveCountryXid ) {
overseasWhere . checkInCountryXid = { not : effectiveCountryXid } ;
}
const formattedOverSeasActivities = await rankAndPaginateActivities (
tx ,
overseasWhere ,
page ,
limit ,
2026-02-27 17:24:39 +05:30
connectionInterestMap
2026-02-19 15:55:18 +05:30
) ;
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 ;
2026-03-05 18:57:58 +05:30
const distance = calculateDistance (
userLatitude ,
userLongitude ,
activity . checkInLat ,
activity . checkInLong ,
) ;
2026-02-19 15:55:18 +05:30
return {
interestXid : activity.activityType.interestXid ,
activityId : activity.id ,
2026-02-27 17:24:39 +05:30
connectionInterestedCount :
connectionInterestMap . get ( activity . id ) ? ? 0 ,
2026-02-19 15:55:18 +05:30
activityTitle : activity.activityTitle ,
activityDurationMins : activity.activityDurationMins ,
sustainabilityScore : activity.sustainabilityScore ,
cheapestPrice ,
2026-03-05 18:57:58 +05:30
distance ,
2026-02-28 11:54:32 +05:30
rating : 0 ,
2026-02-19 15:55:18 +05:30
energyLevel : activity.activityType.energyLevel
? {
2026-02-19 20:05:33 +05:30
. . . activity . activityType . energyLevel ,
presignedUrl : await attachPresignedUrl (
activity . activityType . energyLevel . energyIcon ,
) ,
}
2026-02-19 15:55:18 +05:30
: null ,
media : await attachMediaWithPresignedUrl ( activity . ActivitiesMedia ) ,
} ;
} ) ,
) ;
const interestsWithActivities = await Promise . all (
[ . . . userInterests ]
. sort ( ( a , b ) = >
a . interest . interestName . localeCompare ( b . interest . interestName ) ,
)
. map ( async ( ui ) = > ( {
interestId : ui.interest.id ,
interestName : ui.interest.interestName ,
interestColor : ui.interest.interestColor ,
interestImage : ui.interest.interestImage ,
interestImagePresignedUrl : await attachPresignedUrl (
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 ,
2026-02-27 17:24:39 +05:30
experiencesLogged : 0 ,
citiesDiscovered : 0 ,
2026-02-19 15:55:18 +05:30
loggedInNetworkCount : 0 ,
citiesInNetworkCount : 0 ,
2026-02-27 17:24:39 +05:30
rating : 0 ,
2026-03-10 21:30:43 +05:30
latestBucketInterestedCoverImage : latestCoverImage ,
latestBucketInterestedCoverImagePresignedUrl :
latestCoverImagePresignedUrl ,
2026-02-28 13:11:26 +05:30
interestedCount : userInterestedActivityIds.length ,
bucketCount : userBucketActivityIds.length ,
2026-02-19 15:55:18 +05:30
pagination : {
page ,
limit ,
} ,
2026-02-27 17:24:39 +05:30
randomActivities ,
2026-02-19 15:55:18 +05:30
interests : interestsWithActivities ,
otherStatesActivities : formattedOtherStatesActivities ,
overSeasActivities : formattedOverSeasActivities ,
newArrivalsActivities : formattedNewArrivalsActivities ,
mostHypedActivities : formattedMostHypedActivities ,
} ;
} ) ;
2026-02-04 15:32:11 +05:30
2026-02-19 15:55:18 +05:30
return data ;
}
2026-02-09 15:27:10 +05:30
2026-02-19 15:55:18 +05:30
async getSurpriseMeDetails (
userId : number ,
page : number ,
limit : number ,
countryName : string ,
stateName : string ,
cityName : string ,
) {
const data = await this . prisma . $transaction ( async ( tx ) = > {
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
2026-02-09 15:27:10 +05:30
1 ️ ⃣ USER LOCATION
=== === === === === === === === === === === === === === === === === == * /
2026-02-19 15:55:18 +05:30
const userAddressDetails = await tx . userAddressDetails . findFirst ( {
where : { userXid : userId } ,
select : {
stateXid : true ,
cityXid : true ,
countryXid : true ,
2026-03-05 18:57:58 +05:30
locationLat : true ,
locationLong : true ,
2026-02-19 15:55:18 +05:30
} ,
} ) ;
2026-03-05 18:57:58 +05:30
const userLat = userAddressDetails ? . locationLat ? ? null ;
const userLng = userAddressDetails ? . locationLong ? ? null ;
2026-02-19 15:55:18 +05:30
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 ,
} ;
}
2026-02-09 15:27:10 +05:30
2026-02-19 15:55:18 +05:30
const effectiveCountryXid = effectiveLocation ? . countryXid ? ? null ;
const effectiveStateXid = effectiveLocation ? . stateXid ? ? null ;
2026-02-09 15:27:10 +05:30
2026-02-19 15:55:18 +05:30
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
2026-02-09 15:27:10 +05:30
2 ️ ⃣ USER INTERESTS ( TO EXCLUDE )
=== === === === === === === === === === === === === === === === === == * /
2026-02-19 15:55:18 +05:30
const userInterests = await tx . userInterests . findMany ( {
where : { userXid : userId , isActive : true } ,
select : { interestXid : true } ,
} ) ;
2026-02-09 15:27:10 +05:30
2026-02-19 15:55:18 +05:30
const userInterestTypeIds = await tx . activityTypes . findMany ( {
where : {
interestXid : { in : userInterests . map ( ( i ) = > i . interestXid ) } ,
isActive : true ,
} ,
select : { id : true } ,
} ) ;
2026-02-09 15:27:10 +05:30
2026-02-19 15:55:18 +05:30
const excludedActivityTypeIds = userInterestTypeIds . map ( ( a ) = > a . id ) ;
2026-02-09 15:27:10 +05:30
2026-02-19 15:55:18 +05:30
const excludeUserInterestCondition =
excludedActivityTypeIds . length > 0
? { activityTypeXid : { notIn : excludedActivityTypeIds } }
: { } ;
2026-02-09 15:27:10 +05:30
2026-02-19 15:55:18 +05:30
const skip = ( page - 1 ) * limit ;
2026-02-09 15:27:10 +05:30
2026-02-28 13:11:26 +05:30
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 ] ;
2026-02-27 17:24:39 +05:30
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
CONNECTION INTEREST MAP
=== === === === === === === === === === === === === === === === === == * /
const userConnectionDetails = await tx . connectDetails . findMany ( {
where : { userXid : userId , isActive : true } ,
select : { schoolCompanyXid : true } ,
} ) ;
const otherConnectionUsers = await tx . connectDetails . findMany ( {
where : {
userXid : { not : userId } ,
isActive : true ,
schoolCompanyXid : {
in : userConnectionDetails . map ( ( u ) = > u . schoolCompanyXid ) ,
} ,
} ,
select : { userXid : true } ,
} ) ;
// Prevent empty IN crash
const connectionUserIds =
otherConnectionUsers . length > 0
? otherConnectionUsers . map ( ( u ) = > u . userXid )
: [ - 1 ] ;
// Only bucket = true (important!)
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 ,
] ) ,
) ;
2026-02-19 15:55:18 +05:30
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
2026-02-09 15:27:10 +05:30
3 ️ ⃣ OTHER INTERESTS ( GROUPED WITH ACTIVITIES )
=== === === === === === === === === === === === === === === === === == * /
2026-02-19 15:55:18 +05:30
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 ,
} ,
} ) ;
2026-02-09 15:27:10 +05:30
2026-02-19 15:55:18 +05:30
const otherInterestActivities = await tx . activities . findMany ( {
where : {
isActive : true ,
2026-02-28 13:11:26 +05:30
activityInternalStatus : ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED ,
amInternalStatus : ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED ,
id : { notIn : safeExcludedIds } ,
2026-02-19 15:55:18 +05:30
. . . excludeUserInterestCondition ,
} ,
skip ,
take : limit ,
orderBy : { id : 'desc' } ,
select : {
id : true ,
activityTitle : true ,
2026-03-05 18:57:58 +05:30
checkInLat : true ,
checkInLong : true ,
2026-03-10 18:12:26 +05:30
activityDurationMins : true ,
sustainabilityScore : true ,
ActivityVenues : {
select : {
ActivityPrices : {
select : {
sellPrice : true ,
} ,
} ,
} ,
} ,
2026-02-19 15:55:18 +05:30
activityType : {
select : {
interestXid : true ,
energyLevel : true ,
} ,
} ,
ActivitiesMedia : {
where : { isActive : true } ,
select : { id : true , mediaFileName : true , mediaType : true } ,
} ,
} ,
} ) ;
2026-03-10 18:12:26 +05:30
2026-02-19 15:55:18 +05:30
const formattedOtherInterestActivities = await Promise . all (
otherInterestActivities . map ( async ( a ) = > ( {
2026-03-10 18:12:26 +05:30
cheapestPrice :
a . ActivityVenues . flatMap ( v = > v . ActivityPrices )
. map ( p = > p . sellPrice )
. filter ( Boolean )
. sort ( ( a , b ) = > a - b ) [ 0 ] ? ? null ,
2026-02-19 15:55:18 +05:30
interestXid : a.activityType.interestXid ,
activityId : a.id ,
2026-02-27 17:24:39 +05:30
connectionInterestedCount :
connectionInterestMap . get ( a . id ) ? ? 0 ,
2026-02-19 15:55:18 +05:30
activityTitle : a.activityTitle ,
2026-03-05 18:57:58 +05:30
distance : calculateDistance (
userLat ,
userLng ,
a . checkInLat ,
a . checkInLong
) ,
2026-03-10 18:12:26 +05:30
activityDurationMins : a.activityDurationMins ,
sustainabilityScore : a.sustainabilityScore ,
2026-02-28 11:54:32 +05:30
rating : 0 ,
2026-02-19 15:55:18 +05:30
energyLevel : {
. . . a . activityType . energyLevel ,
presignedUrl : await attachPresignedUrl (
a . activityType . energyLevel ? . energyIcon ,
) ,
} ,
media : await attachMediaWithPresignedUrl ( a . ActivitiesMedia ) ,
} ) ) ,
) ;
const interestsWithActivities = await Promise . all (
otherInterests . map ( async ( interest ) = > ( {
interestId : interest.id ,
interestName : interest.interestName ,
interestColor : interest.interestColor ,
interestImage : interest.interestImage ,
interestImagePresignedUrl : await attachPresignedUrl (
interest . interestImage ,
) ,
page ,
limit ,
hasMore : formattedOtherInterestActivities.length === limit ,
activities : formattedOtherInterestActivities
. filter ( ( a ) = > a . interestXid === interest . id )
. map ( ( { interestXid , . . . rest } ) = > rest ) ,
} ) ) ,
) ;
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
2026-02-09 15:27:10 +05:30
4 ️ ⃣ MOST HYPED
=== === === === === === === === === === === === === === === === === == * /
2026-02-19 15:55:18 +05:30
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 ( {
2026-02-28 13:11:26 +05:30
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 ,
} ,
2026-02-19 15:55:18 +05:30
select : {
id : true ,
activityTitle : true ,
2026-03-05 18:57:58 +05:30
checkInLat : true ,
checkInLong : true ,
2026-02-19 15:55:18 +05:30
activityType : { select : { energyLevel : true } } ,
2026-03-10 18:12:26 +05:30
activityDurationMins : true ,
sustainabilityScore : true ,
ActivityVenues : {
select : {
ActivityPrices : {
select : {
sellPrice : true ,
} ,
} ,
} ,
} ,
2026-02-19 15:55:18 +05:30
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 {
2026-03-10 18:12:26 +05:30
cheapestPrice : act.ActivityVenues.flatMap ( v = > v . ActivityPrices )
. map ( p = > p . sellPrice )
. filter ( Boolean )
. sort ( ( a , b ) = > a - b ) [ 0 ] ? ? null ,
activityDurationMins : act.activityDurationMins ,
sustainabilityScore : act.sustainabilityScore ,
2026-02-19 15:55:18 +05:30
activityId : act.id ,
activityTitle : act.activityTitle ,
hypeCount : g._count.activityXid ,
2026-02-27 17:24:39 +05:30
connectionInterestedCount :
connectionInterestMap . get ( act . id ) ? ? 0 ,
2026-03-05 18:57:58 +05:30
distance : calculateDistance (
userLat ,
userLng ,
act . checkInLat ,
act . checkInLong
) ,
2026-02-28 11:54:32 +05:30
rating : 0 ,
2026-02-19 15:55:18 +05:30
energyLevel : {
. . . act . activityType . energyLevel ,
presignedUrl : await attachPresignedUrl (
act . activityType . energyLevel ? . energyIcon ,
) ,
} ,
media : await attachMediaWithPresignedUrl ( act . ActivitiesMedia ) ,
} ;
} ) ,
) . then ( ( a ) = > a . filter ( Boolean ) ) ;
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
2026-02-09 15:27:10 +05:30
5 ️ ⃣ NEW ARRIVALS
=== === === === === === === === === === === === === === === === === == * /
2026-02-19 15:55:18 +05:30
const newArrivalsWhere = {
2026-02-28 13:11:26 +05:30
id : { notIn : safeExcludedIds } ,
2026-02-19 15:55:18 +05:30
isActive : true ,
2026-02-28 13:11:26 +05:30
activityInternalStatus : ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED ,
amInternalStatus : ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED ,
2026-02-19 15:55:18 +05:30
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 : {
2026-02-27 17:24:39 +05:30
id : true ,
2026-02-19 15:55:18 +05:30
activityTitle : true ,
activityType : { select : { energyLevel : true } } ,
2026-03-10 18:12:26 +05:30
activityDurationMins : true ,
sustainabilityScore : true ,
ActivityVenues : {
select : {
ActivityPrices : {
select : {
sellPrice : true ,
} ,
} ,
} ,
} ,
2026-02-19 15:55:18 +05:30
ActivitiesMedia : {
where : { isActive : true } ,
select : { id : true , mediaFileName : true , mediaType : true } ,
} ,
} ,
} ) ;
2026-02-09 15:27:10 +05:30
2026-02-19 15:55:18 +05:30
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
2026-02-09 15:27:10 +05:30
6 ️ ⃣ OTHER STATES & OVERSEAS
=== === === === === === === === === === === === === === === === === == * /
2026-02-19 15:55:18 +05:30
const otherStatesWhere : any = {
isActive : true ,
2026-02-28 13:11:26 +05:30
id : { notIn : safeExcludedIds } ,
activityInternalStatus : ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED ,
amInternalStatus : ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED ,
2026-02-19 15:55:18 +05:30
. . . excludeUserInterestCondition ,
} ;
if ( effectiveCountryXid )
otherStatesWhere . checkInCountryXid = effectiveCountryXid ;
if ( effectiveStateXid )
otherStatesWhere . checkInStateXid = { not : effectiveStateXid } ;
const overseasWhere : any = {
isActive : true ,
2026-02-28 13:11:26 +05:30
id : { notIn : safeExcludedIds } ,
activityInternalStatus : ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED ,
amInternalStatus : ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED ,
2026-02-19 15:55:18 +05:30
. . . 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 : {
2026-02-27 17:24:39 +05:30
id : true ,
2026-02-19 15:55:18 +05:30
activityTitle : true ,
activityType : { select : { energyLevel : true } } ,
2026-03-10 18:12:26 +05:30
activityDurationMins : true ,
sustainabilityScore : true ,
ActivityVenues : {
select : {
ActivityPrices : {
select : {
sellPrice : true ,
} ,
} ,
} ,
} ,
2026-02-19 15:55:18 +05:30
ActivitiesMedia : {
where : { isActive : true } ,
select : { id : true , mediaFileName : true , mediaType : true } ,
} ,
} ,
} ) ,
tx . activities . findMany ( {
where : overseasWhere ,
skip ,
take : limit ,
select : {
2026-02-27 17:24:39 +05:30
id : true ,
2026-02-19 15:55:18 +05:30
activityTitle : true ,
activityType : { select : { energyLevel : true } } ,
2026-03-10 18:12:26 +05:30
activityDurationMins : true ,
sustainabilityScore : true ,
ActivityVenues : {
select : {
ActivityPrices : {
select : {
sellPrice : true ,
} ,
} ,
} ,
} ,
2026-02-19 15:55:18 +05:30
ActivitiesMedia : {
where : { isActive : true } ,
select : { id : true , mediaFileName : true , mediaType : true } ,
} ,
} ,
} ) ,
] ) ;
2026-03-09 15:57:14 +05:30
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
RANDOM ACTIVITIES ( 5 COVER IMAGES )
=== === === === === === === === === === === === === === === === === == * /
2026-03-25 13:34:12 +05:30
let randomActivities : any [ ] = [ ] ;
const eligibleRandomActivityIds = await tx . activities . findMany ( {
2026-03-09 15:57:14 +05:30
where : {
isActive : true ,
activityInternalStatus : ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED ,
amInternalStatus : ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED ,
deletedAt : null ,
id : {
notIn : safeExcludedIds ,
} ,
2026-03-25 13:34:12 +05:30
ActivitiesMedia : {
some : {
isActive : true ,
isCoverImage : true ,
} ,
} ,
2026-03-09 15:57:14 +05:30
. . . excludeUserInterestCondition ,
} ,
2026-03-25 13:34:12 +05:30
select : {
id : true ,
} ,
2026-03-09 15:57:14 +05:30
} ) ;
2026-03-25 13:34:12 +05:30
if ( eligibleRandomActivityIds . length > 0 ) {
const takeCount = Math . min ( 5 , eligibleRandomActivityIds . length ) ;
const selectedIds = eligibleRandomActivityIds
. sort ( ( ) = > Math . random ( ) - 0.5 )
. slice ( 0 , takeCount )
. map ( ( activity ) = > activity . id ) ;
2026-03-09 15:57:14 +05:30
2026-03-25 13:34:12 +05:30
const randomFetched = await tx . activities . findMany ( {
where : {
id : { in : selectedIds } ,
} ,
select : {
id : true ,
activityTitle : true ,
ActivitiesMedia : {
where : { isActive : true , isCoverImage : true } ,
orderBy : { displayOrder : 'asc' } ,
take : 1 ,
2026-03-09 15:57:14 +05:30
select : {
2026-03-25 13:34:12 +05:30
mediaFileName : true ,
2026-03-09 15:57:14 +05:30
} ,
2026-03-25 13:34:12 +05:30
} ,
} ,
} ) ;
2026-03-09 15:57:14 +05:30
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 attachPresignedUrl ( cover . mediaFileName )
: null ,
} ;
} ) ,
) ;
}
2026-02-19 15:55:18 +05:30
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
2026-02-09 15:27:10 +05:30
7 ️ ⃣ FINAL RESPONSE
=== === === === === === === === === === === === === === === === === == * /
2026-02-19 15:55:18 +05:30
return {
pagination : { page , limit } ,
interests : interestsWithActivities ,
2026-02-28 13:11:26 +05:30
interestedCount : interestedActivityIds.length ,
bucketCount : bucketActivityIds.length ,
2026-03-09 15:57:14 +05:30
randomActivities ,
2026-02-19 15:55:18 +05:30
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 ) = > ( {
2026-03-10 18:12:26 +05:30
cheapestPrice : a.ActivityVenues.flatMap ( v = > v . ActivityPrices )
. map ( p = > p . sellPrice )
. filter ( Boolean )
. sort ( ( a , b ) = > a - b ) [ 0 ] ? ? null ,
activityDurationMins : a.activityDurationMins ,
sustainabilityScore : a.sustainabilityScore ,
2026-03-09 15:18:59 +05:30
activityId : a.id ,
2026-02-19 15:55:18 +05:30
activityTitle : a.activityTitle ,
2026-02-27 17:24:39 +05:30
connectionInterestedCount :
connectionInterestMap . get ( a . id ) ? ? 0 ,
2026-02-28 11:54:32 +05:30
distance : 0 ,
rating : 0 ,
2026-02-19 15:55:18 +05:30
energyLevel : {
. . . a . activityType . energyLevel ,
presignedUrl : await attachPresignedUrl (
a . activityType . energyLevel ? . energyIcon ,
) ,
} ,
media : await attachMediaWithPresignedUrl ( a . ActivitiesMedia ) ,
} ) ) ,
) ,
} ,
otherStatesActivities : {
page ,
limit ,
totalCount : otherStatesCount ,
hasMore : skip + limit < otherStatesCount ,
activities : await Promise . all (
otherStatesRaw . map ( async ( a ) = > ( {
2026-03-10 18:12:26 +05:30
cheapestPrice : a.ActivityVenues.flatMap ( v = > v . ActivityPrices )
. map ( p = > p . sellPrice )
. filter ( Boolean )
. sort ( ( a , b ) = > a - b ) [ 0 ] ? ? null ,
activityDurationMins : a.activityDurationMins ,
sustainabilityScore : a.sustainabilityScore ,
2026-03-09 15:18:59 +05:30
activityId : a.id ,
2026-02-19 15:55:18 +05:30
activityTitle : a.activityTitle ,
2026-02-27 17:24:39 +05:30
connectionInterestedCount :
connectionInterestMap . get ( a . id ) ? ? 0 ,
2026-02-28 11:54:32 +05:30
distance : 0 ,
rating : 0 ,
2026-02-19 15:55:18 +05:30
energyLevel : {
. . . a . activityType . energyLevel ,
presignedUrl : await attachPresignedUrl (
a . activityType . energyLevel ? . energyIcon ,
) ,
} ,
media : await attachMediaWithPresignedUrl ( a . ActivitiesMedia ) ,
} ) ) ,
) ,
} ,
overSeasActivities : {
page ,
limit ,
totalCount : overseasCount ,
hasMore : skip + limit < overseasCount ,
activities : await Promise . all (
overseasRaw . map ( async ( a ) = > ( {
2026-03-10 18:12:26 +05:30
cheapestPrice : a.ActivityVenues.flatMap ( v = > v . ActivityPrices )
. map ( p = > p . sellPrice )
. filter ( Boolean )
. sort ( ( a , b ) = > a - b ) [ 0 ] ? ? null ,
activityDurationMins : a.activityDurationMins ,
sustainabilityScore : a.sustainabilityScore ,
2026-03-09 15:18:59 +05:30
activityId : a.id ,
2026-02-19 15:55:18 +05:30
activityTitle : a.activityTitle ,
2026-02-27 17:24:39 +05:30
connectionInterestedCount :
connectionInterestMap . get ( a . id ) ? ? 0 ,
2026-02-28 11:54:32 +05:30
distance : 0 ,
rating : 0 ,
2026-02-19 15:55:18 +05:30
energyLevel : {
. . . a . activityType . energyLevel ,
presignedUrl : await attachPresignedUrl (
a . activityType . energyLevel ? . energyIcon ,
) ,
} ,
media : await attachMediaWithPresignedUrl ( a . ActivitiesMedia ) ,
} ) ) ,
) ,
} ,
} ;
} ) ;
2026-02-09 15:27:10 +05:30
2026-02-19 15:55:18 +05:30
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 ,
2026-03-06 19:59:31 +05:30
checkInCity : {
select : {
id : true ,
cityName : true
}
} ,
checkOutCity : {
select : {
id : true ,
cityName : true
}
} ,
checkInState : {
select : {
id : true ,
stateName : true
}
} ,
checkOutState : {
select : {
id : true ,
stateName : true
}
} ,
2026-02-19 15:55:18 +05:30
activityType : {
select : {
interestXid : true , // ✅ VERY IMPORTANT
activityTypeName : true ,
energyLevel : {
select : {
id : true ,
energyLevelName : true ,
energyColor : true ,
energyIcon : true ,
2026-02-09 15:27:10 +05:30
} ,
2026-02-19 15:55:18 +05:30
} ,
} ,
} ,
ActivityOtherDetails : {
where : { isActive : true } ,
select : {
id : true ,
exclusiveNotes : true ,
2026-03-05 16:15:15 +05:30
SafetyInstruction : true ,
Cancellations : true ,
2026-02-19 15:55:18 +05:30
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 ,
} ,
} ,
2026-02-09 15:27:10 +05:30
2026-02-19 15:55:18 +05:30
ActivityAllowedEntry : {
where : { isActive : true } ,
select : {
id : true ,
allowedEntryTypeXid : true ,
allowedEntryType : {
select : {
id : true ,
allowedEntryTypeName : true ,
2026-02-09 15:27:10 +05:30
} ,
2026-02-19 15:55:18 +05:30
} ,
} ,
} ,
2026-02-09 15:27:10 +05:30
2026-02-19 15:55:18 +05:30
ActivityFoodCost : {
where : { isActive : true } ,
select : {
id : true ,
totalAmount : true ,
} ,
} ,
2026-02-09 15:27:10 +05:30
2026-02-19 15:55:18 +05:30
activityFoodTypes : {
where : { isActive : true } ,
select : {
id : true ,
foodTypeXid : true ,
foodType : {
select : {
id : true ,
foodTypeName : true ,
2026-02-09 15:27:10 +05:30
} ,
2026-02-19 15:55:18 +05:30
} ,
} ,
} ,
2026-02-09 15:27:10 +05:30
2026-02-19 15:55:18 +05:30
ActivityEquipments : {
where : { isActive : true } ,
select : {
id : true ,
equipmentName : true ,
isEquipmentChargeable : true ,
equipmentTotalPrice : true ,
} ,
} ,
2026-02-09 15:27:10 +05:30
2026-02-19 15:55:18 +05:30
ActivityNavigationModes : {
where : { isActive : true } ,
select : {
id : true ,
2026-03-04 12:23:29 +05:30
navigationModeName : true ,
2026-02-19 15:55:18 +05:30
isInActivityChargeable : true ,
navigationModesTotalPrice : true ,
} ,
} ,
2026-02-09 15:27:10 +05:30
2026-02-19 15:55:18 +05:30
ActivityAmenities : {
where : { isActive : true } ,
select : {
id : true ,
amenitiesXid : true ,
amenities : {
select : {
id : true ,
amenitiesName : true ,
amenitiesIcon : true ,
2026-02-06 19:27:37 +05:30
} ,
2026-02-19 15:55:18 +05:30
} ,
} ,
} ,
2026-02-09 15:27:10 +05:30
2026-02-19 15:55:18 +05:30
ActivityPickUpDetails : {
where : { isActive : true } ,
select : {
id : true ,
isPickUp : true ,
locationLat : true ,
locationLong : true ,
locationAddress : true ,
transportTotalPrice : true ,
} ,
} ,
2026-02-09 15:27:10 +05:30
2026-02-19 15:55:18 +05:30
activityPickUpTransports : {
where : { isActive : true } ,
select : {
id : true ,
transportMode : {
2026-02-06 19:27:37 +05:30
select : {
2026-02-19 15:55:18 +05:30
id : true ,
transportModeName : true ,
transportModeIcon : true ,
} ,
} ,
} ,
} ,
2026-02-06 19:27:37 +05:30
2026-02-19 15:55:18 +05:30
activityCuisines : {
where : { isActive : true } ,
select : {
id : true ,
foodCuisineXid : true ,
foodCuisine : {
select : {
id : true ,
cuisineName : true ,
2026-02-06 19:27:37 +05:30
} ,
2026-02-19 15:55:18 +05:30
} ,
} ,
} ,
2026-02-06 19:27:37 +05:30
2026-02-19 15:55:18 +05:30
ActivityVenues : {
where : {
isActive : true ,
2026-03-05 18:12:35 +05:30
ScheduleHeader : {
some : { }
}
2026-02-19 15:55:18 +05:30
} ,
select : {
id : true ,
venueName : true ,
venueLabel : true ,
venueCapacity : true ,
availableSeats : true ,
isMinPeopleReqMandatory : true ,
minPeopleRequired : true ,
minReqfullfilledBeforeMins : true ,
venueDescription : true ,
ActivityVenueArtifacts : {
2026-02-06 19:27:37 +05:30
select : {
2026-02-19 15:55:18 +05:30
id : true ,
mediaFileName : true ,
mediaType : true ,
2026-02-06 19:27:37 +05:30
} ,
2026-02-19 15:55:18 +05:30
} ,
2026-03-05 18:12:35 +05:30
ScheduleHeader : {
select : {
id : true ,
scheduleType : true ,
startDate : true ,
endDate : true ,
earlyCheckInMins : true ,
bookingCutOffMins : true ,
effectiveFromDt : true ,
effectiveToDt : true ,
}
} ,
2026-02-19 15:55:18 +05:30
ActivityPrices : {
select : {
id : true ,
sellPrice : true ,
} ,
} ,
} ,
} ,
2026-02-17 11:48:06 +05:30
2026-02-19 15:55:18 +05:30
ActivitiesMedia : {
where : { isActive : true } ,
select : {
id : true ,
mediaFileName : true ,
mediaType : true ,
} ,
} ,
} ,
} ) ;
2026-02-17 11:48:06 +05:30
2026-03-06 15:40:21 +05:30
if ( ! activity ) {
throw new Error ( "Activity not found" ) ;
}
2026-03-06 17:36:14 +05:30
const userActivityStatus = await tx . userBucketInterested . findFirst ( {
where : {
activityXid : activityXid ,
userXid : userId ,
isActive : true ,
} ,
select : {
isBucket : true ,
} ,
} ) ;
const isBucket = userActivityStatus ? . isBucket === true ;
const isInterested =
userActivityStatus ? userActivityStatus . isBucket === false : false ;
2026-03-06 15:40:21 +05:30
const userLocation = await tx . userAddressDetails . findFirst ( {
where : { userXid : userId } ,
select : {
locationLat : true ,
locationLong : true ,
} ,
} ) ;
const userLat = userLocation ? . locationLat ? ? null ;
const userLng = userLocation ? . locationLong ? ? null ;
2026-03-06 15:49:13 +05:30
let distance = 0 ;
if (
userLat &&
userLng &&
activity ? . checkInLat &&
activity ? . checkInLong
) {
distance = calculateDistance (
userLat ,
userLng ,
activity . checkInLat ,
activity . checkInLong
)
} ;
2026-03-05 19:21:16 +05:30
2026-02-19 15:55:18 +05:30
// ================= PRESIGNED URL SECTION =================
2026-02-19 14:51:26 +05:30
2026-02-19 15:55:18 +05:30
// 1️ ⃣ Activity Media
if ( Array . isArray ( activity ? . ActivitiesMedia ) ) {
activity . ActivitiesMedia = await Promise . all (
activity . ActivitiesMedia . map ( async ( m : any ) = > ( {
. . . m ,
presignedUrl : await attachPresignedUrl ( m . mediaFileName ) ,
} ) ) ,
) ;
}
// 2️ ⃣ Energy Level Icon
if ( activity ? . activityType ? . energyLevel ? . energyIcon ) {
activity . activityType . energyLevel . energyIcon = await attachPresignedUrl (
activity . activityType . energyLevel . energyIcon ,
) ;
}
// 5️ ⃣ PickUp Transport Mode Icons
if ( Array . isArray ( activity ? . activityPickUpTransports ) ) {
await Promise . all (
activity . activityPickUpTransports . map ( async ( item : any ) = > {
if ( item ? . transportMode ? . transportModeIcon ) {
item . transportMode . presignedUrl = await attachPresignedUrl (
item . transportMode . transportModeIcon ,
) ;
2026-02-19 14:51:26 +05:30
}
2026-02-19 15:55:18 +05:30
} ) ,
) ;
}
2026-02-19 14:51:26 +05:30
2026-02-19 15:55:18 +05:30
// 3️ ⃣ Activity Venue Artifacts
if ( Array . isArray ( activity ? . ActivityVenues ) ) {
await Promise . all (
activity . ActivityVenues . map ( async ( venue : any ) = > {
if ( Array . isArray ( venue ? . ActivityVenueArtifacts ) ) {
venue . ActivityVenueArtifacts = await Promise . all (
venue . ActivityVenueArtifacts . map ( async ( artifact : any ) = > ( {
. . . artifact ,
presignedUrl : await attachPresignedUrl (
artifact . mediaFileName ,
) ,
} ) ) ,
) ;
2026-02-17 11:48:06 +05:30
}
2026-02-19 15:55:18 +05:30
} ) ,
) ;
}
2026-02-17 11:48:06 +05:30
2026-02-19 15:55:18 +05:30
// 3️ ⃣ Navigation Mode Icons
if ( Array . isArray ( activity ? . ActivityNavigationModes ) ) {
await Promise . all (
activity . ActivityNavigationModes . map ( async ( item : any ) = > {
if ( item ? . navigationMode ? . navigationModeIcon ) {
item . navigationMode . presignedUrl = await attachPresignedUrl (
item . navigationMode . navigationModeIcon ,
) ;
2026-02-17 11:48:06 +05:30
}
2026-02-19 15:55:18 +05:30
} ) ,
) ;
}
2026-02-17 11:48:06 +05:30
2026-02-19 15:55:18 +05:30
// 4️ ⃣ Amenities Icons (IMPORTANT: make sure amenitiesIcon is selected in Prisma)
if ( Array . isArray ( activity ? . ActivityAmenities ) ) {
await Promise . all (
activity . ActivityAmenities . map ( async ( item : any ) = > {
if ( item ? . amenities ? . amenitiesIcon ) {
item . amenities . presignedUrl = await attachPresignedUrl (
item . amenities . amenitiesIcon ,
) ;
}
} ) ,
) ;
}
2026-02-06 19:27:37 +05:30
2026-02-27 17:24:39 +05:30
// 🔹 Get connection users
const userConnectionDetails = await tx . connectDetails . findMany ( {
where : { userXid : userId , isActive : true } ,
select : { schoolCompanyXid : true } ,
} ) ;
const schoolCompanyXids = userConnectionDetails . map (
( c ) = > c . schoolCompanyXid ,
) ;
const connectionUsers = await tx . connectDetails . findMany ( {
where : {
isActive : true ,
schoolCompanyXid : {
in : schoolCompanyXids . length ? schoolCompanyXids : [ - 1 ] ,
} ,
userXid : { not : userId } ,
} ,
select : { userXid : true } ,
} ) ;
const connectionUserIds = connectionUsers . map ( ( u ) = > u . userXid ) ;
2026-02-19 15:55:18 +05:30
2026-02-27 17:24:39 +05:30
const connectionInterestedCount = connectionUserIds . length
? await tx . userBucketInterested . count ( {
where : {
activityXid ,
userXid : { in : connectionUserIds } ,
isActive : true ,
} ,
} )
: 0 ;
2026-03-06 15:40:21 +05:30
const prices =
activity ? . ActivityVenues ? . flatMap ( ( v ) = >
v . ActivityPrices . map ( ( p ) = > p . sellPrice )
) . filter ( ( p ) = > p !== null ) ? ? [ ] ;
2026-02-19 15:55:18 +05:30
const cheapestPrice = prices . length > 0 ? Math . min ( . . . prices ) : null ;
const totalCapacity = activity . ActivityVenues . map (
( v ) = > v . venueCapacity ? ? 0 ,
) . reduce ( ( sum , capacity ) = > sum + capacity , 0 ) ;
2026-02-28 13:36:33 +05:30
const interestedCount = await tx . userBucketInterested . count ( {
where : {
activityXid ,
isBucket : false ,
isActive : true ,
} ,
} ) ;
const interestedUsers = await tx . userBucketInterested . findMany ( {
where : {
activityXid ,
isBucket : false ,
isActive : true ,
user : {
isActive : true ,
2026-03-10 21:30:43 +05:30
profileImage : { not : null } ,
2026-02-28 13:36:33 +05:30
} ,
} ,
select : {
user : {
select : {
profileImage : true ,
} ,
} ,
} ,
2026-03-10 21:30:43 +05:30
take : 5 ,
2026-02-28 13:36:33 +05:30
} ) ;
2026-03-06 19:59:31 +05:30
const randomFive = interestedUsers
. sort ( ( ) = > Math . random ( ) - 0.5 )
. slice ( 0 , 5 ) ;
2026-02-28 13:36:33 +05:30
2026-03-10 21:30:43 +05:30
// const interestedUserImages: { profileImage: string }[] = [];
2026-02-28 13:36:33 +05:30
2026-03-10 21:30:43 +05:30
// for (const item of randomFive) {
// const profileImage = item.user.profileImage;
2026-02-28 13:36:33 +05:30
2026-03-10 21:30:43 +05:30
// if (profileImage) {
// const key = profileImage.startsWith('http')
// ? new URL(profileImage).pathname.replace(/^\/+/, '')
// : profileImage;
2026-02-28 13:36:33 +05:30
2026-03-10 21:30:43 +05:30
// const presignedUrl = await getPresignedUrl(bucket, key);
2026-03-06 19:59:31 +05:30
2026-03-10 21:30:43 +05:30
// interestedUserImages.push({
// profileImage: presignedUrl,
// });
// }
// }
const interestedUserImages = await Promise . all (
randomFive . map ( async ( { user } ) = > ( {
profileImage : await attachPresignedUrl ( user . profileImage ) ,
} ) )
) ;
2026-02-28 13:36:33 +05:30
2026-03-06 19:59:31 +05:30
const checkInLocation =
activity ? . checkInCity ? . cityName && activity ? . checkInState ? . stateName
? ` ${ activity . checkInCity . cityName } , ${ activity . checkInState . stateName } `
: null ;
const checkOutLocation =
activity ? . checkOutCity ? . cityName && activity ? . checkOutState ? . stateName
? ` ${ activity . checkOutCity . cityName } , ${ activity . checkOutState . stateName } `
: null ;
2026-02-19 15:55:18 +05:30
return {
activity ,
interestedCount ,
2026-02-27 17:24:39 +05:30
connectionInterestedCount ,
2026-02-19 15:55:18 +05:30
cheapestPrice ,
totalCapacity ,
rating : 0 , // ⭐ Placeholder, implement rating logic as needed
2026-03-06 15:49:13 +05:30
distance : distance || 0 ,
2026-03-06 17:36:14 +05:30
interestedUserImages ,
isBucket ,
2026-03-06 19:59:31 +05:30
isInterested ,
checkInLocation ,
checkOutLocation
2026-02-19 15:55:18 +05:30
} ;
} ) ;
}
2026-02-19 20:05:33 +05:30
2026-02-19 15:55:18 +05:30
async searchActivities (
2026-03-10 21:30:43 +05:30
activityType? : string
2026-02-19 15:55:18 +05:30
) {
// Build the where clause dynamically
const where : any = {
isActive : true ,
} ;
2026-02-17 11:48:06 +05:30
2026-03-11 13:41:15 +05:30
if ( activityType && activityType . trim ( ) . length > 0 ) {
where . activityTypeName = {
contains : activityType.trim ( ) ,
mode : 'insensitive' ,
2026-02-19 15:55:18 +05:30
} ;
2026-02-06 19:27:37 +05:30
}
2026-02-13 17:40:12 +05:30
2026-03-11 13:41:15 +05:30
const activityTypes = await this . prisma . activityTypes . findMany ( {
2026-02-19 15:55:18 +05:30
where ,
select : {
id : true ,
2026-03-11 13:41:15 +05:30
activityTypeName : true ,
interests : {
2026-02-19 15:55:18 +05:30
select : {
2026-03-11 13:41:15 +05:30
interestImage : true
}
}
} ,
orderBy : {
activityTypeName : 'asc' ,
2026-02-19 15:55:18 +05:30
} ,
2026-03-11 13:41:15 +05:30
take : 20 , // limit suggestions
2026-02-19 15:55:18 +05:30
} ) ;
2026-02-13 17:40:12 +05:30
2026-02-19 15:55:18 +05:30
// Get interested count for each activity
2026-03-11 13:41:15 +05:30
const formattedResults = await Promise . all (
activityTypes . map ( async ( activity ) = > {
const image = activity . interests ? . interestImage ? ? null ;
2026-02-13 17:40:12 +05:30
2026-03-11 13:41:15 +05:30
const presignedUrl = image
? await attachPresignedUrl ( image )
: null ;
2026-02-13 17:40:12 +05:30
2026-02-19 15:55:18 +05:30
return {
2026-03-11 13:41:15 +05:30
id : activity.id ,
activityTypeName : activity.activityTypeName ,
interestImage : image ,
interestImagePresignedUrl : presignedUrl ,
2026-02-13 17:40:12 +05:30
} ;
2026-02-19 15:55:18 +05:30
} ) ,
) ;
2026-02-19 15:50:48 +05:30
2026-03-11 13:41:15 +05:30
return formattedResults ;
2026-02-19 15:55:18 +05:30
}
2026-02-19 15:50:48 +05:30
2026-02-23 20:00:50 +05:30
async getNearbyActivities (
userId : number ,
userLat : number ,
userLong : number ,
radiusKm : number ,
page : number ,
limit : number ,
) {
2026-03-11 13:41:15 +05:30
// If lat/long not provided, fetch from user saved address
if ( userLat === undefined || userLong === undefined ) {
const userAddress = await this . prisma . userAddressDetails . findFirst ( {
where : { userXid : userId , isActive : true } ,
select : {
locationLat : true ,
locationLong : true ,
} ,
} ) ;
if ( ! userAddress ? . locationLat || ! userAddress ? . locationLong ) {
throw new ApiError (
400 ,
'User location not found. Please provide lat/long.' ,
) ;
}
2026-02-23 20:00:50 +05:30
2026-03-11 13:41:15 +05:30
userLat = userAddress . locationLat ;
userLong = userAddress . locationLong ;
2026-02-23 20:00:50 +05:30
}
const skip = ( page - 1 ) * limit ;
2026-02-27 17:24:39 +05:30
// 0.5️⃣ Get connection users
const userConnectionDetails = await this . prisma . connectDetails . findMany ( {
where : { userXid : userId , isActive : true } ,
select : { schoolCompanyXid : true } ,
} ) ;
const schoolCompanyXids = userConnectionDetails . map (
( c ) = > c . schoolCompanyXid ,
) ;
const connectionUsers = await this . prisma . connectDetails . findMany ( {
where : {
isActive : true ,
schoolCompanyXid : { in : schoolCompanyXids . length ? schoolCompanyXids : [ - 1 ] } ,
userXid : { not : userId } ,
} ,
select : { userXid : true } ,
} ) ;
const connectionUserIds = connectionUsers . map ( ( u ) = > u . userXid ) ;
2026-02-25 14:48:54 +05:30
// 0️ ⃣ Get user's interests and map to activity types
const userInterests = await this . prisma . userInterests . findMany ( {
where : { userXid : userId , isActive : true } ,
select : { interestXid : true } ,
} ) ;
if ( ! userInterests . length ) {
return {
page ,
limit ,
totalCount : 0 ,
hasMore : false ,
activities : [ ] ,
} ;
}
const activityTypeIds = (
await this . prisma . activityTypes . findMany ( {
where : { interestXid : { in : userInterests . map ( ( u ) = > u . interestXid ) } , isActive : true } ,
select : { id : true } ,
} )
) . map ( ( t ) = > t . id ) ;
if ( ! activityTypeIds . length ) {
return {
page ,
limit ,
totalCount : 0 ,
hasMore : false ,
activities : [ ] ,
} ;
}
2026-02-23 20:00:50 +05:30
// Rough bounding box in degrees to reduce DB scan
const earthRadiusKm = 6371 ;
const latDelta = ( radiusKm / earthRadiusKm ) * ( 180 / Math . PI ) ;
const lonDelta =
( radiusKm / ( earthRadiusKm * Math . cos ( deg2rad ( userLat ) ) ) ) *
( 180 / Math . PI ) ;
const candidates = await this . prisma . activities . findMany ( {
where : {
isActive : true ,
activityInternalStatus : ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED ,
amInternalStatus : ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED ,
2026-02-25 14:48:54 +05:30
activityTypeXid : { in : activityTypeIds } ,
2026-02-23 20:00:50 +05:30
checkInLat : {
not : null ,
gte : userLat - latDelta ,
lte : userLat + latDelta ,
} ,
checkInLong : {
not : null ,
gte : userLong - lonDelta ,
lte : userLong + lonDelta ,
} ,
} ,
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 withDistance = candidates
. map ( ( activity : any ) = > {
const distanceKm = getDistanceFromLatLon (
userLat ,
userLong ,
activity . checkInLat ,
activity . checkInLong ,
) ;
return {
. . . activity ,
distanceKm ,
} ;
} )
. filter ( ( a ) = > a . distanceKm <= radiusKm )
. sort ( ( a , b ) = > a . distanceKm - b . distanceKm ) ;
2026-02-27 17:24:39 +05:30
const nearbyActivityIds = withDistance . map ( ( a ) = > a . id ) ;
let connectionInterestMap = new Map < number , number > ( ) ;
if ( nearbyActivityIds . length && connectionUserIds . length ) {
const connectionInterestCounts =
await this . prisma . userBucketInterested . groupBy ( {
by : [ 'activityXid' ] ,
where : {
activityXid : { in : nearbyActivityIds } ,
userXid : { in : connectionUserIds } ,
isActive : true ,
isBucket : true , // ✅ only real interest
} ,
_count : { activityXid : true } ,
} ) ;
connectionInterestMap = new Map (
connectionInterestCounts . map ( ( item ) = > [
item . activityXid ,
item . _count . activityXid ,
] ) ,
) ;
}
2026-02-23 20:00:50 +05:30
const totalCount = withDistance . length ;
const paged = withDistance . slice ( skip , skip + limit ) ;
const formattedActivities = await Promise . all (
paged . 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 {
activityId : activity.id ,
activityTitle : activity.activityTitle ,
2026-02-27 17:24:39 +05:30
connectionInterestedCount :
connectionInterestMap . get ( activity . id ) ? ? 0 ,
2026-02-23 20:00:50 +05:30
activityDurationMins : activity.activityDurationMins ,
sustainabilityScore : activity.sustainabilityScore ,
2026-02-27 17:24:39 +05:30
rating : 0 ,
2026-02-28 11:54:32 +05:30
distance : activity.distanceKm ,
2026-02-23 20:00:50 +05:30
cheapestPrice ,
energyLevel : activity.activityType.energyLevel
? {
. . . activity . activityType . energyLevel ,
presignedUrl : await attachPresignedUrl (
activity . activityType . energyLevel . energyIcon ,
) ,
}
: null ,
media : await attachMediaWithPresignedUrl ( activity . ActivitiesMedia ) ,
} ;
} ) ,
) ;
return {
page ,
limit ,
totalCount ,
hasMore : skip + limit < totalCount ,
activities : formattedActivities ,
} ;
}
2026-02-19 16:25:43 +05:30
// CONNECTIONS
2026-02-19 15:50:48 +05:30
2026-02-19 16:25:43 +05:30
async getAllConnectionDetailsOfUser ( userXid : number ) {
return await this . prisma . connectDetails . findMany ( {
where : { userXid , isActive : true } ,
select : {
id : true ,
schoolCompany : {
select : {
id : true ,
schoolCompanyName : true ,
isSchool : true ,
2026-02-19 20:05:33 +05:30
cityXid : true ,
cities : {
select : {
id : true ,
cityName : true ,
stateXid : true ,
states : {
select : {
id : true ,
stateName : true
}
}
}
}
2026-02-19 16:25:43 +05:30
} ,
} ,
} ,
} ) ;
}
2026-02-19 15:56:26 +05:30
2026-03-18 13:30:41 +05:30
async searchConnectionPeople ( userXid : number , searchQuery? : string ) {
const userConnectionDetails = await this . prisma . connectDetails . findMany ( {
where : {
userXid ,
isActive : true ,
deletedAt : null ,
} ,
select : {
schoolCompanyXid : true ,
} ,
} ) ;
const schoolCompanyXids = [
. . . new Set ( userConnectionDetails . map ( ( item ) = > item . schoolCompanyXid ) ) ,
] ;
if ( ! schoolCompanyXids . length ) {
return {
count : 0 ,
people : [ ] ,
} ;
}
const trimmedSearchQuery = searchQuery ? . trim ( ) ? ? '' ;
const connectionPeople = await this . prisma . connectDetails . findMany ( {
where : {
isActive : true ,
deletedAt : null ,
schoolCompanyXid : { in : schoolCompanyXids } ,
userXid : { not : userXid } ,
user : {
isActive : true ,
deletedAt : null ,
. . . ( trimmedSearchQuery
? {
OR : [
{
firstName : {
contains : trimmedSearchQuery ,
mode : 'insensitive' ,
} ,
} ,
{
lastName : {
contains : trimmedSearchQuery ,
mode : 'insensitive' ,
} ,
} ,
] ,
}
: { } ) ,
} ,
} ,
distinct : [ 'userXid' ] ,
orderBy : {
createdAt : 'desc' ,
} ,
take : 10 ,
select : {
userXid : true ,
schoolCompany : {
select : {
id : true ,
schoolCompanyName : true ,
isSchool : true ,
} ,
} ,
user : {
select : {
id : true ,
firstName : true ,
lastName : true ,
profileImage : true ,
} ,
} ,
} ,
} ) ;
const people = await Promise . all (
connectionPeople . map ( async ( item ) = > {
const firstName = item . user . firstName ? . trim ( ) ? ? '' ;
const lastName = item . user . lastName ? . trim ( ) ? ? '' ;
const fullName = ` ${ firstName } ${ lastName } ` . trim ( ) ;
return {
userXid : item.user.id ,
fullName ,
firstName : item.user.firstName ,
lastName : item.user.lastName ,
profileImage : item.user.profileImage ,
profileImagePresignedUrl : await attachPresignedUrl (
item . user . profileImage ,
) ,
schoolCompany : item.schoolCompany ,
} ;
} ) ,
) ;
return {
count : people.length ,
people ,
} ;
}
2026-02-19 15:55:18 +05:30
async searchSchoolsAndCompanies ( searchQuery : string , isSchool : boolean ) {
if ( ! searchQuery ) {
throw new ApiError (
400 ,
'Search query is required to search for schools or companies' ,
) ;
}
2026-02-13 17:40:12 +05:30
2026-02-19 15:55:18 +05:30
const results = await this . prisma . schoolCompany . findMany ( {
where : {
schoolCompanyName : {
contains : searchQuery ,
mode : 'insensitive' ,
} ,
isSchool : isSchool ,
isActive : true ,
deletedAt : null ,
} ,
select : {
id : true ,
schoolCompanyName : true ,
2026-03-06 15:49:25 +05:30
cities : {
select : {
id : true ,
cityName : true ,
states : {
select : {
id : true ,
stateName : true
}
}
}
} ,
2026-03-06 17:36:14 +05:30
2026-02-19 15:55:18 +05:30
isSchool : true ,
isActive : true ,
createdAt : true ,
} ,
} ) ;
2026-02-13 17:40:12 +05:30
2026-02-19 15:55:18 +05:30
return results ;
}
async searchCities ( searchQuery : string ) {
if ( ! searchQuery || searchQuery . length < 2 ) {
throw new ApiError (
400 ,
'Search query must be at least 2 characters long' ,
) ;
2026-02-19 15:50:48 +05:30
}
2026-02-19 17:11:34 +05:30
2026-02-19 15:55:18 +05:30
const results = await this . prisma . cities . findMany ( {
where : {
cityName : {
contains : searchQuery ,
mode : 'insensitive' ,
} ,
isActive : true ,
deletedAt : null ,
} ,
select : {
id : true ,
cityName : true ,
stateXid : true ,
} ,
2026-02-23 18:56:08 +05:30
orderBy : {
cityName : 'asc' ,
} ,
take : 50 , // reduce latency by limiting results at DB level
2026-02-19 15:55:18 +05:30
} ) ;
return results ;
}
2026-02-19 16:25:43 +05:30
2026-02-19 20:05:33 +05:30
2026-02-19 17:11:29 +05:30
2026-02-19 16:25:43 +05:30
async addOrFindSchoolCompanyDetail ( dto : AddSchoolCompanyDetailDTO ) {
2026-02-20 14:46:37 +05:30
const { schoolCompanyName , isSchool , cityXid , userId } = dto ;
2026-02-19 20:05:33 +05:30
2026-02-19 17:11:29 +05:30
const normalizedName = normalizeName ( schoolCompanyName ) ;
2026-02-19 20:05:33 +05:30
2026-02-19 17:11:29 +05:30
// ✅ 1. Verify city exists
2026-02-19 16:25:43 +05:30
const cityExists = await this . prisma . cities . findFirst ( {
where : {
id : cityXid ,
isActive : true ,
deletedAt : null ,
} ,
} ) ;
2026-02-19 20:05:33 +05:30
2026-02-19 16:25:43 +05:30
if ( ! cityExists ) {
2026-02-19 17:11:29 +05:30
throw new ApiError ( 404 , "City not found" ) ;
2026-02-19 16:25:43 +05:30
}
2026-02-19 20:05:33 +05:30
2026-02-19 17:11:29 +05:30
// ✅ 2. Check existing (lowercase match)
2026-02-20 14:46:37 +05:30
let schoolCompany = await this . prisma . schoolCompany . findFirst ( {
2026-02-19 16:25:43 +05:30
where : {
2026-02-19 17:11:29 +05:30
schoolCompanyName : normalizedName ,
cityXid ,
isSchool ,
2026-02-19 16:25:43 +05:30
isActive : true ,
deletedAt : null ,
} ,
} ) ;
2026-02-19 20:05:33 +05:30
2026-02-20 14:46:37 +05:30
let isNewSchoolCompany = false ;
if ( ! schoolCompany ) {
schoolCompany = await this . prisma . schoolCompany . create ( {
data : {
schoolCompanyName : normalizedName ,
isSchool ,
cityXid ,
} ,
} ) ;
isNewSchoolCompany = true ;
}
// 4️ ⃣ Check if user already connected
const existingConnection = await this . prisma . connectDetails . findFirst ( {
where : {
userXid : userId ,
schoolCompanyXid : schoolCompany.id ,
isActive : true ,
} ,
} ) ;
if ( existingConnection ) {
2026-02-19 16:25:43 +05:30
return {
isNew : false ,
2026-02-20 14:46:37 +05:30
data : schoolCompany ,
message : "Already connected" ,
2026-02-19 16:25:43 +05:30
} ;
}
2026-02-19 20:05:33 +05:30
2026-02-20 14:46:37 +05:30
// 5️ ⃣ Create connectDetails safely
await this . prisma . connectDetails . create ( {
2026-02-19 16:25:43 +05:30
data : {
2026-02-20 14:46:37 +05:30
userXid : userId ,
schoolCompanyXid : schoolCompany.id ,
isActive : true ,
2026-02-19 16:25:43 +05:30
} ,
} ) ;
2026-02-19 20:05:33 +05:30
2026-02-20 14:46:37 +05:30
return true ;
2026-02-19 16:25:43 +05:30
}
2026-02-19 17:11:34 +05:30
2026-02-19 20:05:33 +05:30
async getAllActivitiesFromConnectionsUserInterests (
userId : number ,
schoolCompanyXids : number [ ] ,
page : number ,
limit : number ,
countryName : string ,
stateName : string ,
cityName : string ,
) {
const data = await this . prisma . $transaction ( async ( tx ) = > {
2026-02-19 17:52:43 +05:30
2026-03-06 17:36:14 +05:30
const userInterests = await tx . userInterests . findMany ( {
2026-02-19 20:05:33 +05:30
where : {
2026-03-06 17:36:14 +05:30
userXid : userId ,
2026-02-19 20:05:33 +05:30
isActive : true ,
2026-03-06 17:36:14 +05:30
} ,
distinct : [ 'interestXid' ] ,
select : {
interestXid : true ,
interest : {
select : {
id : true ,
interestName : true ,
interestColor : true ,
interestImage : true ,
displayOrder : true ,
}
}
}
} ) ;
if ( ! userInterests . length ) {
return {
interests : [ ] ,
mostHypedActivities : null ,
newArrivalsActivities : null ,
otherStatesActivities : null ,
overSeasActivities : null ,
} ;
}
const connectionUsers = await tx . connectDetails . findMany ( {
where : {
isActive : true ,
schoolCompanyXid : { in : schoolCompanyXids } ,
userXid : { not : userId } ,
2026-02-19 20:05:33 +05:30
} ,
select : {
userXid : true ,
} ,
} ) ;
2026-02-19 17:11:34 +05:30
2026-03-06 17:36:14 +05:30
const connectionUserIds = [
. . . new Set ( connectionUsers . map ( ( u ) = > u . userXid ) ) ,
] ;
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 ,
] )
) ;
2026-03-04 17:04:14 +05:30
2026-03-06 17:36:14 +05:30
if ( ! connectionUserIds . length ) {
2026-02-19 20:05:33 +05:30
return {
interests : [ ] ,
mostHypedActivities : null ,
newArrivalsActivities : null ,
otherStatesActivities : null ,
overSeasActivities : null ,
} ;
}
2026-02-19 17:11:34 +05:30
2026-03-06 17:36:14 +05:30
const connectionActivities = await tx . userBucketInterested . findMany ( {
2026-02-19 20:05:33 +05:30
where : {
2026-03-06 17:36:14 +05:30
userXid : { in : connectionUserIds } ,
2026-02-19 20:05:33 +05:30
isActive : true ,
} ,
select : {
2026-03-06 17:36:14 +05:30
activityXid : true ,
} ,
2026-02-19 20:05:33 +05:30
} ) ;
2026-02-19 17:11:34 +05:30
2026-03-06 17:36:14 +05:30
const connectionActivityIds = [
. . . new Set ( connectionActivities . map ( ( a ) = > a . activityXid ) ) ,
] ;
2026-02-19 17:11:34 +05:30
2026-03-06 17:36:14 +05:30
if ( ! connectionActivityIds . length ) {
2026-02-19 20:05:33 +05:30
return {
interests : [ ] ,
mostHypedActivities : null ,
newArrivalsActivities : null ,
otherStatesActivities : null ,
overSeasActivities : null ,
} ;
}
2026-02-19 17:11:34 +05:30
2026-02-19 20:05:33 +05:30
const activityTypes = await tx . activityTypes . findMany ( {
where : {
interestXid : {
2026-03-06 17:36:14 +05:30
in : userInterests . map ( i = > i . interestXid ) ,
2026-02-19 20:05:33 +05:30
} ,
isActive : true ,
} ,
select : { id : true }
} ) ;
if ( ! activityTypes . length ) {
return {
interests : [ ] ,
mostHypedActivities : null ,
newArrivalsActivities : null ,
otherStatesActivities : null ,
overSeasActivities : null ,
} ;
}
2026-02-19 17:11:34 +05:30
2026-03-06 17:36:14 +05:30
const activityTypeIds = activityTypes . map ( ( a ) = > a . id ) ;
2026-02-19 20:05:33 +05:30
const userAddressDetails = await tx . userAddressDetails . findFirst ( {
where : { userXid : userId } ,
select : {
stateXid : true ,
cityXid : true ,
countryXid : true ,
2026-03-05 19:21:16 +05:30
locationLat : true ,
locationLong : true ,
2026-02-19 20:05:33 +05:30
} ,
} ) ;
2026-02-19 17:11:34 +05:30
2026-03-05 19:21:16 +05:30
const userLatitude = userAddressDetails ? . locationLat ? ? null ;
const userLongitude = userAddressDetails ? . locationLong ? ? null ;
2026-02-19 20:05:33 +05:30
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 skip = ( page - 1 ) * limit ;
2026-02-19 17:11:34 +05:30
2026-02-19 20:05:33 +05:30
const effectiveCountryXid = effectiveLocation ? . countryXid ? ? null ;
const effectiveStateXid = effectiveLocation ? . stateXid ? ? null ;
2026-02-19 17:11:34 +05:30
2026-03-06 15:40:21 +05:30
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 ) ;
2026-02-19 20:05:33 +05:30
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
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 : {
2026-03-06 17:36:14 +05:30
id : { in : connectionActivityIds } , // 🔥 NEW FILTER
2026-02-19 20:05:33 +05:30
isActive : true ,
activityInternalStatus : ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED ,
amInternalStatus : ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED ,
2026-03-06 17:36:14 +05:30
activityTypeXid : { in : activityTypeIds } ,
2026-02-19 20:05:33 +05:30
} ,
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 ,
2026-02-19 17:11:34 +05:30
} ,
2026-02-19 20:05:33 +05:30
} ,
} ,
} ,
ActivityVenues : {
select : {
ActivityPrices : {
2026-02-19 17:11:34 +05:30
select : {
2026-02-19 20:05:33 +05:30
sellPrice : true ,
2026-02-19 17:11:34 +05:30
} ,
2026-02-19 20:05:33 +05:30
} ,
} ,
} ,
ActivitiesMedia : {
where : { isActive : true } ,
select : {
id : true ,
mediaFileName : true ,
mediaType : true ,
} ,
} ,
} ,
} ) ;
2026-02-19 17:11:34 +05:30
2026-02-19 20:05:33 +05:30
const mostHypedTotalCount = await tx . userBucketInterested . groupBy ( {
by : [ 'activityXid' ] ,
where : {
isActive : true ,
isBucket : false ,
Activities : {
2026-03-06 17:36:14 +05:30
id : { in : connectionActivityIds } ,
2026-02-19 20:05:33 +05:30
activityTypeXid : { in : activityTypeIds } ,
isActive : true ,
activityInternalStatus : ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED ,
amInternalStatus : ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED ,
}
}
} ) ;
2026-02-19 17:11:34 +05:30
2026-02-19 20:05:33 +05:30
const totalHypedActivities = mostHypedTotalCount . length ;
2026-02-19 17:11:34 +05:30
2026-02-19 20:05:33 +05:30
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
2 ️ ⃣ MOST HYPED ACTIVITIES ( RANKED )
=== === === === === === === === === === === === === === === === === == * /
const mostHypedGrouped = await tx . userBucketInterested . groupBy ( {
by : [ 'activityXid' ] ,
where : {
isActive : true ,
isBucket : false ,
Activities : {
2026-03-06 17:36:14 +05:30
id : { in : connectionActivityIds } ,
2026-02-19 20:05:33 +05:30
activityTypeXid : { in : activityTypeIds } ,
isActive : true ,
activityInternalStatus : ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED ,
amInternalStatus : ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED ,
}
} ,
_count : {
activityXid : true ,
} ,
orderBy : {
_count : {
activityXid : 'desc' ,
} ,
} ,
skip ,
take : limit ,
} ) ;
2026-02-19 17:11:34 +05:30
2026-02-19 20:05:33 +05:30
const mostHypedActivityIds = mostHypedGrouped . map ( a = > a . activityXid ) ;
2026-02-19 17:11:34 +05:30
2026-02-19 20:05:33 +05:30
// 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 }
} ,
select : {
id : true ,
activityTitle : true ,
sustainabilityScore : true ,
totalScore : true ,
activityType : {
select : {
energyLevel : {
select : {
id : true ,
energyLevelName : true ,
energyColor : true ,
energyIcon : true ,
2026-02-19 17:11:34 +05:30
} ,
2026-02-19 20:05:33 +05:30
} ,
} ,
} ,
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 } ,
} ,
} ,
} ,
} ,
} ) ;
const hypeCountMap = new Map (
mostHypedGrouped . map ( g = > [ g . activityXid , g . _count . activityXid ] )
) ;
// 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 : hypeCountMap.get ( act . id ) ? ? 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 ,
2026-02-28 11:54:32 +05:30
connectionInterestedCount :
connectionInterestMap . get ( activity . id ) ? ? 0 ,
distance : 0 ,
rating : 0 ,
2026-02-19 20:05:33 +05:30
hypeCount : activity.hypeCount ,
energyLevel : activity.activityType.energyLevel
? {
. . . activity . activityType . energyLevel ,
presignedUrl : await attachPresignedUrl (
activity . activityType . energyLevel . energyIcon
) ,
}
: null ,
media : await attachMediaWithPresignedUrl ( activity . ActivitiesMedia ) ,
} ) )
) ;
const formattedMostHypedActivities = {
page ,
limit ,
totalCount : totalHypedActivities ,
hasMore : skip + limit < totalHypedActivities ,
activities : mostHypedActivities ,
} ;
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
3 ️ ⃣ NEW ARRIVALS ( RANKED )
=== === === === === === === === === === === === === === === === === == * /
const newArrivalsWhere = {
2026-03-06 17:36:14 +05:30
id : { in : connectionActivityIds } ,
2026-02-19 20:05:33 +05:30
isActive : true ,
activityInternalStatus : ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED ,
amInternalStatus : ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED ,
activityTypeXid : { in : activityTypeIds } ,
createdAt : { gte : new Date ( Date . now ( ) - 31 * 24 * 60 * 60 * 1000 ) }
} ;
2026-02-27 17:24:39 +05:30
const formattedNewArrivalsActivities = await rankAndPaginateActivities ( tx , newArrivalsWhere , page , limit , connectionInterestMap ) ;
2026-02-19 20:05:33 +05:30
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
4 ️ ⃣ OTHER STATES ACTIVITIES ( RANKED )
=== === === === === === === === === === === === === === === === === == * /
const otherStatesWhere : any = {
2026-03-06 17:36:14 +05:30
id : { in : connectionActivityIds } ,
2026-02-19 20:05:33 +05:30
isActive : true ,
activityInternalStatus : ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED ,
amInternalStatus : ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED ,
activityTypeXid : { in : activityTypeIds } ,
} ;
if ( effectiveCountryXid ) {
otherStatesWhere . checkInCountryXid = effectiveCountryXid ;
}
if ( effectiveStateXid ) {
otherStatesWhere . checkInStateXid = { not : effectiveStateXid } ;
}
2026-02-27 17:24:39 +05:30
const formattedOtherStatesActivities = await rankAndPaginateActivities ( tx , otherStatesWhere , page , limit , connectionInterestMap ) ;
2026-02-19 20:05:33 +05:30
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
5 ️ ⃣ OVERSEAS ACTIVITIES ( RANKED )
=== === === === === === === === === === === === === === === === === == * /
const overseasWhere : any = {
2026-03-06 17:36:14 +05:30
id : { in : connectionActivityIds } ,
2026-02-19 20:05:33 +05:30
isActive : true ,
activityInternalStatus : ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED ,
amInternalStatus : ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED ,
activityTypeXid : { in : activityTypeIds } ,
} ;
if ( effectiveCountryXid ) {
overseasWhere . checkInCountryXid = { not : effectiveCountryXid } ;
}
2026-02-27 17:24:39 +05:30
const formattedOverSeasActivities = await rankAndPaginateActivities ( tx , overseasWhere , page , limit , connectionInterestMap ) ;
2026-02-19 20:05:33 +05:30
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 ;
2026-03-05 19:21:16 +05:30
const distance = calculateDistance (
userLatitude ,
userLongitude ,
activity . checkInLat ,
activity . checkInLong ,
) ;
2026-02-19 20:05:33 +05:30
return {
interestXid : activity.activityType.interestXid ,
activityId : activity.id ,
activityTitle : activity.activityTitle ,
2026-02-27 17:24:39 +05:30
connectionInterestedCount :
connectionInterestMap . get ( activity . id ) ? ? 0 ,
2026-03-05 19:21:16 +05:30
distance ,
2026-02-28 11:54:32 +05:30
rating : 0 ,
2026-02-19 20:05:33 +05:30
activityDurationMins : activity.activityDurationMins ,
sustainabilityScore : activity.sustainabilityScore ,
cheapestPrice ,
energyLevel : activity.activityType.energyLevel
? {
. . . activity . activityType . energyLevel ,
presignedUrl : await attachPresignedUrl (
activity . activityType . energyLevel . energyIcon
) ,
}
: null ,
media : await attachMediaWithPresignedUrl ( activity . ActivitiesMedia ) ,
} ;
} )
) ;
const interestsWithActivities = await Promise . all (
2026-03-06 17:36:14 +05:30
userInterests
2026-02-19 20:05:33 +05:30
. sort ( ( a , b ) = >
a . interest . interestName . localeCompare ( b . interest . interestName )
)
. map ( async ( ui ) = > ( {
interestId : ui.interest.id ,
interestName : ui.interest.interestName ,
interestColor : ui.interest.interestColor ,
interestImage : ui.interest.interestImage ,
interestImagePresignedUrl : await attachPresignedUrl (
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 ) ,
} ) )
) ;
2026-03-09 16:00:31 +05:30
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
RANDOM ACTIVITIES FROM CONNECTION USERS ( 5 COVER IMAGES )
=== === === === === === === === === === === === === === === === === == * /
2026-02-19 20:05:33 +05:30
2026-03-25 13:34:12 +05:30
let randomActivities : any [ ] = [ ] ;
const eligibleRandomActivityIds = await tx . activities . findMany ( {
2026-03-09 16:00:31 +05:30
where : {
id : { in : connectionActivityIds } ,
isActive : true ,
activityInternalStatus : ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED ,
amInternalStatus : ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED ,
activityTypeXid : { in : activityTypeIds } ,
deletedAt : null ,
2026-03-25 13:34:12 +05:30
ActivitiesMedia : {
some : {
isActive : true ,
isCoverImage : true ,
} ,
} ,
} ,
select : {
id : true ,
2026-03-09 16:00:31 +05:30
} ,
} ) ;
2026-03-25 13:34:12 +05:30
if ( eligibleRandomActivityIds . length > 0 ) {
const takeCount = Math . min ( 5 , eligibleRandomActivityIds . length ) ;
const selectedIds = eligibleRandomActivityIds
. sort ( ( ) = > Math . random ( ) - 0.5 )
. slice ( 0 , takeCount )
. map ( ( activity ) = > activity . id ) ;
2026-03-09 16:00:31 +05:30
2026-03-25 13:34:12 +05:30
const randomFetched = await tx . activities . findMany ( {
where : {
id : { in : selectedIds } ,
} ,
select : {
id : true ,
activityTitle : true ,
ActivitiesMedia : {
where : { isActive : true , isCoverImage : true } ,
orderBy : { displayOrder : 'asc' } ,
take : 1 ,
2026-03-09 16:00:31 +05:30
select : {
2026-03-25 13:34:12 +05:30
mediaFileName : true ,
2026-03-09 16:00:31 +05:30
} ,
2026-03-25 13:34:12 +05:30
} ,
} ,
} ) ;
2026-03-09 16:00:31 +05:30
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 attachPresignedUrl ( cover . mediaFileName )
: null ,
} ;
} ) ,
) ;
}
2026-02-19 20:05:33 +05:30
return {
experiencesLogged : 25 ,
citiesDiscovered : 10 ,
loggedInNetworkCount : 0 ,
citiesInNetworkCount : 0 ,
2026-03-09 16:00:31 +05:30
randomActivities ,
2026-03-06 15:40:21 +05:30
interestedCount : userInterestedActivityIds.length ,
bucketCount : userBucketActivityIds.length ,
2026-02-19 20:05:33 +05:30
pagination : {
page ,
limit ,
} ,
interests : interestsWithActivities ,
otherStatesActivities : formattedOtherStatesActivities ,
overSeasActivities : formattedOverSeasActivities ,
newArrivalsActivities : formattedNewArrivalsActivities ,
mostHypedActivities : formattedMostHypedActivities ,
} ;
} ) ;
return data ;
}
async viewMoreActivitiesByInterest (
interestId : number ,
page : number ,
limit : number
) {
return await this . prisma . $transaction ( async ( tx ) = > {
const skip = ( page - 1 ) * limit ;
// 1️ ⃣ Get activity types under this interest
const activityTypes = await tx . activityTypes . findMany ( {
where : {
interestXid : interestId ,
isActive : true ,
} ,
select : { id : true } ,
} ) ;
if ( ! activityTypes . length ) {
return {
interestId ,
page ,
limit ,
totalCount : 0 ,
hasMore : false ,
activities : [ ] ,
} ;
}
// 2️ ⃣ Total Count
const totalCount = await tx . activities . count ( {
where : {
isActive : true ,
activityInternalStatus : ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED ,
amInternalStatus : ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED ,
activityTypeXid : {
in : activityTypes . map ( ( a ) = > a . id ) ,
} ,
} ,
} ) ;
// 3️ ⃣ Fetch Paginated 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 : activityTypes . map ( ( a ) = > a . id ) ,
} ,
} ,
skip ,
take : limit ,
orderBy : { id : 'desc' } ,
select : {
id : true ,
activityTitle : true ,
activityDurationMins : true ,
sustainabilityScore : true ,
totalScore : true ,
activityType : {
select : {
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 ,
} ,
} ,
} ,
} ) ;
// 4️ ⃣ Format Response
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 {
activityId : activity.id ,
activityTitle : activity.activityTitle ,
activityDurationMins : activity.activityDurationMins ,
sustainabilityScore : activity.sustainabilityScore ,
cheapestPrice ,
energyLevel : activity.activityType.energyLevel
? {
. . . activity . activityType . energyLevel ,
presignedUrl : await attachPresignedUrl (
activity . activityType . energyLevel . energyIcon
) ,
}
: null ,
media : await attachMediaWithPresignedUrl ( activity . ActivitiesMedia ) ,
} ;
} )
) ;
return {
interestId ,
page ,
limit ,
totalCount ,
hasMore : skip + limit < totalCount ,
activities : formattedActivities ,
} ;
} ) ;
}
2026-02-23 19:49:05 +05:30
async viewMoreActivities (
userId : number ,
type : string ,
page : number ,
limit : number ,
countryName? : string ,
stateName? : string ,
cityName? : string ,
) {
return await this . prisma . $transaction ( async ( tx ) = > {
const userAddressDetails = await tx . userAddressDetails . findFirst ( {
where : { userXid : userId } ,
select : {
countryXid : true ,
stateXid : true ,
cityXid : true ,
} ,
} ) ;
let effectiveLocation = null ;
if ( countryName && stateName && cityName ) {
effectiveLocation = await findOrCreateLocation ( tx , {
countryName ,
stateName ,
cityName ,
} ) ;
} else if ( userAddressDetails ) {
effectiveLocation = userAddressDetails ;
}
const effectiveCountryXid = effectiveLocation ? . countryXid ? ? null ;
const effectiveStateXid = effectiveLocation ? . stateXid ? ? null ;
2026-02-27 17:24:39 +05:30
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 . map ( u = > u . userXid ) ;
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 ,
] )
) ;
2026-02-23 19:49:05 +05:30
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
SWITCH BASED VIEW MORE TYPE
=== === === === === === === === === === === === === === === === === === = * /
switch ( type ) {
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
1 ️ ⃣ MOST HYPED
=== === === === === === === === === === === === === === * /
case 'mostHyped' : {
const grouped = await tx . userBucketInterested . groupBy ( {
by : [ 'activityXid' ] ,
where : {
isActive : true ,
isBucket : false ,
} ,
_count : {
activityXid : true ,
} ,
} ) ;
const sortedIds = grouped
. sort ( ( a , b ) = > b . _count . activityXid - a . _count . activityXid )
. map ( g = > g . activityXid ) ;
const where = {
id : { in : sortedIds } ,
isActive : true ,
activityInternalStatus : ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED ,
amInternalStatus : ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED ,
} ;
2026-02-27 17:24:39 +05:30
return await rankAndPaginateActivities ( tx , where , page , limit , connectionInterestMap ) ;
2026-02-23 19:49:05 +05:30
}
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
2 ️ ⃣ NEW ARRIVALS
=== === === === === === === === === === === === === === * /
case 'newArrivals' : {
const where = {
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 ) ,
} ,
} ;
2026-02-27 17:24:39 +05:30
return await rankAndPaginateActivities ( tx , where , page , limit , connectionInterestMap ) ;
2026-02-23 19:49:05 +05:30
}
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
3 ️ ⃣ OTHER STATES
=== === === === === === === === === === === === === === * /
case 'otherStates' : {
const where : any = {
isActive : true ,
activityInternalStatus : ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED ,
amInternalStatus : ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED ,
} ;
if ( effectiveCountryXid ) {
where . checkInCountryXid = effectiveCountryXid ;
}
if ( effectiveStateXid ) {
where . checkInStateXid = { not : effectiveStateXid } ;
}
2026-02-27 17:24:39 +05:30
return await rankAndPaginateActivities ( tx , where , page , limit , connectionInterestMap ) ;
2026-02-23 19:49:05 +05:30
}
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
4 ️ ⃣ OVERSEAS
=== === === === === === === === === === === === === === * /
case 'overSeas' : {
const where : any = {
isActive : true ,
activityInternalStatus : ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED ,
amInternalStatus : ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED ,
} ;
if ( effectiveCountryXid ) {
where . checkInCountryXid = { not : effectiveCountryXid } ;
}
2026-02-27 17:24:39 +05:30
return await rankAndPaginateActivities ( tx , where , page , limit , connectionInterestMap ) ;
2026-02-23 19:49:05 +05:30
}
default :
throw new Error ( 'Invalid type' ) ;
}
} ) ;
}
2026-02-19 20:41:00 +05:30
async getConnectionCountOfUser ( userXid : number ) {
return await this . prisma . connectDetails . count ( {
where : {
userXid ,
isActive : true ,
} ,
} ) ;
}
async deleteConnectDetails ( userXid : number , connectDetailsXid : number ) {
if ( ! connectDetailsXid || isNaN ( connectDetailsXid ) ) {
throw new ApiError ( 400 , 'Invalid connection detail ID' ) ;
}
const existing = await this . prisma . connectDetails . findFirst ( {
where : {
id : connectDetailsXid ,
userXid ,
isActive : true ,
} ,
} ) ;
if ( ! existing ) {
throw new ApiError ( 404 , 'Connection detail not found' ) ;
}
await this . prisma . connectDetails . delete ( {
where : {
id : connectDetailsXid
}
} ) ;
return true ;
}
2026-02-23 12:05:52 +05:30
async getRandomActiveActivity() {
return await this . prisma . $transaction ( async ( tx ) = > {
// Get count of active activities
const count = await tx . activities . count ( {
where : {
isActive : true ,
activityInternalStatus : ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED ,
amInternalStatus : ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED ,
deletedAt : null ,
} ,
} ) ;
if ( count === 0 ) {
return [ ] ;
}
// Determine how many activities to fetch (50 or less if count is smaller)
const takeCount = Math . min ( 50 , count ) ;
// Fetch random activities - using ORDER BY RANDOM() equivalent approach
// Get all IDs first, shuffle, then take 50
const allActivityIds = await tx . activities . findMany ( {
where : {
isActive : true ,
activityInternalStatus : ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED ,
amInternalStatus : ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED ,
deletedAt : null ,
} ,
select : { id : true } ,
} ) ;
// Shuffle array and take first 50
const shuffled = allActivityIds . sort ( ( ) = > Math . random ( ) - 0.5 ) ;
const selectedIds = shuffled . slice ( 0 , takeCount ) . map ( a = > a . id ) ;
// Fetch activities with only activityTitle and ActivitiesMedia
const activities = await tx . activities . findMany ( {
where : {
id : { in : selectedIds } ,
isActive : true ,
activityInternalStatus : ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED ,
amInternalStatus : ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED ,
deletedAt : null ,
} ,
select : {
id : true ,
activityTitle : true ,
ActivitiesMedia : {
2026-02-27 17:24:39 +05:30
where : {
2026-02-23 12:05:52 +05:30
isActive : true ,
} ,
select : {
id : true ,
mediaFileName : true ,
mediaType : true ,
} ,
orderBy : {
displayOrder : 'asc' , // Get the first image by display order
} ,
take : 1 , // Get only the first image
} ,
} ,
} ) ;
// Process activities to attach presigned URLs and format response
const result = await Promise . all (
activities . map ( async ( activity ) = > {
let activityImage = null ;
let activityImagePresignedUrl = null ;
// Get the first image and attach presigned URL
if ( Array . isArray ( activity . ActivitiesMedia ) && activity . ActivitiesMedia . length > 0 ) {
const firstImage = activity . ActivitiesMedia [ 0 ] ;
activityImage = firstImage . mediaFileName ;
activityImagePresignedUrl = await attachPresignedUrl ( firstImage . mediaFileName ) ;
}
return {
id : activity.id ,
activityName : activity.activityTitle ,
activityImage : activityImage ,
activityImagePresignedUrl : activityImagePresignedUrl ,
} ;
} )
) ;
return result ;
} ) ;
}
2026-02-27 17:24:39 +05:30
async getFiveRandomActivities() {
return await this . prisma . $transaction ( async ( tx ) = > {
2026-03-25 13:34:12 +05:30
const eligibleRandomActivityIds = await tx . activities . findMany ( {
2026-02-27 17:24:39 +05:30
where : {
isActive : true ,
activityInternalStatus : ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED ,
amInternalStatus : ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED ,
deletedAt : null ,
2026-03-25 13:34:12 +05:30
ActivitiesMedia : {
some : {
isActive : true ,
isCoverImage : true ,
} ,
} ,
} ,
select : {
id : true ,
2026-02-27 17:24:39 +05:30
} ,
} ) ;
2026-03-25 13:34:12 +05:30
if ( eligibleRandomActivityIds . length === 0 ) return [ ] ;
2026-02-27 17:24:39 +05:30
2026-03-25 13:34:12 +05:30
const takeCount = Math . min ( 5 , eligibleRandomActivityIds . length ) ;
const selectedIds = eligibleRandomActivityIds
. sort ( ( ) = > Math . random ( ) - 0.5 )
. slice ( 0 , takeCount )
. map ( ( activity ) = > activity . id ) ;
2026-02-27 17:24:39 +05:30
2026-03-25 13:34:12 +05:30
const activities = await tx . activities . findMany ( {
where : {
id : { in : selectedIds } ,
} ,
select : {
id : true ,
activityTitle : true ,
ActivitiesMedia : {
2026-02-27 17:24:39 +05:30
where : {
isActive : true ,
2026-03-25 13:34:12 +05:30
isCoverImage : true ,
} ,
orderBy : {
displayOrder : 'asc' ,
2026-02-27 17:24:39 +05:30
} ,
2026-03-25 13:34:12 +05:30
take : 1 ,
2026-02-27 17:24:39 +05:30
select : {
2026-03-25 13:34:12 +05:30
mediaFileName : true ,
2026-02-27 17:24:39 +05:30
} ,
2026-03-25 13:34:12 +05:30
} ,
} ,
} ) ;
2026-02-27 17:24:39 +05:30
// Step 4: Attach presigned URLs
const result = await Promise . all (
activities
. filter ( Boolean )
. map ( async ( activity ) = > {
const media = activity ! . ActivitiesMedia ? . [ 0 ] ;
let presignedUrl = null ;
if ( media ? . mediaFileName ) {
presignedUrl = await attachPresignedUrl ( media . mediaFileName ) ;
}
return {
id : activity ! . id ,
title : activity ! . activityTitle ,
coverImage : media?.mediaFileName ? ? null ,
coverImagePresignedUrl : presignedUrl ,
} ;
} )
) ;
return result ;
} ) ;
}
2026-02-28 13:11:26 +05:30
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 ( {
2026-03-25 13:34:12 +05:30
where : { userXid , activityXid , isActive : true } ,
2026-02-28 13:11:26 +05:30
} ) ;
if ( existing ) {
throw new ApiError ( 400 , 'Activity already added' ) ;
}
await this . prisma . userBucketInterested . create ( {
data : {
userXid ,
activityXid ,
isBucket ,
bucketTypeName ,
} ,
} ) ;
2026-03-10 19:36:44 +05:30
const latestActivityImage = await this . prisma . activities . findFirst ( {
where : {
id : activityXid ,
isActive : true ,
activityInternalStatus : ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED ,
amInternalStatus : ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED ,
} ,
select : {
ActivitiesMedia : {
where : {
isCoverImage : true
} ,
select : {
mediaFileName : true ,
}
}
}
} )
const coverImage = latestActivityImage ? . ActivitiesMedia ? . [ 0 ] ? . mediaFileName ? ? null ;
// Generate presigned URL
const coverImagePresignedUrl = await attachPresignedUrl ( coverImage ) ;
2026-02-28 13:11:26 +05:30
// ✅ 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 ,
2026-03-10 19:36:44 +05:30
coverImage ,
coverImagePresignedUrl ,
2026-02-28 13:11:26 +05:30
} ;
}
2026-03-06 15:49:25 +05:30
async removeFromBucketInterested (
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 , isActive : true } ,
} ) ;
if ( ! existing ) {
throw new ApiError ( 400 , 'Activity not found in bucket/interested list' ) ;
}
await this . prisma . userBucketInterested . update ( {
where : { id : existing.id } ,
data : {
isActive : false ,
} ,
} ) ;
// 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 ,
} ;
}
2026-03-13 13:09:25 +05:30
async getAllBucketActivities ( userXid : number ) {
const bucketActivities = await this . prisma . userBucketInterested . findMany ( {
where : {
userXid ,
isBucket : true ,
isActive : true ,
} ,
select : {
id : true ,
bucketTypeName : true ,
activityXid : true ,
Activities : {
select : {
activityTitle : true ,
ActivitiesMedia : {
where : {
isCoverImage : true ,
isActive : true ,
} ,
select : {
mediaFileName : true ,
} ,
} ,
} ,
} ,
} ,
} ) ;
const ready : any [ ] = [ ] ;
const planning : any [ ] = [ ] ;
const oneDay : any [ ] = [ ] ;
for ( const item of bucketActivities ) {
const media = item . Activities ? . ActivitiesMedia ? . [ 0 ] ? . mediaFileName ;
let presignedUrl = null ;
if ( media ) {
presignedUrl = await attachPresignedUrl ( media ) ;
// your presigned url function
}
const activityData = {
id : item.id ,
activityXid : item.activityXid ,
bucketTypeName : item.bucketTypeName ,
activityTitle : item.Activities?.activityTitle ,
coverImage : presignedUrl ,
} ;
if ( item . bucketTypeName === 'Ready' ) {
ready . push ( activityData ) ;
} else if ( item . bucketTypeName === 'Planning' ) {
planning . push ( activityData ) ;
} else if ( item . bucketTypeName === 'One-day' ) {
oneDay . push ( activityData ) ;
}
}
return {
ready ,
planning ,
oneDay ,
} ;
}
2026-03-18 13:30:41 +05:30
}