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-09 15:50:56 +05:30
import { ACTIVITY_AM_INTERNAL_STATUS , ACTIVITY_INTERNAL_STATUS } from '../../../common/utils/constants/host.constant' ;
import ApiError from '../../../common/utils/helper/ApiError' ;
import { UserPersonalInfoSchema } from '../../../common/utils/validation/user/addPersonalInfo.validation' ;
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-02-06 15:55:20 +05:30
async function findOrCreateLocation (
tx : any ,
{
countryName ,
stateName ,
cityName ,
} : {
countryName : string ;
stateName : string ;
cityName : string ;
}
) {
/ * - - - - - - - - - - - - - - - - - - - - - - - - - - -
1 ️ ⃣ COUNTRY
-- -- -- -- -- -- -- -- -- -- -- -- -- -- * /
let country = await tx . countries . findFirst ( {
where : {
countryName : {
equals : countryName ,
mode : 'insensitive' ,
} ,
isActive : true ,
} ,
} ) ;
if ( ! country ) {
country = await tx . countries . create ( {
data : {
countryName : countryName.trim ( ) ,
countryCode : countryName.slice ( 0 , 3 ) . toUpperCase ( ) , // optional
countryFlag : '' ,
isActive : true ,
} ,
} ) ;
}
/ * - - - - - - - - - - - - - - - - - - - - - - - - - - -
2 ️ ⃣ STATE
-- -- -- -- -- -- -- -- -- -- -- -- -- -- * /
let state = await tx . states . findFirst ( {
where : {
stateName : {
equals : stateName ,
mode : 'insensitive' ,
} ,
countryXid : country.id ,
isActive : true ,
} ,
} ) ;
if ( ! state ) {
state = await tx . states . create ( {
data : {
stateName : stateName.trim ( ) ,
countryXid : country.id ,
isActive : true ,
} ,
} ) ;
}
/ * - - - - - - - - - - - - - - - - - - - - - - - - - - -
3 ️ ⃣ CITY
-- -- -- -- -- -- -- -- -- -- -- -- -- -- * /
let city = await tx . cities . findFirst ( {
where : {
cityName : {
equals : cityName ,
mode : 'insensitive' ,
} ,
stateXid : state.id ,
isActive : true ,
} ,
} ) ;
if ( ! city ) {
city = await tx . cities . create ( {
data : {
cityName : cityName.trim ( ) ,
stateXid : state.id ,
isActive : true ,
} ,
} ) ;
}
return {
countryXid : country.id ,
stateXid : state.id ,
cityXid : city.id ,
} ;
}
2026-02-05 16:07:43 +05:30
const attachMediaWithPresignedUrl = async ( mediaArr = [ ] ) = > {
return (
await Promise . all (
mediaArr . map ( async ( m ) = > {
if ( ! m ? . mediaFileName ) return null ;
const key = m . mediaFileName . startsWith ( 'http' )
? new URL ( m . mediaFileName ) . pathname . replace ( /^\/+/ , '' )
: m . mediaFileName ;
return {
id : m.id ,
mediaType : m.mediaType ,
mediaFileName : m.mediaFileName ,
presignedUrl : await getPresignedUrl ( bucket , key ) ,
} ;
} )
)
) . filter ( Boolean ) ;
} ;
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 (
tx : any ,
whereClause : any ,
page : number ,
limit : number
) {
const skip = ( page - 1 ) * limit ;
// 1️ ⃣ Fetch Metadata for ALL matching activities for in-memory sorting
const allCandidates = await tx . activities . findMany ( {
where : whereClause ,
select : {
id : true ,
sustainabilityScore : true ,
totalScore : true , // Quality Score
ItineraryActivities : {
select : {
ActivityFeedbacks : {
select : { activityStars : true } ,
} ,
} ,
} ,
ActivityVenues : {
select : {
ActivityPrices : {
select : { sellPrice : true } ,
} ,
} ,
} ,
} ,
} ) ;
const totalCount = allCandidates . length ;
// 2️ ⃣ Calculate Metrics & Sort
const sortedCandidates = allCandidates . map ( ( act : any ) = > {
// Flatten feedbacks
const feedbacks = act . ItineraryActivities . flatMap ( ( ia : any ) = > ia . ActivityFeedbacks ) ;
// Avg Rating
const totalStars = feedbacks . reduce ( ( sum : number , f : any ) = > sum + f . activityStars , 0 ) ;
const avgRating = feedbacks . length > 0 ? totalStars / feedbacks.length : 0 ;
// Min Price
const prices = act . ActivityVenues . flatMap ( ( v : any ) = > v . ActivityPrices . map ( ( p : any ) = > p . sellPrice ) ) . filter ( ( p : any ) = > p !== null ) as number [ ] ;
const minPrice = prices . length > 0 ? Math . min ( . . . prices ) : Infinity ;
return {
id : act.id ,
avgRating ,
minPrice ,
sustainabilityScore : act.sustainabilityScore ? ? 0 ,
totalScore : act.totalScore ? ? 0 ,
} ;
} ) . sort ( ( a : any , b : any ) = > {
// 1. Rating (Highest first)
if ( b . avgRating !== a . avgRating ) return b . avgRating - a . avgRating ;
// 2. Price (Lowest first)
if ( a . minPrice !== b . minPrice ) return a . minPrice - b . minPrice ;
// 3. Sustainability Score (Highest first)
if ( b . sustainabilityScore !== a . sustainabilityScore ) return b . sustainabilityScore - a . sustainabilityScore ;
// 4. Quality Score (Highest first)
return b . totalScore - a . totalScore ;
} ) ;
// 3️ ⃣ Paginate IDs
const paginatedCandidates = sortedCandidates . slice ( skip , skip + limit ) ;
const targetIds = paginatedCandidates . map ( ( c : any ) = > c . id ) ;
// 4️ ⃣ Fetch Full Details for the page
const activitiesUnsorted = await tx . activities . findMany ( {
where : { id : { in : targetIds } } ,
select : {
id : true ,
activityTitle : true ,
activityDurationMins : true ,
sustainabilityScore : true ,
activityType : {
select : {
interestXid : true ,
energyLevel : {
select : {
id : true ,
energyLevelName : true ,
energyColor : true ,
energyIcon : true ,
} ,
} ,
} ,
} ,
ActivityVenues : {
select : {
ActivityPrices : {
select : { sellPrice : true } ,
} ,
} ,
} ,
ActivitiesMedia : {
where : { isActive : true } ,
select : {
id : true ,
mediaFileName : true ,
mediaType : true ,
} ,
} ,
} ,
} ) ;
// Re-sort to match the calculated order
const activities = targetIds
. map ( ( id : number ) = > activitiesUnsorted . find ( ( a : any ) = > a . id === id ) )
. filter ( Boolean ) ;
// 5️ ⃣ Format Response
const formattedActivities = await Promise . all (
activities . map ( async ( activity : any ) = > {
const prices = activity . ActivityVenues . flatMap ( ( v : any ) = > v . ActivityPrices . map ( ( p : any ) = > p . sellPrice ) ) . filter ( ( p : any ) = > p !== null ) as number [ ] ;
const cheapestPrice = prices . length > 0 ? Math . min ( . . . prices ) : null ;
return {
// interestXid: activity.activityType.interestXid,
activityId : activity.id ,
activityTitle : activity.activityTitle ,
// activityDurationMins: activity.activityDurationMins,
// sustainabilityScore: activity.sustainabilityScore,
// cheapestPrice,
energyLevel : activity.activityType.energyLevel ,
media : await attachMediaWithPresignedUrl ( activity . ActivitiesMedia ) ,
} ;
} )
) ;
return {
page ,
limit ,
totalCount ,
hasMore : skip + limit < totalCount ,
activities : formattedActivities ,
} ;
}
2026-02-04 15:32:11 +05:30
2026-01-16 17:50:30 +05:30
@Injectable ( )
export class UserService {
constructor ( private prisma : PrismaClient ) { }
async getUserById ( userId : number ) {
return this . prisma . user . findUnique ( {
where : { id : userId , isActive : true } ,
} ) ;
}
2026-02-01 10:02:35 +05:30
async addPersonalInfo ( userId : number , data : UserPersonalInfoSchema ) {
2026-01-16 17:50:30 +05:30
return await this . prisma . $transaction ( async ( tx ) = > {
2026-02-01 10:02:35 +05:30
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-23 17:56:46 +05:30
} ) ;
2026-01-16 17:50:30 +05:30
2026-02-01 10:02:35 +05:30
return updatedUser ;
} ) ;
}
async getAllInterestDetails() {
2026-02-05 16:07:43 +05:30
const interests = await this . prisma . interests . findMany ( {
2026-02-01 10:02:35 +05:30
where : { isActive : true } ,
select : {
id : true ,
interestName : true ,
interestColor : true ,
interestImage : true ,
displayOrder : true
2026-01-23 17:56:46 +05:30
}
} )
2026-02-05 16:07:43 +05:30
for ( const interest of interests ) {
if ( interest . interestImage ) {
const key = interest . interestImage . startsWith ( 'http' )
? new URL ( interest . interestImage ) . pathname . replace ( /^\/+/ , '' )
: interest . interestImage ;
( interest as any ) . presignedUrl = await getPresignedUrl ( bucket , key ) ;
} else {
( interest as any ) . presignedUrl = null ;
}
}
return interests ;
2026-01-23 17:56:46 +05:30
}
2026-02-01 10:02:35 +05:30
2026-01-23 17:56:46 +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 > {
const user = await this . prisma . user . findFirst ( {
where : { mobileNumber : mobileNumber , isActive : true } ,
select : {
id : true ,
mobileNumber : true ,
UserOtp : {
where : { isActive : true , isVerified : false } ,
orderBy : { createdAt : 'desc' } ,
take : 1 ,
} ,
} ,
} ) ;
if ( ! user ) {
throw new ApiError ( 404 , 'User not found.' ) ;
}
const userOtp = user . UserOtp [ 0 ] ;
if ( ! userOtp ) {
throw new ApiError ( 400 , 'No OTP found.' ) ;
}
if ( new Date ( ) > userOtp . expiresOn ) {
throw new ApiError ( 400 , 'OTP has expired.' ) ;
}
const isMatch = await bcrypt . compare ( otp , userOtp . otpCode ) ;
if ( ! isMatch ) {
throw new ApiError ( 400 , 'Invalid OTP.' ) ;
}
await this . prisma . userOtp . update ( {
where : { id : userOtp.id } ,
data : {
isVerified : true ,
verifiedOn : new Date ( ) ,
isActive : false ,
} ,
} ) ;
return true ;
}
2026-01-30 15:30:45 +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' ) ;
}
// Hash the passcode
const hashedPasscode = await bcrypt . hash ( userPasscode , 10 ) ;
// Update user with passcode
const updatedUser = await this . prisma . user . update ( {
where : { id : userId } ,
data : {
userPasscode : hashedPasscode ,
} ,
} ) ;
if ( ! updatedUser ) {
throw new ApiError ( 400 , 'Failed to set passcode' ) ;
}
return updatedUser ;
}
2026-02-02 14:50:23 +05:30
2026-02-10 15:14:11 +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 } ,
} ) ;
if ( ! user || ! user . userPasscode ) {
throw new ApiError ( 404 , 'User passcode not found' ) ;
}
const isMatch = await bcrypt . compare ( passcode , user . userPasscode ) ;
if ( ! isMatch ) {
return false ;
}
return true ;
}
2026-02-02 14:50:23 +05:30
async setUserInterests ( userId : number , interest_Xid : number [ ] ) : Promise < void > {
// Remove existing interests
await this . prisma . userInterests . deleteMany ( {
where : { userXid : userId } ,
} ) ;
// Add new interests
const interestRecords = interest_Xid . map ( ( interestId ) = > ( {
userXid : userId ,
interestXid : interestId ,
} ) ) ;
await this . prisma . userInterests . createMany ( {
data : interestRecords ,
} ) ;
}
2026-02-02 17:09:42 +05:30
async setUserLocationDetails (
userId : number ,
countryName : string ,
stateName : string ,
cityName : string ,
pinCode : string ,
latitude? : number ,
longitude? : number ,
locationName? : string ,
locationAddress? : string
2026-02-04 15:32:11 +05:30
) : Promise < UserAddressDetails > {
2026-02-02 17:09:42 +05:30
return this . prisma . $transaction ( async ( tx ) = > {
2026-02-04 15:32:11 +05:30
// 1️ ⃣ Country: find or create
let country = await tx . countries . findUnique ( {
where : { countryName } ,
select : { id : true } ,
2026-02-02 17:09:42 +05:30
} ) ;
2026-02-04 15:32:11 +05:30
if ( ! country ) {
country = await tx . countries . create ( {
data : {
countryName ,
countryCode : countryName.slice ( 0 , 3 ) . toUpperCase ( ) ,
countryFlag : '' ,
} ,
select : { id : true } ,
} ) ;
}
// 2️ ⃣ State: find or create (GLOBAL UNIQUE)
2026-02-11 15:04:09 +05:30
let state = await tx . states . findFirst ( {
where : { stateName , countryXid : country.id } ,
2026-02-04 15:32:11 +05:30
select : { id : true } ,
} ) ;
if ( ! state ) {
state = await tx . states . create ( {
data : {
stateName ,
countryXid : country.id ,
} ,
select : { id : true } ,
} ) ;
}
// 3️ ⃣ City: find or create (GLOBAL UNIQUE)
2026-02-11 15:04:09 +05:30
let city = await tx . cities . findFirst ( {
where : { cityName , stateXid : state.id } ,
2026-02-04 15:32:11 +05:30
select : { id : true } ,
2026-02-02 17:09:42 +05:30
} ) ;
2026-02-04 15:32:11 +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-02 17:09:42 +05:30
} ) ;
2026-02-04 15:32:11 +05:30
2026-02-02 17:09:42 +05:30
} ) ;
2026-02-04 15:32:11 +05:30
}
2026-02-06 15:55:20 +05:30
async getLandingPageAllDetails (
userId : number ,
page : number ,
limit : number ,
countryName : string ,
stateName : string ,
cityName : string ,
userLat : string ,
userLong : string
) {
2026-02-04 15:32:11 +05:30
const data = await this . prisma . $transaction ( async ( tx ) = > {
2026-02-06 15:55:20 +05:30
2026-02-05 16:07:43 +05:30
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-02-06 15:55:20 +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 ! ,
} ) ;
} else if ( userAddressDetails ) {
// ✅ Fallback to user’ s saved address
effectiveLocation = {
countryXid : userAddressDetails.countryXid ,
stateXid : userAddressDetails.stateXid ,
cityXid : userAddressDetails.cityXid ,
} ;
}
const effectiveCountryXid = effectiveLocation ? . countryXid ? ? null ;
const effectiveStateXid = effectiveLocation ? . stateXid ? ? null ;
2026-02-05 16:07:43 +05:30
2026-02-04 15:32:11 +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
}
} ,
}
} )
if ( ! userInterests . length ) {
2026-02-06 15:55:20 +05:30
return {
userAddressDetails ,
interests : [ ] ,
otherStatesActivities : null ,
overSeasActivities : null ,
} ;
2026-02-04 15:32:11 +05:30
}
const activitiyTypesOfUserInterests = await tx . activityTypes . findMany ( {
where : { interestXid : { in : userInterests . map ( ui = > ui . interestXid ) } , isActive : true } ,
select : {
id : true
}
} )
if ( ! activitiyTypesOfUserInterests . length ) {
return {
userAddressDetails ,
2026-02-06 15:55:20 +05:30
interests : [ ] ,
otherStatesActivities : null ,
overSeasActivities : null ,
2026-02-04 15:32:11 +05:30
} ;
}
2026-02-06 15:55:20 +05:30
const skip = ( page - 1 ) * limit ;
2026-02-04 15:32:11 +05:30
2026-02-11 18:49:28 +05:30
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
1 ️ ⃣ FETCH ALL CANDIDATES FOR INTERESTS ( SIMPLE SORT )
=== === === === === === === === === === === === === === === === === == * /
// Reverted to simple ID based sorting for Interest-based activities
2026-02-04 15:32:11 +05:30
const activities = await tx . activities . findMany ( {
where : {
isActive : true ,
activityInternalStatus : ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED ,
amInternalStatus : ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED ,
activityTypeXid : {
in : activitiyTypesOfUserInterests . map ( at = > at . id ) ,
} ,
} ,
2026-02-06 15:55:20 +05:30
skip ,
take : limit ,
orderBy : { id : 'desc' } ,
2026-02-04 15:32:11 +05:30
select : {
id : true ,
activityTitle : true ,
activityDurationMins : true ,
sustainabilityScore : true ,
checkInLat : true ,
checkInLong : true ,
activityType : {
select : {
2026-02-11 18:49:28 +05:30
interestXid : true ,
2026-02-04 15:32:11 +05:30
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 ,
} ,
} ,
} ,
} ) ;
2026-02-06 15:55:20 +05:30
const mostHypedTotalCount = await tx . userBucketInterested . groupBy ( {
by : [ 'activityXid' ] ,
2026-02-05 16:07:43 +05:30
where : {
isActive : true ,
2026-02-06 15:55:20 +05:30
isBucket : false ,
} ,
} ) ;
2026-02-05 16:07:43 +05:30
2026-02-11 18:49:28 +05:30
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
2 ️ ⃣ MOST HYPED ACTIVITIES ( RANKED )
=== === === === === === === === === === === === === === === === === == * /
2026-02-06 15:55:20 +05:30
const mostHypedGrouped = await tx . userBucketInterested . groupBy ( {
by : [ 'activityXid' ] ,
where : {
isActive : true ,
isBucket : false
} ,
_count : {
activityXid : true ,
} ,
orderBy : {
_count : {
activityXid : 'desc' ,
2026-02-05 16:07:43 +05:30
} ,
2026-02-06 15:55:20 +05:30
} ,
skip ,
take : limit ,
} ) ;
2026-02-05 16:07:43 +05:30
2026-02-06 15:55:20 +05:30
const totalHypedActivities = mostHypedTotalCount . length ;
const mostHypedActivityIds = mostHypedGrouped . map ( a = > a . activityXid ) ;
2026-02-11 18:49:28 +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:
2026-02-06 15:55:20 +05:30
const mostHypedActivitiesRaw = await tx . activities . findMany ( {
where : {
id : { in : mostHypedActivityIds } ,
isActive : true ,
activityInternalStatus : ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED ,
amInternalStatus : ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED ,
2026-02-05 16:07:43 +05:30
} ,
select : {
id : true ,
activityTitle : true ,
2026-02-11 18:49:28 +05:30
sustainabilityScore : true ,
totalScore : true ,
2026-02-05 16:07:43 +05:30
activityType : {
select : {
energyLevel : {
select : {
id : true ,
energyLevelName : true ,
energyColor : true ,
energyIcon : true ,
} ,
} ,
} ,
} ,
ActivitiesMedia : {
where : { isActive : true } ,
select : {
id : true ,
mediaFileName : true ,
mediaType : true ,
} ,
} ,
2026-02-11 18:49:28 +05:30
// Fetch ranking metadata
ItineraryActivities : {
select : {
ActivityFeedbacks : {
select : { activityStars : true } ,
} ,
} ,
} ,
ActivityVenues : {
select : {
ActivityPrices : {
select : { sellPrice : true } ,
} ,
} ,
} ,
2026-02-05 16:07:43 +05:30
} ,
} ) ;
2026-02-11 18:49:28 +05:30
// 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 ;
2026-02-06 15:55:20 +05:30
2026-02-11 18:49:28 +05:30
return {
. . . act , // Keep original fields for final output
avgRating ,
minPrice ,
sustainabilityScore : act.sustainabilityScore ? ? 0 ,
totalScore : act.totalScore ? ? 0 ,
hypeCount : mostHypedGrouped.find ( g = > g . activityXid === act . id ) ? . _count . activityXid ? ? 0
} ;
} ) . sort ( ( a , b ) = > {
// 1. Rating (Highest first)
if ( b . avgRating !== a . avgRating ) return b . avgRating - a . avgRating ;
// 2. Price (Lowest first)
if ( a . minPrice !== b . minPrice ) return a . minPrice - b . minPrice ;
// 3. Sustainability Score
if ( b . sustainabilityScore !== a . sustainabilityScore ) return b . sustainabilityScore - a . sustainabilityScore ;
// 4. Quality Score
return b . totalScore - a . totalScore ;
} ) ;
const mostHypedActivities = await Promise . all (
mostHypedSorted . map ( async activity = > ( {
activityId : activity.id ,
activityTitle : activity.activityTitle ,
hypeCount : activity.hypeCount ,
energyLevel : activity.activityType.energyLevel ,
media : await attachMediaWithPresignedUrl ( activity . ActivitiesMedia ) ,
} ) )
) ;
2026-02-06 15:55:20 +05:30
const formattedMostHypedActivities = {
page ,
limit ,
totalCount : totalHypedActivities ,
hasMore : skip + limit < totalHypedActivities ,
activities : mostHypedActivities ,
} ;
2026-02-11 18:49:28 +05:30
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
3 ️ ⃣ NEW ARRIVALS ( RANKED )
=== === === === === === === === === === === === === === === === === == * /
const newArrivalsWhere = {
isActive : true ,
activityInternalStatus : ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED ,
amInternalStatus : ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED ,
createdAt : { gte : new Date ( Date . now ( ) - 31 * 24 * 60 * 60 * 1000 ) }
} ;
2026-02-05 16:07:43 +05:30
2026-02-11 18:49:28 +05:30
const formattedNewArrivalsActivities = await rankAndPaginateActivities ( tx , newArrivalsWhere , page , limit ) ;
2026-02-04 15:32:11 +05:30
2026-02-06 15:55:20 +05:30
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
2026-02-11 18:49:28 +05:30
4 ️ ⃣ OTHER STATES ACTIVITIES ( RANKED )
=== === === === === === === === === === === === === === === === === == * /
2026-02-06 15:55:20 +05:30
const otherStatesWhere : any = {
isActive : true ,
activityInternalStatus : ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED ,
amInternalStatus : ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED ,
} ;
if ( effectiveCountryXid ) {
otherStatesWhere . checkInCountryXid = effectiveCountryXid ;
}
if ( effectiveStateXid ) {
otherStatesWhere . checkInStateXid = { not : effectiveStateXid } ;
}
2026-02-11 18:49:28 +05:30
const formattedOtherStatesActivities = await rankAndPaginateActivities ( tx , otherStatesWhere , page , limit ) ;
2026-02-06 15:55:20 +05:30
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
2026-02-11 18:49:28 +05:30
5 ️ ⃣ OVERSEAS ACTIVITIES ( RANKED )
2026-02-06 15:55:20 +05:30
=== === === === === === === === === === === === === === === === === == * /
const overseasWhere : any = {
isActive : true ,
activityInternalStatus : ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED ,
amInternalStatus : ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED ,
} ;
if ( effectiveCountryXid ) {
overseasWhere . checkInCountryXid = { not : effectiveCountryXid } ;
}
2026-02-11 18:49:28 +05:30
const formattedOverSeasActivities = await rankAndPaginateActivities ( tx , overseasWhere , page , limit ) ;
2026-02-06 15:55:20 +05:30
2026-02-04 15:32:11 +05:30
const formattedActivities = await Promise . all (
activities . map ( async ( activity ) = > {
2026-02-06 15:55:20 +05:30
const cheapestPrice =
activity . ActivityVenues . flatMap ( v = > v . ActivityPrices )
. map ( p = > p . sellPrice )
. filter ( Boolean )
. sort ( ( a , b ) = > a - b ) [ 0 ] ? ? null ;
2026-02-04 15:32:11 +05:30
return {
interestXid : activity.activityType.interestXid ,
2026-02-09 14:53:33 +05:30
activityId : activity.id ,
2026-02-04 15:32:11 +05:30
activityTitle : activity.activityTitle ,
activityDurationMins : activity.activityDurationMins ,
sustainabilityScore : activity.sustainabilityScore ,
cheapestPrice ,
2026-02-06 15:55:20 +05:30
energyLevel : activity.activityType.energyLevel ,
2026-02-05 16:07:43 +05:30
media : await attachMediaWithPresignedUrl ( activity . ActivitiesMedia ) ,
2026-02-04 15:32:11 +05:30
} ;
} )
) ;
2026-02-06 15:55:20 +05:30
const interestsWithActivities = [ . . . userInterests ]
. sort ( ( a , b ) = >
a . interest . interestName . localeCompare ( b . interest . interestName )
)
. map ( ui = > ( {
2026-02-04 15:32:11 +05:30
interestId : ui.interest.id ,
interestName : ui.interest.interestName ,
interestColor : ui.interest.interestColor ,
interestImage : ui.interest.interestImage ,
displayOrder : ui.interest.displayOrder ,
2026-02-06 15:55:20 +05:30
page ,
limit ,
hasMore : formattedActivities.length === limit ,
activities : formattedActivities
. filter ( a = > a . interestXid === ui . interestXid )
. map ( ( { interestXid , . . . rest } ) = > rest ) ,
} ) ) ;
2026-02-04 15:32:11 +05:30
return {
userAddressDetails ,
2026-02-05 16:07:43 +05:30
experiencesLogged : 25 ,
citiesDiscovered : 10 ,
loggedInNetworkCount : 0 ,
citiesInNetworkCount : 0 ,
2026-02-06 15:55:20 +05:30
pagination : {
page ,
limit ,
} ,
2026-02-04 15:32:11 +05:30
interests : interestsWithActivities ,
2026-02-11 18:49:28 +05:30
otherStatesActivities : formattedOtherStatesActivities ,
overSeasActivities : formattedOverSeasActivities ,
2026-02-06 15:55:20 +05:30
newArrivalsActivities : formattedNewArrivalsActivities ,
mostHypedActivities : formattedMostHypedActivities ,
2026-02-04 15:32:11 +05:30
} ;
} )
return data ;
}
2026-02-09 15:27:10 +05:30
async getSurpriseMeDetails (
userId : number ,
page : number ,
limit : number ,
countryName : string ,
stateName : string ,
cityName : string
) {
const data = await this . prisma . $transaction ( async ( tx ) = > {
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
1 ️ ⃣ USER LOCATION
=== === === === === === === === === === === === === === === === === == * /
const userAddressDetails = await tx . userAddressDetails . findFirst ( {
where : { userXid : userId } ,
select : {
stateXid : true ,
cityXid : true ,
countryXid : true ,
} ,
} ) ;
let effectiveLocation : {
countryXid? : number | null ;
stateXid? : number | null ;
cityXid? : number | null ;
} | null = null ;
if ( countryName && stateName && cityName ) {
effectiveLocation = await findOrCreateLocation ( tx , {
countryName ,
stateName ,
cityName ,
} ) ;
} else if ( userAddressDetails ) {
effectiveLocation = {
countryXid : userAddressDetails.countryXid ,
stateXid : userAddressDetails.stateXid ,
cityXid : userAddressDetails.cityXid ,
} ;
}
const effectiveCountryXid = effectiveLocation ? . countryXid ? ? null ;
const effectiveStateXid = effectiveLocation ? . stateXid ? ? null ;
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
2 ️ ⃣ USER INTERESTS ( TO EXCLUDE )
=== === === === === === === === === === === === === === === === === == * /
const userInterests = await tx . userInterests . findMany ( {
where : { userXid : userId , isActive : true } ,
select : { interestXid : true } ,
} ) ;
const userInterestTypeIds = await tx . activityTypes . findMany ( {
where : {
interestXid : { in : userInterests . map ( i = > i . interestXid ) } ,
isActive : true ,
} ,
select : { id : true } ,
} ) ;
const excludedActivityTypeIds = userInterestTypeIds . map ( a = > a . id ) ;
const excludeUserInterestCondition =
excludedActivityTypeIds . length > 0
? { activityTypeXid : { notIn : excludedActivityTypeIds } }
: { } ;
const skip = ( page - 1 ) * limit ;
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
3 ️ ⃣ OTHER INTERESTS ( GROUPED WITH ACTIVITIES )
=== === === === === === === === === === === === === === === === === == * /
const otherInterests = await tx . interests . findMany ( {
where : {
isActive : true ,
id : { notIn : userInterests.map ( i = > i . interestXid ) } ,
} ,
orderBy : { interestName : 'asc' } ,
select : {
id : true ,
interestName : true ,
interestColor : true ,
interestImage : true ,
} ,
} ) ;
const otherInterestActivities = await tx . activities . findMany ( {
where : {
isActive : true ,
. . . excludeUserInterestCondition ,
} ,
skip ,
take : limit ,
orderBy : { id : 'desc' } ,
select : {
id : true ,
activityTitle : true ,
activityType : {
select : {
interestXid : true ,
energyLevel : true ,
} ,
} ,
ActivitiesMedia : {
where : { isActive : true } ,
select : { id : true , mediaFileName : true , mediaType : true } ,
} ,
} ,
} ) ;
const formattedOtherInterestActivities = await Promise . all (
otherInterestActivities . map ( async a = > ( {
interestXid : a.activityType.interestXid ,
activityId : a.id ,
activityTitle : a.activityTitle ,
energyLevel : a.activityType.energyLevel ,
media : await attachMediaWithPresignedUrl ( a . ActivitiesMedia ) ,
} ) )
) ;
const interestsWithActivities = otherInterests . map ( interest = > ( {
interestId : interest.id ,
interestName : interest.interestName ,
interestColor : interest.interestColor ,
interestImage : interest.interestImage ,
page ,
limit ,
hasMore : formattedOtherInterestActivities.length === limit ,
activities : formattedOtherInterestActivities.filter (
a = > a . interestXid === interest . id
) . map ( ( { interestXid , . . . rest } ) = > rest ) ,
} ) ) ;
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
4 ️ ⃣ MOST HYPED
=== === === === === === === === === === === === === === === === === == * /
const mostHypedGrouped = await tx . userBucketInterested . groupBy ( {
by : [ 'activityXid' ] ,
where : {
isActive : true ,
isBucket : false ,
Activities : excludeUserInterestCondition ,
} ,
_count : { activityXid : true } ,
orderBy : { _count : { activityXid : 'desc' } } ,
skip ,
take : limit ,
} ) ;
const totalHypedCount = (
await tx . userBucketInterested . groupBy ( {
by : [ 'activityXid' ] ,
where : {
isActive : true ,
isBucket : false ,
Activities : excludeUserInterestCondition ,
} ,
} )
) . length ;
const hypedActivities = await tx . activities . findMany ( {
where : { id : { in : mostHypedGrouped . map ( h = > h . activityXid ) } } ,
select : {
id : true ,
activityTitle : true ,
activityType : { select : { energyLevel : true } } ,
ActivitiesMedia : {
where : { isActive : true } ,
select : { id : true , mediaFileName : true , mediaType : true } ,
} ,
} ,
} ) ;
const mostHypedActivities = await Promise . all (
mostHypedGrouped . map ( async g = > {
const act = hypedActivities . find ( a = > a . id === g . activityXid ) ;
if ( ! act ) return null ;
return {
activityId : act.id ,
activityTitle : act.activityTitle ,
hypeCount : g._count.activityXid ,
energyLevel : act.activityType.energyLevel ,
media : await attachMediaWithPresignedUrl ( act . ActivitiesMedia ) ,
} ;
} )
) . then ( a = > a . filter ( Boolean ) ) ;
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
5 ️ ⃣ NEW ARRIVALS
=== === === === === === === === === === === === === === === === === == * /
const newArrivalsWhere = {
isActive : true ,
createdAt : { gte : new Date ( Date . now ( ) - 31 * 24 * 60 * 60 * 1000 ) } ,
. . . excludeUserInterestCondition ,
} ;
const newArrivalsCount = await tx . activities . count ( {
where : newArrivalsWhere ,
} ) ;
const newArrivalsRaw = await tx . activities . findMany ( {
where : newArrivalsWhere ,
skip ,
take : limit ,
orderBy : { id : 'desc' } ,
select : {
activityTitle : true ,
activityType : { select : { energyLevel : true } } ,
ActivitiesMedia : {
where : { isActive : true } ,
select : { id : true , mediaFileName : true , mediaType : true } ,
} ,
} ,
} ) ;
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
6 ️ ⃣ OTHER STATES & OVERSEAS
=== === === === === === === === === === === === === === === === === == * /
const otherStatesWhere : any = {
isActive : true ,
. . . excludeUserInterestCondition ,
} ;
if ( effectiveCountryXid ) otherStatesWhere . checkInCountryXid = effectiveCountryXid ;
if ( effectiveStateXid ) otherStatesWhere . checkInStateXid = { not : effectiveStateXid } ;
const overseasWhere : any = {
isActive : true ,
. . . excludeUserInterestCondition ,
} ;
if ( effectiveCountryXid ) overseasWhere . checkInCountryXid = { not : effectiveCountryXid } ;
const [ otherStatesCount , overseasCount ] = await Promise . all ( [
tx . activities . count ( { where : otherStatesWhere } ) ,
tx . activities . count ( { where : overseasWhere } ) ,
] ) ;
const [ otherStatesRaw , overseasRaw ] = await Promise . all ( [
tx . activities . findMany ( {
where : otherStatesWhere ,
skip ,
take : limit ,
select : {
activityTitle : true ,
activityType : { select : { energyLevel : true } } ,
ActivitiesMedia : {
where : { isActive : true } ,
select : { id : true , mediaFileName : true , mediaType : true } ,
} ,
} ,
} ) ,
tx . activities . findMany ( {
where : overseasWhere ,
skip ,
take : limit ,
select : {
activityTitle : true ,
activityType : { select : { energyLevel : true } } ,
ActivitiesMedia : {
where : { isActive : true } ,
select : { id : true , mediaFileName : true , mediaType : true } ,
} ,
} ,
} ) ,
] ) ;
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
7 ️ ⃣ FINAL RESPONSE
=== === === === === === === === === === === === === === === === === == * /
return {
pagination : { page , limit } ,
interests : interestsWithActivities ,
mostHypedActivities : {
page ,
limit ,
totalCount : totalHypedCount ,
hasMore : skip + limit < totalHypedCount ,
activities : mostHypedActivities ,
} ,
newArrivalsActivities : {
page ,
limit ,
totalCount : newArrivalsCount ,
hasMore : skip + limit < newArrivalsCount ,
activities : await Promise . all (
newArrivalsRaw . map ( async a = > ( {
activityTitle : a.activityTitle ,
energyLevel : a.activityType.energyLevel ,
media : await attachMediaWithPresignedUrl ( a . ActivitiesMedia ) ,
} ) )
) ,
} ,
otherStatesActivities : {
page ,
limit ,
totalCount : otherStatesCount ,
hasMore : skip + limit < otherStatesCount ,
activities : await Promise . all (
otherStatesRaw . map ( async a = > ( {
activityTitle : a.activityTitle ,
energyLevel : a.activityType.energyLevel ,
media : await attachMediaWithPresignedUrl ( a . ActivitiesMedia ) ,
} ) )
) ,
} ,
overSeasActivities : {
page ,
limit ,
totalCount : overseasCount ,
hasMore : skip + limit < overseasCount ,
activities : await Promise . all (
overseasRaw . map ( async a = > ( {
activityTitle : a.activityTitle ,
energyLevel : a.activityType.energyLevel ,
media : await attachMediaWithPresignedUrl ( a . ActivitiesMedia ) ,
} ) )
) ,
} ,
} ;
} ) ;
return data ;
}
2026-02-06 19:27:37 +05:30
async getActivityDetailsById (
userId : number ,
activityXid : number ) {
return await this . prisma . $transaction ( async ( tx ) = > {
const activity = await tx . activities . findUnique ( {
where : {
id : activityXid ,
isActive : true ,
activityInternalStatus : ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED ,
amInternalStatus : ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED
} ,
select : {
id : true ,
activityTitle : true ,
activityDurationMins : true ,
sustainabilityScore : true ,
checkInLat : true ,
checkInLong : true ,
activityRefNumber : true ,
checkInAddress : true ,
checkOutAddress : true ,
checkOutLat : true ,
checkOutLong : true ,
activityDescription : true ,
foodAvailable : true ,
foodIsChargeable : true ,
alcoholAvailable : true ,
trainerAvailable : true ,
trainerIsChargeable : true ,
pickUpDropAvailable : true ,
pickUpDropIsChargeable : true ,
inActivityAvailable : true ,
inActivityIsChargeable : true ,
isLateCheckingAllowed : true ,
equipmentAvailable : true ,
equipmentIsChargeable : true ,
cancellationAvailable : true ,
cancellationAllowedBeforeMins : true ,
activityType : {
select : {
interestXid : true , // ✅ VERY IMPORTANT
2026-02-09 14:53:33 +05:30
activityTypeName : true ,
2026-02-06 19:27:37 +05:30
energyLevel : {
select : {
id : true ,
energyLevelName : true ,
energyColor : true ,
energyIcon : true ,
} ,
} ,
} ,
} ,
ActivityOtherDetails : {
where : { isActive : true } ,
select : {
id : true ,
exclusiveNotes : true ,
dosNotes : true ,
dontsNotes : true ,
tipsNotes : true ,
termsAndCondition : true
}
} ,
ActivityEligibility : {
where : { isActive : true } ,
select : {
id : true ,
isAgeRestriction : true ,
ageRestrictionName : true ,
ageEntered : true ,
ageIn : true ,
minAge : true ,
maxAge : true ,
isWeightRestriction : true ,
weightRestrictionName : true ,
weightEntered : true ,
weightIn : true ,
minWeight : true ,
maxWeight : true ,
isHeightRestriction : true ,
heightRestrictionName : true ,
heightEntered : true ,
heightIn : true ,
minHeight : true ,
maxHeight : true
}
} ,
ActivityTrainers : {
where : { isActive : true } ,
select : {
id : true ,
totalAmount : true
}
} ,
ActivityAllowedEntry : {
where : { isActive : true } ,
select : {
id : true ,
allowedEntryTypeXid : true ,
allowedEntryType : {
select : {
id : true ,
allowedEntryTypeName : true
}
}
}
} ,
ActivityFoodCost : {
where : { isActive : true } ,
select : {
id : true ,
totalAmount : true
}
} ,
activityFoodTypes : {
where : { isActive : true } ,
select : {
id : true ,
foodTypeXid : true ,
foodType : {
select : {
id : true ,
foodTypeName : true
}
}
}
} ,
ActivityEquipments : {
where : { isActive : true } ,
select : {
id : true ,
equipmentName : true ,
isEquipmentChargeable : true ,
equipmentTotalPrice : true
}
} ,
ActivityNavigationModes : {
where : { isActive : true } ,
select : {
id : true ,
navigationModeXid : true ,
navigationMode : {
select : {
id : true ,
navigationModeName : true ,
navigationModeIcon : true
}
} ,
isInActivityChargeable : true ,
navigationModesTotalPrice : true
}
} ,
ActivityAmenities : {
where : { isActive : true } ,
select : {
id : true ,
amenitiesXid : true ,
amenities : {
select : {
id : true ,
amenitiesName : true
}
}
}
} ,
ActivityPickUpDetails : {
where : { isActive : true } ,
select : {
id : true ,
isPickUp : true ,
locationLat : true ,
locationLong : true ,
locationAddress : true ,
transportTotalPrice : true
}
} ,
activityCuisines : {
where : { isActive : true } ,
select : {
id : true ,
foodCuisineXid : true ,
foodCuisine : {
select : {
id : true ,
cuisineName : true
}
}
}
} ,
ActivityVenues : {
where : {
isActive : true
} ,
select : {
2026-02-09 15:50:56 +05:30
id : true ,
2026-02-06 19:27:37 +05:30
venueName : true ,
venueLabel : true ,
venueCapacity : true ,
availableSeats : true ,
isMinPeopleReqMandatory : true ,
minPeopleRequired : true ,
minReqfullfilledBeforeMins : true ,
venueDescription : true ,
ActivityPrices : {
select : {
id : true ,
sellPrice : true ,
} ,
} ,
} ,
} ,
ActivitiesMedia : {
where : { isActive : true } ,
select : {
id : true ,
mediaFileName : true ,
mediaType : true ,
} ,
} ,
} ,
} )
2026-02-02 17:09:42 +05:30
2026-02-06 19:27:37 +05:30
const interestedCount = await tx . userBucketInterested . count ( {
where : {
activityXid ,
isActive : true ,
} ,
} )
return {
activity ,
interestedCount ,
2026-02-09 14:53:33 +05:30
rating : 0 , // ⭐ Placeholder, implement rating logic as needed
distance : 0
2026-02-06 19:27:37 +05:30
}
} )
}
2026-01-16 17:50:30 +05:30
}