1104 lines
38 KiB
TypeScript
1104 lines
38 KiB
TypeScript
import { Injectable } from '@nestjs/common';
|
||
import { PrismaClient, User, UserAddressDetails } from '@prisma/client';
|
||
import ApiError from '../../../common/utils/helper/ApiError';
|
||
import * as bcrypt from 'bcryptjs';
|
||
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';
|
||
import { isNotIn } from 'class-validator';
|
||
// function deg2rad(deg) {
|
||
// return deg * (Math.PI / 180);
|
||
// }
|
||
|
||
// function getDistanceFromLatLon(userLat1, userLon1, activityLat2, activityLon2) {
|
||
// const R = 6371; // Earth radius in km
|
||
|
||
// const dLat = deg2rad(activityLat2 - userLat1);
|
||
// const dLon = deg2rad(activityLon2 - userLon1);
|
||
|
||
// const a =
|
||
// Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
||
// Math.cos(deg2rad(userLat1)) *
|
||
// Math.cos(deg2rad(activityLat2)) *
|
||
// Math.sin(dLon / 2) *
|
||
// Math.sin(dLon / 2);
|
||
|
||
// const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||
// return R * c;
|
||
// }
|
||
|
||
async function findOrCreateLocation(
|
||
tx: any,
|
||
{
|
||
countryName,
|
||
stateName,
|
||
cityName,
|
||
}: {
|
||
countryName: string;
|
||
stateName: string;
|
||
cityName: string;
|
||
}
|
||
) {
|
||
/* ---------------------------
|
||
1️⃣ COUNTRY
|
||
----------------------------*/
|
||
let country = await tx.countries.findFirst({
|
||
where: {
|
||
countryName: {
|
||
equals: countryName,
|
||
mode: 'insensitive',
|
||
},
|
||
isActive: true,
|
||
},
|
||
});
|
||
|
||
if (!country) {
|
||
country = await tx.countries.create({
|
||
data: {
|
||
countryName: countryName.trim(),
|
||
countryCode: countryName.slice(0, 3).toUpperCase(), // optional
|
||
countryFlag: '',
|
||
isActive: true,
|
||
},
|
||
});
|
||
}
|
||
|
||
/* ---------------------------
|
||
2️⃣ STATE
|
||
----------------------------*/
|
||
let state = await tx.states.findFirst({
|
||
where: {
|
||
stateName: {
|
||
equals: stateName,
|
||
mode: 'insensitive',
|
||
},
|
||
countryXid: country.id,
|
||
isActive: true,
|
||
},
|
||
});
|
||
|
||
if (!state) {
|
||
state = await tx.states.create({
|
||
data: {
|
||
stateName: stateName.trim(),
|
||
countryXid: country.id,
|
||
isActive: true,
|
||
},
|
||
});
|
||
}
|
||
|
||
/* ---------------------------
|
||
3️⃣ CITY
|
||
----------------------------*/
|
||
let city = await tx.cities.findFirst({
|
||
where: {
|
||
cityName: {
|
||
equals: cityName,
|
||
mode: 'insensitive',
|
||
},
|
||
stateXid: state.id,
|
||
isActive: true,
|
||
},
|
||
});
|
||
|
||
if (!city) {
|
||
city = await tx.cities.create({
|
||
data: {
|
||
cityName: cityName.trim(),
|
||
stateXid: state.id,
|
||
isActive: true,
|
||
},
|
||
});
|
||
}
|
||
|
||
return {
|
||
countryXid: country.id,
|
||
stateXid: state.id,
|
||
cityXid: city.id,
|
||
};
|
||
}
|
||
|
||
|
||
const attachMediaWithPresignedUrl = async (mediaArr = []) => {
|
||
return (
|
||
await Promise.all(
|
||
mediaArr.map(async (m) => {
|
||
if (!m?.mediaFileName) return null;
|
||
|
||
const key = m.mediaFileName.startsWith('http')
|
||
? new URL(m.mediaFileName).pathname.replace(/^\/+/, '')
|
||
: m.mediaFileName;
|
||
|
||
return {
|
||
id: m.id,
|
||
mediaType: m.mediaType,
|
||
mediaFileName: m.mediaFileName,
|
||
presignedUrl: await getPresignedUrl(bucket, key),
|
||
};
|
||
})
|
||
)
|
||
).filter(Boolean);
|
||
};
|
||
|
||
|
||
|
||
const bucket = config.aws.bucketName;
|
||
|
||
|
||
@Injectable()
|
||
export class UserService {
|
||
constructor(private prisma: PrismaClient) { }
|
||
|
||
async getUserById(userId: number) {
|
||
return this.prisma.user.findUnique({
|
||
where: { id: userId, isActive: true },
|
||
});
|
||
}
|
||
|
||
async addPersonalInfo(userId: number, data: UserPersonalInfoSchema) {
|
||
return await this.prisma.$transaction(async (tx) => {
|
||
const updatedUser = await tx.user.update({
|
||
where: { id: userId },
|
||
data: {
|
||
firstName: data.firstName,
|
||
lastName: data.lastName ?? null,
|
||
genderName: data.genderName,
|
||
dateOfBirth: data.dateOfBirth
|
||
? new Date(data.dateOfBirth)
|
||
: null,
|
||
isProfileUpdated: true,
|
||
},
|
||
});
|
||
|
||
return updatedUser;
|
||
});
|
||
}
|
||
|
||
async getAllInterestDetails() {
|
||
const interests = await this.prisma.interests.findMany({
|
||
where: { isActive: true },
|
||
select: {
|
||
id: true,
|
||
interestName: true,
|
||
interestColor: true,
|
||
interestImage: true,
|
||
displayOrder: true
|
||
}
|
||
})
|
||
|
||
for (const interest of interests) {
|
||
if (interest.interestImage) {
|
||
const key = interest.interestImage.startsWith('http')
|
||
? new URL(interest.interestImage).pathname.replace(/^\/+/, '')
|
||
: interest.interestImage;
|
||
|
||
(interest as any).presignedUrl = await getPresignedUrl(bucket, key);
|
||
} else {
|
||
(interest as any).presignedUrl = null;
|
||
}
|
||
}
|
||
|
||
return interests;
|
||
}
|
||
|
||
|
||
async getUserByMobileNumber(mobileNumber: string): Promise<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;
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
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,
|
||
});
|
||
}
|
||
|
||
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 },
|
||
});
|
||
}
|
||
|
||
// 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 },
|
||
});
|
||
|
||
if (!city) {
|
||
city = await tx.cities.create({
|
||
data: {
|
||
cityName,
|
||
stateXid: state.id,
|
||
},
|
||
select: { id: true },
|
||
});
|
||
}
|
||
|
||
return tx.userAddressDetails.create({
|
||
data: {
|
||
user: { connect: { id: userId } },
|
||
country: { connect: { id: country.id } },
|
||
states: { connect: { id: state.id } },
|
||
cities: { connect: { id: city.id } },
|
||
address1: locationAddress ?? '',
|
||
pinCode,
|
||
locationName: locationName ?? null,
|
||
locationAddress: locationAddress ?? null,
|
||
locationLat: latitude ?? null,
|
||
locationLong: longitude ?? null,
|
||
},
|
||
});
|
||
|
||
});
|
||
}
|
||
|
||
async getLandingPageAllDetails(
|
||
userId: number,
|
||
page: number,
|
||
limit: number,
|
||
countryName: string,
|
||
stateName: string,
|
||
cityName: string,
|
||
userLat: string,
|
||
userLong: string
|
||
) {
|
||
const data = await this.prisma.$transaction(async (tx) => {
|
||
|
||
const userAddressDetails = await tx.userAddressDetails.findFirst({
|
||
where: { userXid: userId },
|
||
select: {
|
||
id: true,
|
||
address1: true,
|
||
address2: true,
|
||
pinCode: true,
|
||
locationName: true,
|
||
stateXid: true,
|
||
cityXid: true,
|
||
countryXid: true,
|
||
locationLat: true,
|
||
locationLong: true,
|
||
}
|
||
})
|
||
|
||
let effectiveLocation: {
|
||
countryXid?: number | null;
|
||
stateXid?: number | null;
|
||
cityXid?: number | null;
|
||
} | null = null;
|
||
|
||
const hasRequestLocation = countryName && stateName && cityName;
|
||
|
||
if (hasRequestLocation) {
|
||
// ✅ Create/find ONLY if request location is sent
|
||
effectiveLocation = await findOrCreateLocation(tx, {
|
||
countryName: countryName!,
|
||
stateName: stateName!,
|
||
cityName: cityName!,
|
||
});
|
||
} else if (userAddressDetails) {
|
||
// ✅ Fallback to user’s saved address
|
||
effectiveLocation = {
|
||
countryXid: userAddressDetails.countryXid,
|
||
stateXid: userAddressDetails.stateXid,
|
||
cityXid: userAddressDetails.cityXid,
|
||
};
|
||
}
|
||
|
||
const effectiveCountryXid = effectiveLocation?.countryXid ?? null;
|
||
const effectiveStateXid = effectiveLocation?.stateXid ?? null;
|
||
|
||
const userInterests = await tx.userInterests.findMany({
|
||
where: { userXid: userId, isActive: true },
|
||
select: {
|
||
id: true,
|
||
interestXid: true,
|
||
interest: {
|
||
select: {
|
||
id: true,
|
||
interestName: true,
|
||
interestColor: true,
|
||
interestImage: true,
|
||
displayOrder: true
|
||
}
|
||
},
|
||
}
|
||
})
|
||
|
||
if (!userInterests.length) {
|
||
return {
|
||
userAddressDetails,
|
||
interests: [],
|
||
otherStatesActivities: null,
|
||
overSeasActivities: null,
|
||
};
|
||
}
|
||
|
||
const activitiyTypesOfUserInterests = await tx.activityTypes.findMany({
|
||
where: { interestXid: { in: userInterests.map(ui => ui.interestXid) }, isActive: true },
|
||
select: {
|
||
id: true
|
||
}
|
||
})
|
||
|
||
if (!activitiyTypesOfUserInterests.length) {
|
||
return {
|
||
userAddressDetails,
|
||
interests: [],
|
||
otherStatesActivities: null,
|
||
overSeasActivities: null,
|
||
};
|
||
}
|
||
|
||
const skip = (page - 1) * limit;
|
||
|
||
const activities = await tx.activities.findMany({
|
||
where: {
|
||
isActive: true,
|
||
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||
activityTypeXid: {
|
||
in: activitiyTypesOfUserInterests.map(at => at.id),
|
||
},
|
||
},
|
||
skip,
|
||
take: limit,
|
||
orderBy: { id: 'desc' },
|
||
select: {
|
||
id: true,
|
||
activityTitle: true,
|
||
activityDurationMins: true,
|
||
sustainabilityScore: true,
|
||
checkInLat: true,
|
||
checkInLong: true,
|
||
|
||
activityType: {
|
||
select: {
|
||
interestXid: true, // ✅ 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,
|
||
},
|
||
},
|
||
},
|
||
});
|
||
|
||
const mostHypedTotalCount = await tx.userBucketInterested.groupBy({
|
||
by: ['activityXid'],
|
||
where: {
|
||
isActive: true,
|
||
isBucket: false,
|
||
},
|
||
});
|
||
|
||
const mostHypedGrouped = await tx.userBucketInterested.groupBy({
|
||
by: ['activityXid'],
|
||
where: {
|
||
isActive: true,
|
||
isBucket: false
|
||
},
|
||
_count: {
|
||
activityXid: true,
|
||
},
|
||
orderBy: {
|
||
_count: {
|
||
activityXid: 'desc',
|
||
},
|
||
},
|
||
skip,
|
||
take: limit,
|
||
});
|
||
|
||
const totalHypedActivities = mostHypedTotalCount.length;
|
||
|
||
const mostHypedActivityIds = mostHypedGrouped.map(a => a.activityXid);
|
||
|
||
const mostHypedActivitiesRaw = await tx.activities.findMany({
|
||
where: {
|
||
id: { in: mostHypedActivityIds },
|
||
isActive: true,
|
||
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||
},
|
||
select: {
|
||
id: true,
|
||
activityTitle: true,
|
||
activityType: {
|
||
select: {
|
||
energyLevel: {
|
||
select: {
|
||
id: true,
|
||
energyLevelName: true,
|
||
energyColor: true,
|
||
energyIcon: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
ActivitiesMedia: {
|
||
where: { isActive: true },
|
||
select: {
|
||
id: true,
|
||
mediaFileName: true,
|
||
mediaType: true,
|
||
},
|
||
},
|
||
},
|
||
});
|
||
|
||
const mostHypedActivities = await Promise.all(
|
||
mostHypedGrouped.map(async g => {
|
||
const activity = mostHypedActivitiesRaw.find(a => a.id === g.activityXid);
|
||
if (!activity) return null;
|
||
|
||
return {
|
||
activityId: activity.id,
|
||
activityTitle: activity.activityTitle,
|
||
hypeCount: g._count.activityXid, // 🔥 VERY IMPORTANT
|
||
energyLevel: activity.activityType.energyLevel,
|
||
media: await attachMediaWithPresignedUrl(activity.ActivitiesMedia),
|
||
};
|
||
})
|
||
).then(arr => arr.filter(Boolean));
|
||
|
||
const formattedMostHypedActivities = {
|
||
page,
|
||
limit,
|
||
totalCount: totalHypedActivities,
|
||
hasMore: skip + limit < totalHypedActivities,
|
||
activities: mostHypedActivities,
|
||
};
|
||
|
||
|
||
const newArrivalsCount = await tx.activities.count({
|
||
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) }
|
||
},
|
||
});
|
||
|
||
const newArrivalsActivities = 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),
|
||
// },
|
||
createdAt: { gte: new Date(Date.now() - 31 * 24 * 60 * 60 * 1000) }
|
||
},
|
||
skip,
|
||
take: limit,
|
||
orderBy: { id: 'desc' },
|
||
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,
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
/* =====================================================
|
||
6️⃣ OTHER STATES ACTIVITIES
|
||
===================================================== */
|
||
const otherStatesWhere: any = {
|
||
isActive: true,
|
||
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||
};
|
||
|
||
if (effectiveCountryXid) {
|
||
otherStatesWhere.checkInCountryXid = effectiveCountryXid;
|
||
}
|
||
if (effectiveStateXid) {
|
||
otherStatesWhere.checkInStateXid = { not: effectiveStateXid };
|
||
}
|
||
|
||
const otherStatesTotalCount = await tx.activities.count({
|
||
where: otherStatesWhere,
|
||
});
|
||
|
||
const otherStatesActivities = await tx.activities.findMany({
|
||
where: otherStatesWhere,
|
||
skip,
|
||
take: limit,
|
||
orderBy: { id: 'desc' },
|
||
select: {
|
||
activityTitle: true,
|
||
activityType: { select: { energyLevel: true } },
|
||
ActivitiesMedia: {
|
||
where: { isActive: true },
|
||
select: { id: true, mediaFileName: true, mediaType: true },
|
||
},
|
||
},
|
||
});
|
||
|
||
/* =====================================================
|
||
7️⃣ OVERSEAS ACTIVITIES
|
||
===================================================== */
|
||
const overseasWhere: any = {
|
||
isActive: true,
|
||
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||
};
|
||
|
||
if (effectiveCountryXid) {
|
||
overseasWhere.checkInCountryXid = { not: effectiveCountryXid };
|
||
}
|
||
|
||
const overSeasTotalCount = await tx.activities.count({
|
||
where: overseasWhere,
|
||
});
|
||
|
||
const overSeasActivities = await tx.activities.findMany({
|
||
where: overseasWhere,
|
||
skip,
|
||
take: limit,
|
||
orderBy: { id: 'desc' },
|
||
select: {
|
||
activityTitle: true,
|
||
activityType: { select: { energyLevel: true } },
|
||
ActivitiesMedia: {
|
||
where: { isActive: true },
|
||
select: { id: true, mediaFileName: true, mediaType: true },
|
||
},
|
||
},
|
||
});
|
||
|
||
const formattedActivities = await Promise.all(
|
||
activities.map(async (activity) => {
|
||
const cheapestPrice =
|
||
activity.ActivityVenues.flatMap(v => v.ActivityPrices)
|
||
.map(p => p.sellPrice)
|
||
.filter(Boolean)
|
||
.sort((a, b) => a - b)[0] ?? null;
|
||
|
||
return {
|
||
interestXid: activity.activityType.interestXid,
|
||
activityId: activity.id,
|
||
activityTitle: activity.activityTitle,
|
||
activityDurationMins: activity.activityDurationMins,
|
||
sustainabilityScore: activity.sustainabilityScore,
|
||
cheapestPrice,
|
||
energyLevel: activity.activityType.energyLevel,
|
||
media: await attachMediaWithPresignedUrl(activity.ActivitiesMedia),
|
||
};
|
||
})
|
||
);
|
||
|
||
|
||
const formattedOtherStatesActivities = {
|
||
page,
|
||
limit,
|
||
totalCount: otherStatesTotalCount,
|
||
hasMore: skip + limit < otherStatesTotalCount,
|
||
activities: await Promise.all(
|
||
otherStatesActivities.map(async a => ({
|
||
activityTitle: a.activityTitle,
|
||
energyLevel: a.activityType.energyLevel,
|
||
media: await attachMediaWithPresignedUrl(a.ActivitiesMedia),
|
||
}))
|
||
),
|
||
};
|
||
|
||
|
||
const formattedNewArrivalsActivities = {
|
||
page,
|
||
limit,
|
||
totalCount: newArrivalsCount,
|
||
hasMore: skip + limit < newArrivalsCount,
|
||
activities: await Promise.all(
|
||
newArrivalsActivities.map(async a => ({
|
||
activityTitle: a.activityTitle,
|
||
energyLevel: a.activityType.energyLevel,
|
||
media: await attachMediaWithPresignedUrl(a.ActivitiesMedia),
|
||
}))
|
||
),
|
||
};
|
||
|
||
const interestsWithActivities = [...userInterests]
|
||
.sort((a, b) =>
|
||
a.interest.interestName.localeCompare(b.interest.interestName)
|
||
)
|
||
.map(ui => ({
|
||
interestId: ui.interest.id,
|
||
interestName: ui.interest.interestName,
|
||
interestColor: ui.interest.interestColor,
|
||
interestImage: ui.interest.interestImage,
|
||
displayOrder: ui.interest.displayOrder,
|
||
page,
|
||
limit,
|
||
hasMore: formattedActivities.length === limit,
|
||
activities: formattedActivities
|
||
.filter(a => a.interestXid === ui.interestXid)
|
||
.map(({ interestXid, ...rest }) => rest),
|
||
}));
|
||
|
||
|
||
|
||
return {
|
||
userAddressDetails,
|
||
experiencesLogged: 25,
|
||
citiesDiscovered: 10,
|
||
loggedInNetworkCount: 0,
|
||
citiesInNetworkCount: 0,
|
||
pagination: {
|
||
page,
|
||
limit,
|
||
},
|
||
interests: interestsWithActivities,
|
||
otherStatesActivities: {
|
||
page,
|
||
limit,
|
||
totalCount: otherStatesTotalCount,
|
||
hasMore: skip + limit < otherStatesTotalCount,
|
||
activities: await Promise.all(
|
||
otherStatesActivities.map(async a => ({
|
||
activityTitle: a.activityTitle,
|
||
energyLevel: a.activityType.energyLevel,
|
||
media: await attachMediaWithPresignedUrl(a.ActivitiesMedia),
|
||
}))
|
||
),
|
||
},
|
||
overSeasActivities: {
|
||
page,
|
||
limit,
|
||
totalCount: overSeasTotalCount,
|
||
hasMore: skip + limit < overSeasTotalCount,
|
||
activities: await Promise.all(
|
||
overSeasActivities.map(async a => ({
|
||
activityTitle: a.activityTitle,
|
||
energyLevel: a.activityType.energyLevel,
|
||
media: await attachMediaWithPresignedUrl(a.ActivitiesMedia),
|
||
}))
|
||
),
|
||
},
|
||
newArrivalsActivities: formattedNewArrivalsActivities,
|
||
mostHypedActivities: formattedMostHypedActivities,
|
||
};
|
||
})
|
||
|
||
return data;
|
||
}
|
||
|
||
async getActivityDetailsById(
|
||
userId: number,
|
||
activityXid: number) {
|
||
return await this.prisma.$transaction(async (tx) => {
|
||
const activity = await tx.activities.findUnique({
|
||
where: {
|
||
id: activityXid,
|
||
isActive: true,
|
||
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED
|
||
},
|
||
select: {
|
||
id: true,
|
||
activityTitle: true,
|
||
activityDurationMins: true,
|
||
sustainabilityScore: true,
|
||
checkInLat: true,
|
||
checkInLong: true,
|
||
activityRefNumber: true,
|
||
checkInAddress: true,
|
||
checkOutAddress: true,
|
||
checkOutLat: true,
|
||
checkOutLong: true,
|
||
activityDescription: true,
|
||
foodAvailable: true,
|
||
foodIsChargeable: true,
|
||
alcoholAvailable: true,
|
||
trainerAvailable: true,
|
||
trainerIsChargeable: true,
|
||
pickUpDropAvailable: true,
|
||
pickUpDropIsChargeable: true,
|
||
inActivityAvailable: true,
|
||
inActivityIsChargeable: true,
|
||
isLateCheckingAllowed: true,
|
||
equipmentAvailable: true,
|
||
equipmentIsChargeable: true,
|
||
cancellationAvailable: true,
|
||
cancellationAllowedBeforeMins: true,
|
||
|
||
activityType: {
|
||
select: {
|
||
interestXid: true, // ✅ VERY IMPORTANT
|
||
activityTypeName: true,
|
||
energyLevel: {
|
||
select: {
|
||
id: true,
|
||
energyLevelName: true,
|
||
energyColor: true,
|
||
energyIcon: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
|
||
ActivityOtherDetails: {
|
||
where: { isActive: true },
|
||
select: {
|
||
id: true,
|
||
exclusiveNotes: true,
|
||
dosNotes: true,
|
||
dontsNotes: true,
|
||
tipsNotes: true,
|
||
termsAndCondition: true
|
||
}
|
||
},
|
||
|
||
ActivityEligibility: {
|
||
where: { isActive: true },
|
||
select: {
|
||
id: true,
|
||
isAgeRestriction: true,
|
||
ageRestrictionName: true,
|
||
ageEntered: true,
|
||
ageIn: true,
|
||
minAge: true,
|
||
maxAge: true,
|
||
isWeightRestriction: true,
|
||
weightRestrictionName: true,
|
||
weightEntered: true,
|
||
weightIn: true,
|
||
minWeight: true,
|
||
maxWeight: true,
|
||
isHeightRestriction: true,
|
||
heightRestrictionName: true,
|
||
heightEntered: true,
|
||
heightIn: true,
|
||
minHeight: true,
|
||
maxHeight: true
|
||
}
|
||
},
|
||
|
||
ActivityTrainers: {
|
||
where: { isActive: true },
|
||
select: {
|
||
id: true,
|
||
totalAmount: true
|
||
}
|
||
},
|
||
|
||
ActivityAllowedEntry: {
|
||
where: { isActive: true },
|
||
select: {
|
||
id: true,
|
||
allowedEntryTypeXid: true,
|
||
allowedEntryType: {
|
||
select: {
|
||
id: true,
|
||
allowedEntryTypeName: true
|
||
}
|
||
}
|
||
}
|
||
},
|
||
|
||
ActivityFoodCost: {
|
||
where: { isActive: true },
|
||
select: {
|
||
id: true,
|
||
totalAmount: true
|
||
}
|
||
},
|
||
|
||
activityFoodTypes: {
|
||
where: { isActive: true },
|
||
select: {
|
||
id: true,
|
||
foodTypeXid: true,
|
||
foodType: {
|
||
select: {
|
||
id: true,
|
||
foodTypeName: true
|
||
}
|
||
}
|
||
}
|
||
},
|
||
|
||
ActivityEquipments: {
|
||
where: { isActive: true },
|
||
select: {
|
||
id: true,
|
||
equipmentName: true,
|
||
isEquipmentChargeable: true,
|
||
equipmentTotalPrice: true
|
||
}
|
||
},
|
||
|
||
ActivityNavigationModes: {
|
||
where: { isActive: true },
|
||
select: {
|
||
id: true,
|
||
navigationModeXid: true,
|
||
navigationMode: {
|
||
select: {
|
||
id: true,
|
||
navigationModeName: true,
|
||
navigationModeIcon: true
|
||
}
|
||
},
|
||
isInActivityChargeable: true,
|
||
navigationModesTotalPrice: true
|
||
}
|
||
},
|
||
|
||
ActivityAmenities: {
|
||
where: { isActive: true },
|
||
select: {
|
||
id: true,
|
||
amenitiesXid: true,
|
||
amenities: {
|
||
select: {
|
||
id: true,
|
||
amenitiesName: true
|
||
}
|
||
}
|
||
}
|
||
},
|
||
|
||
ActivityPickUpDetails: {
|
||
where: { isActive: true },
|
||
select: {
|
||
id: true,
|
||
isPickUp: true,
|
||
locationLat: true,
|
||
locationLong: true,
|
||
locationAddress: true,
|
||
transportTotalPrice: true
|
||
}
|
||
},
|
||
|
||
activityCuisines: {
|
||
where: { isActive: true },
|
||
select: {
|
||
id: true,
|
||
foodCuisineXid: true,
|
||
foodCuisine: {
|
||
select: {
|
||
id: true,
|
||
cuisineName: true
|
||
}
|
||
}
|
||
}
|
||
},
|
||
|
||
ActivityVenues: {
|
||
where: {
|
||
isActive: true
|
||
},
|
||
select: {
|
||
venueName: true,
|
||
venueLabel: true,
|
||
venueCapacity: true,
|
||
availableSeats: true,
|
||
isMinPeopleReqMandatory: true,
|
||
minPeopleRequired: true,
|
||
minReqfullfilledBeforeMins: true,
|
||
venueDescription: true,
|
||
ActivityPrices: {
|
||
select: {
|
||
id: true,
|
||
sellPrice: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
|
||
ActivitiesMedia: {
|
||
where: { isActive: true },
|
||
select: {
|
||
id: true,
|
||
mediaFileName: true,
|
||
mediaType: true,
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
const interestedCount = await tx.userBucketInterested.count({
|
||
where: {
|
||
activityXid,
|
||
isActive: true,
|
||
},
|
||
})
|
||
|
||
return {
|
||
activity,
|
||
interestedCount,
|
||
rating: 0, // ⭐ Placeholder, implement rating logic as needed
|
||
distance: 0
|
||
}
|
||
})
|
||
}
|
||
} |