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-02-04 15:32:11 +05:30
|
|
|
|
import ApiError from '../../../common/utils/helper/ApiError';
|
2026-01-23 17:56:46 +05:30
|
|
|
|
import * as bcrypt from 'bcryptjs';
|
2026-02-04 15:32:11 +05:30
|
|
|
|
import { UserPersonalInfoSchema } from '../../../common/utils/validation/user/addPersonalInfo.validation';
|
|
|
|
|
|
import { ACTIVITY_AM_INTERNAL_STATUS, ACTIVITY_INTERNAL_STATUS } from '../../../common/utils/constants/host.constant';
|
|
|
|
|
|
import { getPresignedUrl } from '../../../common/middlewares/aws/getPreSignedUrl';
|
|
|
|
|
|
|
|
|
|
|
|
import config from '@/config/config';
|
2026-02-05 16:07:43 +05:30
|
|
|
|
import { isNotIn } from 'class-validator';
|
2026-02-04 15:32:11 +05:30
|
|
|
|
// 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-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-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
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
let state = await tx.states.findUnique({
|
|
|
|
|
|
where: { stateName },
|
|
|
|
|
|
select: { id: true },
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!state) {
|
|
|
|
|
|
state = await tx.states.create({
|
|
|
|
|
|
data: {
|
|
|
|
|
|
stateName,
|
|
|
|
|
|
countryXid: country.id,
|
|
|
|
|
|
},
|
|
|
|
|
|
select: { id: true },
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3️⃣ City: find or create (GLOBAL UNIQUE)
|
|
|
|
|
|
let city = await tx.cities.findUnique({
|
|
|
|
|
|
where: { cityName },
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async getLandingPageAllDetails(userId: number) {
|
|
|
|
|
|
const data = await this.prisma.$transaction(async (tx) => {
|
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,
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const userStateXid = userAddressDetails?.stateXid ?? null;
|
|
|
|
|
|
const userCountryXid = userAddressDetails?.countryXid ?? null;
|
|
|
|
|
|
|
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) {
|
|
|
|
|
|
return { userAddress: null, activities: [] };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const activitiyTypesOfUserInterests = await tx.activityTypes.findMany({
|
|
|
|
|
|
where: { interestXid: { in: userInterests.map(ui => ui.interestXid) }, isActive: true },
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
if (!activitiyTypesOfUserInterests.length) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
userAddressDetails,
|
|
|
|
|
|
activities: [],
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const activities = await tx.activities.findMany({
|
|
|
|
|
|
where: {
|
|
|
|
|
|
isActive: true,
|
|
|
|
|
|
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
|
|
|
|
|
|
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
|
|
|
|
|
|
activityTypeXid: {
|
|
|
|
|
|
in: activitiyTypesOfUserInterests.map(at => at.id),
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
activityTitle: true,
|
|
|
|
|
|
activityDurationMins: true,
|
|
|
|
|
|
sustainabilityScore: true,
|
|
|
|
|
|
checkInLat: true,
|
|
|
|
|
|
checkInLong: true,
|
|
|
|
|
|
|
|
|
|
|
|
activityType: {
|
|
|
|
|
|
select: {
|
|
|
|
|
|
interestXid: true, // ✅ VERY IMPORTANT
|
|
|
|
|
|
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-05 16:07:43 +05:30
|
|
|
|
const otherStatesActivities = await tx.activities.findMany({
|
|
|
|
|
|
where: {
|
|
|
|
|
|
isActive: true,
|
|
|
|
|
|
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
|
|
|
|
|
|
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ Only user's interest types
|
|
|
|
|
|
activityTypeXid: {
|
|
|
|
|
|
in: activitiyTypesOfUserInterests.map(at => at.id),
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ Exclude user's state
|
|
|
|
|
|
...(userStateXid && {
|
|
|
|
|
|
checkInStateXid: { not: userStateXid },
|
|
|
|
|
|
}),
|
|
|
|
|
|
...(userCountryXid && {
|
|
|
|
|
|
checkInCountryXid: userCountryXid,
|
|
|
|
|
|
}),
|
|
|
|
|
|
},
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
activityTitle: true,
|
|
|
|
|
|
activityType: {
|
|
|
|
|
|
select: {
|
|
|
|
|
|
energyLevel: {
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
energyLevelName: true,
|
|
|
|
|
|
energyColor: true,
|
|
|
|
|
|
energyIcon: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
ActivitiesMedia: {
|
|
|
|
|
|
where: { isActive: true },
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
mediaFileName: true,
|
|
|
|
|
|
mediaType: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const overSeasActivity = await tx.activities.findMany({
|
|
|
|
|
|
where: {
|
|
|
|
|
|
isActive: true,
|
|
|
|
|
|
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
|
|
|
|
|
|
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ Only user's interest types
|
|
|
|
|
|
activityTypeXid: {
|
|
|
|
|
|
in: activitiyTypesOfUserInterests.map(at => at.id),
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ Exclude user's state
|
|
|
|
|
|
...(userCountryXid && {
|
|
|
|
|
|
checkInCountryXid: { not: userCountryXid },
|
|
|
|
|
|
}),
|
|
|
|
|
|
},
|
|
|
|
|
|
select: {
|
|
|
|
|
|
id: true,
|
|
|
|
|
|
activityTitle: true,
|
|
|
|
|
|
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-04 15:32:11 +05:30
|
|
|
|
|
|
|
|
|
|
const formattedActivities = await Promise.all(
|
|
|
|
|
|
activities.map(async (activity) => {
|
|
|
|
|
|
let cheapestPrice: number | null = null;
|
|
|
|
|
|
|
|
|
|
|
|
for (const venue of activity.ActivityVenues) {
|
|
|
|
|
|
for (const price of venue.ActivityPrices) {
|
|
|
|
|
|
if (
|
|
|
|
|
|
typeof price.sellPrice === 'number' &&
|
|
|
|
|
|
(cheapestPrice === null || price.sellPrice < cheapestPrice)
|
|
|
|
|
|
) {
|
|
|
|
|
|
cheapestPrice = price.sellPrice;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
interestXid: activity.activityType.interestXid,
|
|
|
|
|
|
activityTitle: activity.activityTitle,
|
|
|
|
|
|
activityDurationMins: activity.activityDurationMins,
|
|
|
|
|
|
sustainabilityScore: activity.sustainabilityScore,
|
|
|
|
|
|
cheapestPrice,
|
2026-02-05 16:07:43 +05:30
|
|
|
|
rating: 4,
|
|
|
|
|
|
distanceFromUser: 2,
|
|
|
|
|
|
connectionsCount: 10,
|
|
|
|
|
|
energyLevel: activity.activityType?.energyLevel ?? null,
|
|
|
|
|
|
media: await attachMediaWithPresignedUrl(activity.ActivitiesMedia),
|
2026-02-04 15:32:11 +05:30
|
|
|
|
};
|
|
|
|
|
|
})
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-02-05 16:07:43 +05:30
|
|
|
|
const formattedOtherStatesActivities = await Promise.all(
|
|
|
|
|
|
otherStatesActivities.map(async (activity) => ({
|
|
|
|
|
|
activityTitle: activity.activityTitle,
|
|
|
|
|
|
energyLevel: activity.activityType.energyLevel,
|
|
|
|
|
|
media: await attachMediaWithPresignedUrl(activity.ActivitiesMedia),
|
|
|
|
|
|
}))
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const formattedOverSeasActivities = await Promise.all(
|
|
|
|
|
|
overSeasActivity.map(async (activity) => ({
|
|
|
|
|
|
activityTitle: activity.activityTitle,
|
|
|
|
|
|
energyLevel: activity.activityType.energyLevel,
|
|
|
|
|
|
media: await attachMediaWithPresignedUrl(activity.ActivitiesMedia),
|
|
|
|
|
|
}))
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-02-04 15:32:11 +05:30
|
|
|
|
const interestsWithActivities = userInterests.map(ui => {
|
|
|
|
|
|
const activitiesForInterest = formattedActivities.filter(
|
|
|
|
|
|
act => act.interestXid === ui.interestXid
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
interestId: ui.interest.id,
|
|
|
|
|
|
interestName: ui.interest.interestName,
|
|
|
|
|
|
interestColor: ui.interest.interestColor,
|
|
|
|
|
|
interestImage: ui.interest.interestImage,
|
|
|
|
|
|
displayOrder: ui.interest.displayOrder,
|
|
|
|
|
|
activities: activitiesForInterest.map(({ interestXid, ...rest }) => rest),
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
userAddressDetails,
|
2026-02-05 16:07:43 +05:30
|
|
|
|
experiencesLogged: 25,
|
|
|
|
|
|
citiesDiscovered: 10,
|
|
|
|
|
|
loggedInNetworkCount: 0,
|
|
|
|
|
|
citiesInNetworkCount: 0,
|
2026-02-04 15:32:11 +05:30
|
|
|
|
interests: interestsWithActivities,
|
2026-02-05 16:07:43 +05:30
|
|
|
|
otherStatesActivities: formattedOtherStatesActivities,
|
|
|
|
|
|
overSeasActivities: formattedOverSeasActivities
|
2026-02-04 15:32:11 +05:30
|
|
|
|
};
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
return data;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-02 17:09:42 +05:30
|
|
|
|
|
2026-01-16 17:50:30 +05:30
|
|
|
|
}
|