Files
MinglarBackendNestJS/src/modules/user/services/user.service.ts

566 lines
19 KiB
TypeScript
Raw Normal View History

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;
// }
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) {
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,
}
})
const userStateXid = userAddressDetails?.stateXid ?? null;
const userCountryXid = userAddressDetails?.countryXid ?? 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 { 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,
},
},
},
});
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,
},
},
},
})
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,
rating: 4,
distanceFromUser: 2,
connectionsCount: 10,
energyLevel: activity.activityType?.energyLevel ?? null,
media: await attachMediaWithPresignedUrl(activity.ActivitiesMedia),
};
})
);
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),
}))
);
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,
experiencesLogged: 25,
citiesDiscovered: 10,
loggedInNetworkCount: 0,
citiesInNetworkCount: 0,
interests: interestsWithActivities,
otherStatesActivities: formattedOtherStatesActivities,
overSeasActivities: formattedOverSeasActivities
};
})
return data;
}
}