17 Commits

Author SHA1 Message Date
8f428fc1cb fixed the i am specific new api issue 2026-03-09 15:18:59 +05:30
0b503cf8bb sending the checkIn and checkOut location 2026-03-06 19:59:31 +05:30
7110d0462c Merge branch 'paritosh-main1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayankSprint2 2026-03-06 17:37:56 +05:30
96648fe37e sending the isBukcet and isInterested flag in getbyid api and changed the logic in the getactivitiesfrom connections api 2026-03-06 17:36:14 +05:30
paritosh18
2095f8e124 filtered landing page for specific search api 2026-03-06 17:11:21 +05:30
21c8799502 Merge branch 'paritosh-main1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayankSprint2 2026-03-06 15:49:46 +05:30
paritosh18
ad9e8e1a3f Added remove api from interested 2026-03-06 15:49:25 +05:30
b200e2cb94 sending the distance in the getbyid of activity 2026-03-06 15:49:13 +05:30
cae66237d2 fixed the check availability api and sending the interested and bucket count in the connection api 2026-03-06 15:40:21 +05:30
25be8a5647 sending the distance in connection activities api 2026-03-05 19:21:16 +05:30
7a4aecdd45 Merge branch 'paritosh-main1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayankSprint2 2026-03-05 18:58:34 +05:30
5cced2981a sending the distance in the apis 2026-03-05 18:57:58 +05:30
paritosh18
b9fbab3717 Merge branch 'mayankSprint2' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into paritosh-main1 2026-03-05 18:52:32 +05:30
paritosh18
90c897ad48 remove deleteMany call to allow reuse of refresh tokens in generateAuthToken method 2026-03-05 18:44:57 +05:30
4a069cc67a if there is no slot then the venue will not be sent 2026-03-05 18:12:35 +05:30
paritosh18
5d046c4bcf update ActivityOtherDetails model to change string fields to text type for better data handling 2026-03-05 16:54:43 +05:30
accfc4b769 sending the safety instruction and cancellations in the getbyid api of activity in user 2026-03-05 16:15:15 +05:30
11 changed files with 1594 additions and 168 deletions

View File

@@ -1038,13 +1038,13 @@ model ActivityOtherDetails {
id Int @id @default(autoincrement())
activityXid Int @map("activity_xid")
activity Activities @relation(fields: [activityXid], references: [id], onDelete: Cascade)
exclusiveNotes String? @map("exclusive_notes") @db.VarChar(500)
SafetyInstruction String? @map("safety_instruction") @db.VarChar(400)
Cancellations String? @map("cancellations") @db.VarChar(400)
dosNotes String? @map("dos_notes") @db.VarChar(400)
dontsNotes String? @map("donts_notes") @db.VarChar(400)
tipsNotes String? @map("tips_notes") @db.VarChar(400)
termsAndCondition String? @map("terms_and_condition") @db.VarChar(500)
exclusiveNotes String? @map("exclusive_notes") @db.Text
SafetyInstruction String? @map("safety_instruction") @db.Text
Cancellations String? @map("cancellations") @db.Text
dosNotes String? @map("dos_notes") @db.Text
dontsNotes String? @map("donts_notes") @db.Text
tipsNotes String? @map("tips_notes") @db.Text
termsAndCondition String? @map("terms_and_condition") @db.Text
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")

View File

@@ -1,21 +1,21 @@
# Host Module Functions
# All authentication and host management endpoints
getHosts:
handler: src/modules/host/handlers/host.handler
memorySize: 384
package:
patterns:
- 'src/modules/host/handlers/host.*'
- 'src/modules/host/services/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /host
method: get
# getHosts:
# handler: src/modules/host/handlers/host.handler
# memorySize: 384
# package:
# patterns:
# - 'src/modules/host/handlers/host.*'
# - 'src/modules/host/services/**'
# - ${file(./serverless/patterns/base.yml):pattern1}
# - ${file(./serverless/patterns/base.yml):pattern2}
# - ${file(./serverless/patterns/base.yml):pattern3}
# - ${file(./serverless/patterns/base.yml):pattern4}
# events:
# - httpApi:
# path: /host
# method: get
verifyOTP:
handler: src/modules/host/handlers/Host_Admin/onboarding/verifyOTP.handler

View File

@@ -362,3 +362,33 @@ addActivityToBucketInterested:
- httpApi:
path: /user/activities/add-to-bucket-interested
method: post
removeActivityFromBucketInterested:
handler: src/modules/user/handlers/activities/removeFromBucketInterested.handler
memorySize: 384
package:
patterns:
- 'src/modules/user/handlers/activities/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /user/activities/remove-from-bucket-interested
method: post
getFilteredLandingPageAllDetails:
handler: src/modules/user/handlers/activities/filteredLandingPageAllDetails.handler
memorySize: 512
package:
patterns:
- 'src/modules/user/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /user/activities/get-filtered-landing-page-details
method: get

View File

@@ -362,6 +362,9 @@ export class SchedulingService {
isActive: true,
startDate: { lte: date },
OR: [{ endDate: null }, { endDate: { gte: date } }],
ScheduleDetails: {
some: {}
}
},
include: {
activityVenue: {

View File

@@ -177,8 +177,8 @@ function computeBasePriceAndTaxes(
return { basePrice, taxDetails };
}
const normalize = (v?: string | null) =>
v ? v.trim().toLowerCase() : null;
const normalize = (v?: string | null, maxLength: number = 50) =>
v ? v.trim().toLowerCase().substring(0, maxLength) : null;
async function renderAgreementPdf(vars: {
effectiveDate: string;
@@ -338,9 +338,11 @@ const findOrCreateState = async (
) => {
if (!stateName || !countryXid) return null;
const trimmedStateName = stateName.trim().substring(0, 50);
const state = await tx.states.findFirst({
where: {
stateName: { equals: stateName.trim(), mode: 'insensitive' },
stateName: { equals: trimmedStateName, mode: 'insensitive' },
countryXid,
isActive: true,
},
@@ -350,7 +352,7 @@ const findOrCreateState = async (
const created = await tx.states.create({
data: {
stateName: stateName.trim(),
stateName: trimmedStateName,
countryXid,
},
});
@@ -365,9 +367,11 @@ const findOrCreateCity = async (
) => {
if (!cityName || !stateXid) return null;
const trimmedCityName = cityName.trim().substring(0, 50);
const city = await tx.cities.findFirst({
where: {
cityName: { equals: cityName.trim(), mode: 'insensitive' },
cityName: { equals: trimmedCityName, mode: 'insensitive' },
stateXid,
isActive: true,
},
@@ -377,7 +381,7 @@ const findOrCreateCity = async (
const created = await tx.cities.create({
data: {
cityName: cityName.trim(),
cityName: trimmedCityName,
stateXid,
},
});

View File

@@ -53,10 +53,10 @@ export class TokenService {
config.jwt.secret
);
await this.prisma.token.deleteMany({
where: { userXid: user_xid }
})
// Optionally keep existing refresh tokens alive instead of deleting
// Removed deleteMany call so the same refresh token can be used multiple
// times. If you want to limit refresh tokens later you can implement
// rotation or blacklist logic elsewhere.
await this.prisma.token.create({
data: {
token: refreshToken.token,

View File

@@ -50,14 +50,22 @@ export const handler = safeHandler(async (
const activity = activityDetails.activity;
// Rooms: combine ActivityVenues with their respective slots for the selected date
const Venues = (activity.ActivityVenues || []).map((v: any) => {
const header = scheduleDetails.find((h: any) => h.activityVenue?.venueXid === v.id);
const Venues = (activity.ActivityVenues || [])
.map((v: any) => {
const header = scheduleDetails.find(
(h: any) => h.activityVenue?.venueXid === v.id
);
const roomSlots = (header?.slots || []).map((s: any) => {
let status = 'Available';
if (s.maxCapacity === 0) status = 'Housefull';
else if (s.maxCapacity <= 2) status = '2 Slots Left';
else if (s.maxCapacity <= 5) status = 'Fast Filling';
if (!header || !header.slots?.length) {
return null; // ❌ venue has no slots for selected date
}
const roomSlots = header.slots.map((s: any) => {
let status = "Available";
if (s.maxCapacity === 0) status = "Housefull";
else if (s.maxCapacity <= 2) status = "2 Slots Left";
else if (s.maxCapacity <= 5) status = "Fast Filling";
return {
slotId: s.slotId,
@@ -81,33 +89,34 @@ export const handler = safeHandler(async (
venueMedia: (v.ActivityVenueArtifacts || []).map((media: any) => ({
id: media.id,
mediaType: media.mediaType,
mediaFileName: media.mediaFileName, // original S3 key / URL
presignedUrl: media.presignedUrl, // presigned URL
mediaFileName: media.mediaFileName,
presignedUrl: media.presignedUrl,
})),
};
});
})
.filter(Boolean); // ✅ removes null venues
// derive check-in/out from all room slots (earliest start, latest end)
const allSlots = Venues.flatMap(r => r.slots || []);
const startTimes = allSlots.map(s => s.startTime).filter(Boolean);
const endTimes = allSlots.map(s => s.endTime).filter(Boolean);
const checkInTime = startTimes.length ? startTimes.sort()[0] : null;
const checkOutTime = endTimes.length ? endTimes.sort().reverse()[0] : null;
// derive check-in/out from all room slots (earliest start, latest end)
const allSlots = Venues.flatMap(r => r.slots || []);
const startTimes = allSlots.map(s => s.startTime).filter(Boolean);
const endTimes = allSlots.map(s => s.endTime).filter(Boolean);
const checkInTime = startTimes.length ? startTimes.sort()[0] : null;
const checkOutTime = endTimes.length ? endTimes.sort().reverse()[0] : null;
const responsePayload = {
const responsePayload = {
selectedDate,
Venues,
checkInTime,
checkOutTime,
};
};
return {
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({ success: true, data: responsePayload }),
};
};
});

View File

@@ -0,0 +1,74 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
import { verifyUserToken } from '../../../../common/middlewares/jwt/authForUser';
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../common/utils/helper/ApiError';
import { FilteredLandingPageService } from '../../services/filteredLandingPage.service';
const filteredLandingPageService = new FilteredLandingPageService(prismaClient);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
// Extract token from headers
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) {
throw new ApiError(400, 'This is a protected route. Please provide a valid token.');
}
// Verify token and get user info
const userInfo = await verifyUserToken(token);
const userId = Number(userInfo.id);
if (!userId || isNaN(userId)) {
throw new ApiError(400, 'Invalid user ID');
}
const page = Number(event.queryStringParameters?.page ?? 1);
const limit = Number(event.queryStringParameters?.limit ?? 20);
const countryName = event.queryStringParameters?.countryName ?? '';
const stateName = event.queryStringParameters?.stateName ?? '';
const cityName = event.queryStringParameters?.cityName ?? '';
const userLat = event.queryStringParameters?.userLat ?? '';
const userLong = event.queryStringParameters?.userLong ?? '';
let activityTypeXids: number[] | undefined;
if (event.queryStringParameters?.activityTypeXids) {
try {
activityTypeXids = JSON.parse(event.queryStringParameters.activityTypeXids);
} catch (error) {
// Handle invalid JSON if needed
}
}
if (page < 1 || limit < 1) {
throw new ApiError(400, 'Invalid pagination values');
}
// Fetch filtered landing page details
const result = await filteredLandingPageService.getFilteredLandingPageAllDetails(
userId,
page,
limit,
countryName,
stateName,
cityName,
userLat,
userLong,
activityTypeXids
);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Filtered landing page data retrieved successfully',
data: result,
}),
};
});

View File

@@ -0,0 +1,71 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
import { verifyUserToken } from '../../../../common/middlewares/jwt/authForUser';
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../common/utils/helper/ApiError';
import { UserService } from '../../services/user.service';
const userService = new UserService(prismaClient);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context
): Promise<APIGatewayProxyResult> => {
// Extract token from headers
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token']
if (!token) {
throw new ApiError(400, 'This is a protected route. Please provide a valid token.');
}
// Authenticate user using verifyUserToken
const userInfo = await verifyUserToken(token);
const userId = userInfo.id;
if (Number.isNaN(userId)) {
throw new ApiError(400, 'User id must be a number');
}
const user = await userService.getUserById(userId);
if (!user) {
throw new ApiError(404, 'User not found');
}
// Parse request body
let body: { activityXid: number; isBucket: boolean; bucketTypeName: string; };
try {
body = event.body ? JSON.parse(event.body) : {};
} catch (error) {
throw new ApiError(400, 'Invalid JSON in request body');
}
const { activityXid, isBucket, bucketTypeName } = body;
// Validate required fields
if (
typeof activityXid !== 'number' ||
typeof isBucket !== 'boolean' ||
!bucketTypeName
) {
throw new ApiError(400, 'Required fields missing or invalid');
}
// Remove from bucket/interested
const counts = await userService.removeFromBucketInterested(userId, isBucket, bucketTypeName, activityXid);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: `Activity removed from ${isBucket ? 'bucket' : 'interested'} successfully`,
data: {
bucketCount: counts.bucketCount,
interestedCount: counts.interestedCount,
}
}),
};
});

View File

@@ -0,0 +1,949 @@
import { Injectable } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import { getPresignedUrl } from '../../../common/middlewares/aws/getPreSignedUrl';
import {
ACTIVITY_AM_INTERNAL_STATUS,
ACTIVITY_INTERNAL_STATUS,
} from '../../../common/utils/constants/host.constant';
import config from '../../../config/config';
const bucket = config.aws.bucketName;
@Injectable()
export class FilteredLandingPageService {
constructor(private readonly prisma: PrismaClient) {}
normalizeName = (name: string): string => {
return name
.toLowerCase()
.replace(/[^a-z0-9\s]/g, '')
.replace(/\s+/g, ' ')
.trim();
};
attachPresignedUrl = async (key: string | null): Promise<string | null> => {
if (!key) return null;
try {
return await getPresignedUrl(bucket, key);
} catch (error) {
console.error(`Failed to generate presigned URL for key: ${key}`, error);
return null;
}
};
findOrCreateLocation = async (
countryName: string,
stateName: string,
cityName: string,
tx: any,
) => {
const normalizedCountry = this.normalizeName(countryName);
const normalizedState = this.normalizeName(stateName);
const normalizedCity = this.normalizeName(cityName);
let country = await tx.countries.findFirst({
where: {
countryName: { contains: normalizedCountry, mode: 'insensitive' },
},
});
if (!country) {
country = await tx.countries.create({
data: {
countryName: countryName.trim(),
isActive: true,
createdAt: new Date(),
updatedAt: new Date(),
},
});
}
let state = await tx.states.findFirst({
where: {
countryXid: country.id,
stateName: { contains: normalizedState, mode: 'insensitive' },
},
});
if (!state) {
state = await tx.states.create({
data: {
countryXid: country.id,
stateName: stateName.trim(),
isActive: true,
createdAt: new Date(),
updatedAt: new Date(),
},
});
}
let city = await tx.cities.findFirst({
where: {
stateXid: state.id,
cityName: { contains: normalizedCity, mode: 'insensitive' },
},
});
if (!city) {
city = await tx.cities.create({
data: {
stateXid: state.id,
cityName: cityName.trim(),
isActive: true,
createdAt: new Date(),
updatedAt: new Date(),
},
});
}
return {
countryXid: country.id,
stateXid: state.id,
cityXid: city.id,
};
};
attachMediaWithPresignedUrl = async (mediaArr = []) => {
return (
await Promise.all(
mediaArr.map(async (m) => {
return {
...m,
presignedUrl: await this.attachPresignedUrl(m.mediaFileName),
};
}),
)
);
};
calculateDistance = (
lat1: number | null,
lon1: number | null,
lat2: number | null,
lon2: number | null,
) => {
if (!lat1 || !lon1 || !lat2 || !lon2) return null;
const R = 6371; // km
const dLat = ((lat2 - lat1) * Math.PI) / 180;
const dLon = ((lon2 - lon1) * Math.PI) / 180;
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos((lat1 * Math.PI) / 180) *
Math.cos((lat2 * Math.PI) / 180) *
Math.sin(dLon / 2) *
Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
};
async rankAndPaginateActivities(
tx: any,
whereClause: any,
page: number,
limit: number,
connectionInterestMap: Map<number, number>,
) {
const skip = (page - 1) * limit;
// Get total count
const totalCount = await tx.activities.count({ where: whereClause });
// Fetch activities with ranking metadata
const activities = await tx.activities.findMany({
where: whereClause,
skip,
take: limit,
select: {
id: true,
activityTitle: true,
sustainabilityScore: true,
totalScore: true,
activityType: {
select: {
interestXid: true,
energyLevel: {
select: {
id: true,
energyLevelName: true,
energyColor: true,
energyIcon: true,
},
},
},
},
ActivitiesMedia: {
where: { isActive: true },
select: {
id: true,
mediaFileName: true,
mediaType: true,
},
},
// Fetch ranking metadata
ItineraryActivities: {
select: {
ActivityFeedbacks: {
select: { activityStars: true },
},
},
},
ActivityVenues: {
select: {
ActivityPrices: {
select: { sellPrice: true },
},
},
},
},
});
// Sort and format
const sortedActivities = activities
.map((act) => {
const feedbacks = act.ItineraryActivities.flatMap(
(ia) => ia.ActivityFeedbacks,
);
const totalStars = feedbacks.reduce(
(sum, f) => sum + f.activityStars,
0,
);
const avgRating =
feedbacks.length > 0 ? totalStars / feedbacks.length : 0;
const prices = act.ActivityVenues.flatMap((v) =>
v.ActivityPrices.map((p) => p.sellPrice),
).filter((p) => p !== null) as number[];
const minPrice = prices.length > 0 ? Math.min(...prices) : Infinity;
return {
...act,
avgRating,
minPrice,
sustainabilityScore: act.sustainabilityScore ?? 0,
totalScore: act.totalScore ?? 0,
};
})
.sort((a, b) => {
if (b.avgRating !== a.avgRating) return b.avgRating - a.avgRating;
if (a.minPrice !== b.minPrice) return a.minPrice - b.minPrice;
if (b.sustainabilityScore !== a.sustainabilityScore)
return b.sustainabilityScore - a.sustainabilityScore;
return b.totalScore - a.totalScore;
});
const formattedActivities = await Promise.all(
sortedActivities.map(async (activity) => ({
interestXid: activity.activityType.interestXid,
activityId: activity.id,
connectionInterestedCount:
connectionInterestMap.get(activity.id) ?? 0,
activityTitle: activity.activityTitle,
sustainabilityScore: activity.sustainabilityScore,
cheapestPrice: activity.minPrice === Infinity ? null : activity.minPrice,
distance: 0,
rating: activity.avgRating,
energyLevel: activity.activityType.energyLevel
? {
...activity.activityType.energyLevel,
presignedUrl: await this.attachPresignedUrl(
activity.activityType.energyLevel.energyIcon,
),
}
: null,
media: await this.attachMediaWithPresignedUrl(activity.ActivitiesMedia),
})),
);
return {
page,
limit,
totalCount,
hasMore: skip + limit < totalCount,
activities: formattedActivities,
};
}
async getFilteredLandingPageAllDetails(
userId: number,
page: number,
limit: number,
countryName: string,
stateName: string,
cityName: string,
userLat: string,
userLong: string,
activityTypeXids?: 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 userLatitude = userAddressDetails?.locationLat ?? null;
const userLongitude = userAddressDetails?.locationLong ?? null;
let effectiveLocation: {
countryXid?: number | null;
stateXid?: number | null;
cityXid?: number | null;
} | null = null;
const hasRequestLocation = countryName && stateName && cityName;
if (hasRequestLocation) {
effectiveLocation = await this.findOrCreateLocation(
countryName!,
stateName!,
cityName!,
tx,
);
} else if (userAddressDetails) {
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: [],
activityTypes: [],
otherStatesActivities: null,
overSeasActivities: null,
};
}
// Get all activity types for user interests, filtered by selected activity types if provided
const activityTypeWhere: any = {
interestXid: { in: userInterests.map((ui) => ui.interestXid) },
isActive: true,
};
if (activityTypeXids && activityTypeXids.length > 0) {
activityTypeWhere.id = { in: activityTypeXids };
}
const activityTypesWithInterests = await tx.activityTypes.findMany({
where: activityTypeWhere,
select: {
id: true,
activityTypeName: true,
interestXid: true,
interests: {
select: {
id: true,
interestName: true,
interestColor: true,
interestImage: true,
displayOrder: true,
},
},
},
});
if (!activityTypesWithInterests.length) {
return {
userAddressDetails,
interests: [],
activityTypes: [],
otherStatesActivities: null,
overSeasActivities: null,
};
}
const userBucketInterested = await tx.userBucketInterested.findMany({
where: {
userXid: userId,
isActive: true,
},
select: {
activityXid: true,
isBucket: true,
},
});
const userBucketActivityIds = userBucketInterested
.filter(u => u.isBucket)
.map(u => u.activityXid);
const userInterestedActivityIds = userBucketInterested
.filter(u => !u.isBucket)
.map(u => u.activityXid);
const allUserExcludedActivityIds = userBucketInterested.map(
u => u.activityXid,
);
const userConnectionDetails = await tx.connectDetails.findMany({
where: { userXid: userId, isActive: true },
select: {
id: true,
schoolCompanyXid: true,
}
})
const otherConnectionUsers = await tx.connectDetails.findMany({
where: { userXid: { notIn: [userId] }, isActive: true, schoolCompanyXid: { in: userConnectionDetails.map((u) => u.schoolCompanyXid) } },
select: {
id: true,
userXid: true,
}
})
const connectionUserIds =
otherConnectionUsers.length > 0
? otherConnectionUsers.map(u => u.userXid)
: [-1];
const connectionInterestByActivity = await tx.userBucketInterested.groupBy({
by: ['activityXid'],
where: {
userXid: { in: connectionUserIds },
isActive: true,
},
_count: {
activityXid: true,
},
});
const connectionInterestMap = new Map(
connectionInterestByActivity.map(item => [
item.activityXid,
item._count.activityXid,
])
);
const skip = (page - 1) * limit;
// Group activity types by interest
const activityTypesByInterest = activityTypesWithInterests.reduce((acc, at) => {
if (!acc[at.interestXid]) {
acc[at.interestXid] = {
interest: at.interests,
activityTypes: [],
};
}
acc[at.interestXid].activityTypes.push({
activityTypeId: at.id,
activityTypeName: at.activityTypeName,
});
return acc;
}, {} as any);
// Fetch activities for each activity type
const activitiesByActivityType = await Promise.all(
activityTypesWithInterests.map(async (activityType) => {
const activities = await tx.activities.findMany({
where: {
isActive: true,
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
activityTypeXid: activityType.id,
id: {
notIn: allUserExcludedActivityIds.length
? allUserExcludedActivityIds
: [-1],
},
},
skip,
take: limit,
orderBy: { id: 'desc' },
select: {
id: true,
activityTitle: true,
activityDurationMins: true,
sustainabilityScore: true,
checkInLat: true,
checkInLong: true,
activityType: {
select: {
id: true,
activityTypeName: true,
interestXid: true,
energyLevel: {
select: {
id: true,
energyLevelName: true,
energyColor: true,
energyIcon: true,
},
},
},
},
ActivityVenues: {
select: {
ActivityPrices: {
select: {
sellPrice: true,
},
},
},
},
ActivitiesMedia: {
where: { isActive: true },
select: {
id: true,
mediaFileName: true,
mediaType: true,
},
},
},
});
const 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;
const distance = this.calculateDistance(
userLatitude,
userLongitude,
activity.checkInLat,
activity.checkInLong,
);
return {
activityId: activity.id,
connectionInterestedCount:
connectionInterestMap.get(activity.id) ?? 0,
activityTitle: activity.activityTitle,
activityDurationMins: activity.activityDurationMins,
sustainabilityScore: activity.sustainabilityScore,
cheapestPrice,
distance,
rating: 0,
energyLevel: activity.activityType.energyLevel
? {
...activity.activityType.energyLevel,
presignedUrl: await this.attachPresignedUrl(
activity.activityType.energyLevel.energyIcon,
),
}
: null,
media: await this.attachMediaWithPresignedUrl(activity.ActivitiesMedia),
};
}),
);
return {
activityTypeId: activityType.id,
activityTypeName: activityType.activityTypeName,
interestXid: activityType.interestXid,
activities: formattedActivities,
pagination: {
page,
limit,
hasMore: formattedActivities.length === limit,
},
};
}),
);
// Group by interests for the final structure
const interestsWithActivityTypes = await Promise.all(Object.values(activityTypesByInterest).map(
async (interestGroup: any) => ({
interestId: interestGroup.interest.id,
interestName: interestGroup.interest.interestName,
interestColor: interestGroup.interest.interestColor,
interestImage: interestGroup.interest.interestImage,
interestImagePresignedUrl: await this.attachPresignedUrl(interestGroup.interest.interestImage),
displayOrder: interestGroup.interest.displayOrder,
activityTypes: interestGroup.activityTypes.map((at: any) => {
const activityTypeData = activitiesByActivityType.find(
(adata) => adata.activityTypeId === at.activityTypeId
);
return {
...at,
activities: activityTypeData?.activities || [],
pagination: activityTypeData?.pagination || { page, limit, hasMore: false },
};
}),
}),
));
// Most Hyped Activities with filtering
const mostHypedGrouped = await tx.userBucketInterested.groupBy({
by: ['activityXid'],
where: {
isActive: true,
isBucket: false,
},
_count: {
activityXid: true,
},
orderBy: {
_count: {
activityXid: 'desc',
},
},
});
// Filter most hyped activities by activity type if provided
let filteredMostHypedActivityIds = mostHypedGrouped.map((a) => a.activityXid);
if (activityTypeXids && activityTypeXids.length > 0) {
const activitiesWithTypes = await tx.activities.findMany({
where: {
id: { in: filteredMostHypedActivityIds },
activityTypeXid: { in: activityTypeXids },
},
select: { id: true },
});
filteredMostHypedActivityIds = activitiesWithTypes.map(a => a.id);
}
const finalMostHypedGrouped = mostHypedGrouped
.filter(group => filteredMostHypedActivityIds.includes(group.activityXid))
.slice(skip, skip + limit);
const totalHypedActivities = filteredMostHypedActivityIds.length;
const mostHypedActivityIds = finalMostHypedGrouped.map((a) => a.activityXid);
const mostHypedActivitiesRaw = await tx.activities.findMany({
where: {
id: {
in: mostHypedActivityIds,
notIn: allUserExcludedActivityIds.length
? allUserExcludedActivityIds
: [-1],
},
isActive: true,
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
},
select: {
id: true,
activityTitle: true,
sustainabilityScore: true,
totalScore: true,
activityType: {
select: {
energyLevel: {
select: {
id: true,
energyLevelName: true,
energyColor: true,
energyIcon: true,
},
},
},
},
ActivitiesMedia: {
where: { isActive: true },
select: {
id: true,
mediaFileName: true,
mediaType: true,
},
},
ItineraryActivities: {
select: {
ActivityFeedbacks: {
select: { activityStars: true },
},
},
},
ActivityVenues: {
select: {
ActivityPrices: {
select: { sellPrice: true },
},
},
},
},
});
// Sort Most Hyped by the 4 criteria
const mostHypedSorted = mostHypedActivitiesRaw
.map((act) => {
const feedbacks = act.ItineraryActivities.flatMap(
(ia) => ia.ActivityFeedbacks,
);
const totalStars = feedbacks.reduce(
(sum, f) => sum + f.activityStars,
0,
);
const avgRating =
feedbacks.length > 0 ? totalStars / feedbacks.length : 0;
const prices = act.ActivityVenues.flatMap((v) =>
v.ActivityPrices.map((p) => p.sellPrice),
).filter((p) => p !== null) as number[];
const minPrice = prices.length > 0 ? Math.min(...prices) : Infinity;
return {
...act,
avgRating,
minPrice,
sustainabilityScore: act.sustainabilityScore ?? 0,
totalScore: act.totalScore ?? 0,
hypeCount:
finalMostHypedGrouped.find((g) => g.activityXid === act.id)?._count
.activityXid ?? 0,
};
})
.sort((a, b) => {
if (b.avgRating !== a.avgRating) return b.avgRating - a.avgRating;
if (a.minPrice !== b.minPrice) return a.minPrice - b.minPrice;
if (b.sustainabilityScore !== a.sustainabilityScore)
return b.sustainabilityScore - a.sustainabilityScore;
return b.totalScore - a.totalScore;
});
const mostHypedActivities = await Promise.all(
mostHypedSorted.map(async (activity) => ({
activityId: activity.id,
activityTitle: activity.activityTitle,
connectionInterestedCount:
connectionInterestMap.get(activity.id) ?? 0,
hypeCount: activity.hypeCount,
distance: 0,
rating: 0,
energyLevel: activity.activityType.energyLevel
? {
...activity.activityType.energyLevel,
presignedUrl: await this.attachPresignedUrl(
activity.activityType.energyLevel.energyIcon,
),
}
: null,
media: await this.attachMediaWithPresignedUrl(activity.ActivitiesMedia),
})),
);
const formattedMostHypedActivities = {
page,
limit,
totalCount: totalHypedActivities,
hasMore: skip + limit < totalHypedActivities,
activities: mostHypedActivities,
};
// New Arrivals with filtering
const newArrivalsWhere: any = {
isActive: true,
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
id: {
notIn: allUserExcludedActivityIds.length
? allUserExcludedActivityIds
: [-1],
},
createdAt: { gte: new Date(Date.now() - 31 * 24 * 60 * 60 * 1000) },
};
if (activityTypeXids && activityTypeXids.length > 0) {
newArrivalsWhere.activityTypeXid = { in: activityTypeXids };
}
const formattedNewArrivalsActivities = await this.rankAndPaginateActivities(
tx,
newArrivalsWhere,
page,
limit,
connectionInterestMap
);
// Other States Activities with filtering
const otherStatesWhere: any = {
isActive: true,
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
id: {
notIn: allUserExcludedActivityIds.length
? allUserExcludedActivityIds
: [-1],
},
};
if (effectiveCountryXid) {
otherStatesWhere.checkInCountryXid = effectiveCountryXid;
}
if (effectiveStateXid) {
otherStatesWhere.checkInStateXid = { not: effectiveStateXid };
}
if (activityTypeXids && activityTypeXids.length > 0) {
otherStatesWhere.activityTypeXid = { in: activityTypeXids };
}
const formattedOtherStatesActivities = await this.rankAndPaginateActivities(
tx,
otherStatesWhere,
page,
limit,
connectionInterestMap
);
// Random Activities with filtering
const totalActiveCount = await tx.activities.count({
where: {
isActive: true,
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
deletedAt: null,
id: {
notIn: allUserExcludedActivityIds.length
? allUserExcludedActivityIds
: [-1],
},
...(activityTypeXids && activityTypeXids.length > 0 && {
activityTypeXid: { in: activityTypeXids },
}),
},
});
let randomActivities: any[] = [];
if (totalActiveCount > 0) {
const takeCount = Math.min(5, totalActiveCount);
const randomOffsets = new Set<number>();
while (randomOffsets.size < takeCount) {
randomOffsets.add(Math.floor(Math.random() * totalActiveCount));
}
const randomFetched = await Promise.all(
Array.from(randomOffsets).map((offset) =>
tx.activities.findFirst({
skip: offset,
where: {
isActive: true,
activityInternalStatus:
ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
amInternalStatus:
ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
deletedAt: null,
id: {
notIn: allUserExcludedActivityIds.length
? allUserExcludedActivityIds
: [-1],
},
...(activityTypeXids && activityTypeXids.length > 0 && {
activityTypeXid: { in: activityTypeXids },
}),
},
select: {
id: true,
activityTitle: true,
ActivitiesMedia: {
where: { isActive: true, isCoverImage: true },
orderBy: { displayOrder: 'asc' },
take: 1,
select: {
mediaFileName: true,
},
},
},
}),
),
);
randomActivities = await Promise.all(
randomFetched
.filter(Boolean)
.map(async (activity) => {
const cover = activity!.ActivitiesMedia?.[0];
return {
activityId: activity!.id,
activityTitle: activity!.activityTitle,
coverImage: cover?.mediaFileName ?? null,
coverImagePresignedUrl: cover?.mediaFileName
? await this.attachPresignedUrl(cover.mediaFileName)
: null,
};
}),
);
}
// Overseas Activities with filtering
const overseasWhere: any = {
isActive: true,
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
id: {
notIn: allUserExcludedActivityIds.length
? allUserExcludedActivityIds
: [-1],
},
};
if (effectiveCountryXid) {
overseasWhere.checkInCountryXid = { not: effectiveCountryXid };
}
if (activityTypeXids && activityTypeXids.length > 0) {
overseasWhere.activityTypeXid = { in: activityTypeXids };
}
const formattedOverSeasActivities = await this.rankAndPaginateActivities(
tx,
overseasWhere,
page,
limit,
connectionInterestMap
);
return {
userAddressDetails,
experiencesLogged: 0,
citiesDiscovered: 0,
loggedInNetworkCount: 0,
citiesInNetworkCount: 0,
rating: 0,
interestedCount: userInterestedActivityIds.length,
bucketCount: userBucketActivityIds.length,
pagination: {
page,
limit,
},
randomActivities,
interests: interestsWithActivityTypes,
activityTypes: activitiesByActivityType,
otherStatesActivities: formattedOtherStatesActivities,
overSeasActivities: formattedOverSeasActivities,
newArrivalsActivities: formattedNewArrivalsActivities,
mostHypedActivities: formattedMostHypedActivities,
};
});
return data;
}
}

View File

@@ -32,6 +32,30 @@ import config from '@/config/config';
// return R * c;
// }
const calculateDistance = (
lat1: number | null,
lon1: number | null,
lat2: number | null,
lon2: number | null,
) => {
if (!lat1 || !lon1 || !lat2 || !lon2) return null;
const R = 6371; // km
const dLat = ((lat2 - lat1) * Math.PI) / 180;
const dLon = ((lon2 - lon1) * Math.PI) / 180;
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos((lat1 * Math.PI) / 180) *
Math.cos((lat2 * Math.PI) / 180) *
Math.sin(dLon / 2) *
Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return Number((R * c).toFixed(2));
};
const normalizeName = (name: string) =>
name.trim().toLowerCase().replace(/\s+/g, " ");
@@ -637,6 +661,9 @@ export class UserService {
},
});
const userLatitude = userAddressDetails?.locationLat ?? null;
const userLongitude = userAddressDetails?.locationLong ?? null;
let effectiveLocation: {
countryXid?: number | null;
stateXid?: number | null;
@@ -709,6 +736,9 @@ export class UserService {
};
}
const userInterestActivityTypeIds =
activitiyTypesOfUserInterests.map((a) => a.id);
const userBucketInterested = await tx.userBucketInterested.findMany({
where: {
userXid: userId,
@@ -783,7 +813,7 @@ export class UserService {
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
activityTypeXid: {
in: activitiyTypesOfUserInterests.map((at) => at.id),
in: userInterestActivityTypeIds
},
id: {
notIn: allUserExcludedActivityIds.length
@@ -839,6 +869,17 @@ export class UserService {
where: {
isActive: true,
isBucket: false,
activityXid: {
notIn: allUserExcludedActivityIds.length
? allUserExcludedActivityIds
: [-1],
},
Activities: {
activityTypeXid: { in: userInterestActivityTypeIds },
isActive: true,
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
}
},
});
@@ -850,6 +891,9 @@ export class UserService {
where: {
isActive: true,
isBucket: false,
activityXid: {
notIn: allUserExcludedActivityIds.length ? allUserExcludedActivityIds : [-1],
},
},
_count: {
activityXid: true,
@@ -876,6 +920,7 @@ export class UserService {
? allUserExcludedActivityIds
: [-1],
},
activityTypeXid: { in: userInterestActivityTypeIds },
isActive: true,
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
@@ -999,10 +1044,11 @@ export class UserService {
isActive: true,
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
activityTypeXid: { in: userInterestActivityTypeIds },
id: {
notIn: allUserExcludedActivityIds.length
? allUserExcludedActivityIds
: [-1], // prevent empty notIn issue
: [-1],
},
createdAt: { gte: new Date(Date.now() - 31 * 24 * 60 * 60 * 1000) },
};
@@ -1022,6 +1068,7 @@ export class UserService {
isActive: true,
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
activityTypeXid: { in: userInterestActivityTypeIds },
id: {
notIn: allUserExcludedActivityIds.length
? allUserExcludedActivityIds
@@ -1129,6 +1176,7 @@ export class UserService {
isActive: true,
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
activityTypeXid: { in: userInterestActivityTypeIds },
id: {
notIn: allUserExcludedActivityIds.length
? allUserExcludedActivityIds
@@ -1156,6 +1204,13 @@ export class UserService {
.filter(Boolean)
.sort((a, b) => a - b)[0] ?? null;
const distance = calculateDistance(
userLatitude,
userLongitude,
activity.checkInLat,
activity.checkInLong,
);
return {
interestXid: activity.activityType.interestXid,
activityId: activity.id,
@@ -1165,7 +1220,7 @@ export class UserService {
activityDurationMins: activity.activityDurationMins,
sustainabilityScore: activity.sustainabilityScore,
cheapestPrice,
distance: 0,
distance,
rating: 0,
energyLevel: activity.activityType.energyLevel
? {
@@ -1246,9 +1301,14 @@ export class UserService {
stateXid: true,
cityXid: true,
countryXid: true,
locationLat: true,
locationLong: true,
},
});
const userLat = userAddressDetails?.locationLat ?? null;
const userLng = userAddressDetails?.locationLong ?? null;
let effectiveLocation: {
countryXid?: number | null;
stateXid?: number | null;
@@ -1398,6 +1458,8 @@ export class UserService {
select: {
id: true,
activityTitle: true,
checkInLat: true,
checkInLong: true,
activityType: {
select: {
interestXid: true,
@@ -1418,7 +1480,12 @@ export class UserService {
connectionInterestedCount:
connectionInterestMap.get(a.id) ?? 0,
activityTitle: a.activityTitle,
distance: 0,
distance: calculateDistance(
userLat,
userLng,
a.checkInLat,
a.checkInLong
),
rating: 0,
energyLevel: {
...a.activityType.energyLevel,
@@ -1489,6 +1556,8 @@ export class UserService {
select: {
id: true,
activityTitle: true,
checkInLat: true,
checkInLong: true,
activityType: { select: { energyLevel: true } },
ActivitiesMedia: {
where: { isActive: true },
@@ -1507,7 +1576,12 @@ export class UserService {
hypeCount: g._count.activityXid,
connectionInterestedCount:
connectionInterestMap.get(act.id) ?? 0,
distance: 0,
distance: calculateDistance(
userLat,
userLng,
act.checkInLat,
act.checkInLong
),
rating: 0,
energyLevel: {
...act.activityType.energyLevel,
@@ -1637,6 +1711,7 @@ export class UserService {
hasMore: skip + limit < newArrivalsCount,
activities: await Promise.all(
newArrivalsRaw.map(async (a) => ({
activityId: a.id,
activityTitle: a.activityTitle,
connectionInterestedCount:
connectionInterestMap.get(a.id) ?? 0,
@@ -1660,6 +1735,7 @@ export class UserService {
hasMore: skip + limit < otherStatesCount,
activities: await Promise.all(
otherStatesRaw.map(async (a) => ({
activityId: a.id,
activityTitle: a.activityTitle,
connectionInterestedCount:
connectionInterestMap.get(a.id) ?? 0,
@@ -1683,6 +1759,7 @@ export class UserService {
hasMore: skip + limit < overseasCount,
activities: await Promise.all(
overseasRaw.map(async (a) => ({
activityId: a.id,
activityTitle: a.activityTitle,
connectionInterestedCount:
connectionInterestMap.get(a.id) ?? 0,
@@ -1740,6 +1817,30 @@ export class UserService {
equipmentIsChargeable: true,
cancellationAvailable: true,
cancellationAllowedBeforeMins: true,
checkInCity: {
select: {
id: true,
cityName: true
}
},
checkOutCity: {
select: {
id: true,
cityName: true
}
},
checkInState: {
select: {
id: true,
stateName: true
}
},
checkOutState: {
select: {
id: true,
stateName: true
}
},
activityType: {
select: {
@@ -1761,6 +1862,8 @@ export class UserService {
select: {
id: true,
exclusiveNotes: true,
SafetyInstruction: true,
Cancellations: true,
dosNotes: true,
dontsNotes: true,
tipsNotes: true,
@@ -1915,6 +2018,9 @@ export class UserService {
ActivityVenues: {
where: {
isActive: true,
ScheduleHeader: {
some: {}
}
},
select: {
id: true,
@@ -1933,6 +2039,18 @@ export class UserService {
mediaType: true,
},
},
ScheduleHeader: {
select: {
id: true,
scheduleType: true,
startDate: true,
endDate: true,
earlyCheckInMins: true,
bookingCutOffMins: true,
effectiveFromDt: true,
effectiveToDt: true,
}
},
ActivityPrices: {
select: {
id: true,
@@ -1953,6 +2071,53 @@ export class UserService {
},
});
if (!activity) {
throw new Error("Activity not found");
}
const userActivityStatus = await tx.userBucketInterested.findFirst({
where: {
activityXid: activityXid,
userXid: userId,
isActive: true,
},
select: {
isBucket: true,
},
});
const isBucket = userActivityStatus?.isBucket === true;
const isInterested =
userActivityStatus ? userActivityStatus.isBucket === false : false;
const userLocation = await tx.userAddressDetails.findFirst({
where: { userXid: userId },
select: {
locationLat: true,
locationLong: true,
},
});
const userLat = userLocation?.locationLat ?? null;
const userLng = userLocation?.locationLong ?? null;
let distance = 0;
if (
userLat &&
userLng &&
activity?.checkInLat &&
activity?.checkInLong
) {
distance = calculateDistance(
userLat,
userLng,
activity.checkInLat,
activity.checkInLong
)
};
// ================= PRESIGNED URL SECTION =================
// 1⃣ Activity Media
@@ -2063,9 +2228,10 @@ export class UserService {
})
: 0;
const prices = activity.ActivityVenues.flatMap((v) =>
v.ActivityPrices.map((p) => p.sellPrice),
).filter((p) => p !== null) as number[];
const prices =
activity?.ActivityVenues?.flatMap((v) =>
v.ActivityPrices.map((p) => p.sellPrice)
).filter((p) => p !== null) ?? [];
const cheapestPrice = prices.length > 0 ? Math.min(...prices) : null;
@@ -2099,10 +2265,11 @@ export class UserService {
},
});
const shuffledUsers = interestedUsers.sort(() => 0.5 - Math.random());
const randomFive = shuffledUsers.slice(0, 5);
const randomFive = interestedUsers
.sort(() => Math.random() - 0.5)
.slice(0, 5);
const interestedUserImages: string[] = [];
const interestedUserImages: { profileImage: string }[] = [];
for (const item of randomFive) {
const profileImage = item.user.profileImage;
@@ -2113,10 +2280,23 @@ export class UserService {
: profileImage;
const presignedUrl = await getPresignedUrl(bucket, key);
interestedUserImages.push(presignedUrl);
interestedUserImages.push({
profileImage: presignedUrl,
});
}
}
const checkInLocation =
activity?.checkInCity?.cityName && activity?.checkInState?.stateName
? `${activity.checkInCity.cityName}, ${activity.checkInState.stateName}`
: null;
const checkOutLocation =
activity?.checkOutCity?.cityName && activity?.checkOutState?.stateName
? `${activity.checkOutCity.cityName}, ${activity.checkOutState.stateName}`
: null;
return {
activity,
interestedCount,
@@ -2124,8 +2304,12 @@ export class UserService {
cheapestPrice,
totalCapacity,
rating: 0, // ⭐ Placeholder, implement rating logic as needed
distance: 0,
interestedUserImages
distance: distance || 0,
interestedUserImages,
isBucket,
isInterested,
checkInLocation,
checkOutLocation
};
});
}
@@ -2521,6 +2705,19 @@ export class UserService {
select: {
id: true,
schoolCompanyName: true,
cities: {
select: {
id: true,
cityName: true,
states: {
select: {
id: true,
stateName: true
}
}
}
},
isSchool: true,
isActive: true,
createdAt: true,
@@ -2646,37 +2843,9 @@ export class UserService {
) {
const data = await this.prisma.$transaction(async (tx) => {
const networkUsers = await tx.connectDetails.findMany({
const userInterests = await tx.userInterests.findMany({
where: {
isActive: true,
schoolCompanyXid: {
in: schoolCompanyXids,
},
userXid: {
not: userId,
},
},
select: {
userXid: true,
},
});
if (!networkUsers.length) {
return {
interests: [],
mostHypedActivities: null,
newArrivalsActivities: null,
otherStatesActivities: null,
overSeasActivities: null,
};
}
const networkUserIds = [...new Set(networkUsers.map(u => u.userXid))];
const networkUserInterests = await tx.userInterests.findMany({
where: {
userXid: { in: networkUserIds },
userXid: userId,
isActive: true,
},
distinct: ['interestXid'],
@@ -2694,9 +2863,74 @@ export class UserService {
}
});
const distinctInterests = networkUserInterests.map(i => i.interestXid);
if (!userInterests.length) {
return {
interests: [],
mostHypedActivities: null,
newArrivalsActivities: null,
otherStatesActivities: null,
overSeasActivities: null,
};
}
if (!distinctInterests.length) {
const connectionUsers = await tx.connectDetails.findMany({
where: {
isActive: true,
schoolCompanyXid: { in: schoolCompanyXids },
userXid: { not: userId },
},
select: {
userXid: true,
},
});
const connectionUserIds = [
...new Set(connectionUsers.map((u) => u.userXid)),
];
const connectionInterestByActivity = await tx.userBucketInterested.groupBy({
by: ["activityXid"],
where: {
userXid: { in: connectionUserIds },
isActive: true,
},
_count: {
activityXid: true,
},
});
const connectionInterestMap = new Map(
connectionInterestByActivity.map((item) => [
item.activityXid,
item._count.activityXid,
])
);
if (!connectionUserIds.length) {
return {
interests: [],
mostHypedActivities: null,
newArrivalsActivities: null,
otherStatesActivities: null,
overSeasActivities: null,
};
}
const connectionActivities = await tx.userBucketInterested.findMany({
where: {
userXid: { in: connectionUserIds },
isActive: true,
},
select: {
activityXid: true,
},
});
const connectionActivityIds = [
...new Set(connectionActivities.map((a) => a.activityXid)),
];
if (!connectionActivityIds.length) {
return {
interests: [],
mostHypedActivities: null,
@@ -2709,7 +2943,7 @@ export class UserService {
const activityTypes = await tx.activityTypes.findMany({
where: {
interestXid: {
in: distinctInterests,
in: userInterests.map(i => i.interestXid),
},
isActive: true,
},
@@ -2726,15 +2960,22 @@ export class UserService {
};
}
const activityTypeIds = activityTypes.map((a) => a.id);
const userAddressDetails = await tx.userAddressDetails.findFirst({
where: { userXid: userId },
select: {
stateXid: true,
cityXid: true,
countryXid: true,
locationLat: true,
locationLong: true,
},
});
const userLatitude = userAddressDetails?.locationLat ?? null;
const userLongitude = userAddressDetails?.locationLong ?? null;
let effectiveLocation: {
countryXid?: number | null;
stateXid?: number | null;
@@ -2760,41 +3001,24 @@ export class UserService {
const effectiveCountryXid = effectiveLocation?.countryXid ?? null;
const effectiveStateXid = effectiveLocation?.stateXid ?? null;
const userConnectionDetails = await tx.connectDetails.findMany({
where: { userXid: userId, isActive: true },
select: {
id: true,
schoolCompanyXid: true,
}
})
const otherConnectionUsers = await tx.connectDetails.findMany({
where: { userXid: { notIn: [userId] }, isActive: true, schoolCompanyXid: { in: userConnectionDetails.map((u) => u.schoolCompanyXid) } },
select: {
id: true,
userXid: true,
}
})
const connectionUserIds = otherConnectionUsers.map(u => u.userXid);
const connectionInterestByActivity = await tx.userBucketInterested.groupBy({
by: ['activityXid'],
const userBucketInterested = await tx.userBucketInterested.findMany({
where: {
userXid: { in: connectionUserIds },
userXid: userId,
isActive: true,
},
_count: {
select: {
activityXid: true,
isBucket: true,
},
});
const connectionInterestMap = new Map(
connectionInterestByActivity.map(item => [
item.activityXid,
item._count.activityXid,
])
);
const userBucketActivityIds = userBucketInterested
.filter(u => u.isBucket)
.map(u => u.activityXid);
const userInterestedActivityIds = userBucketInterested
.filter(u => !u.isBucket)
.map(u => u.activityXid);
/* =====================================================
1⃣ FETCH ALL CANDIDATES FOR INTERESTS (SIMPLE SORT)
@@ -2802,12 +3026,11 @@ export class UserService {
// Reverted to simple ID based sorting for Interest-based activities
const activities = await tx.activities.findMany({
where: {
id: { in: connectionActivityIds }, // 🔥 NEW FILTER
isActive: true,
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
activityTypeXid: {
in: activityTypes.map(at => at.id),
},
activityTypeXid: { in: activityTypeIds },
},
skip,
take: limit,
@@ -2852,14 +3075,13 @@ export class UserService {
},
});
const activityTypeIds = activityTypes.map(a => a.id);
const mostHypedTotalCount = await tx.userBucketInterested.groupBy({
by: ['activityXid'],
where: {
isActive: true,
isBucket: false,
Activities: {
id: { in: connectionActivityIds },
activityTypeXid: { in: activityTypeIds },
isActive: true,
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
@@ -2879,6 +3101,7 @@ export class UserService {
isActive: true,
isBucket: false,
Activities: {
id: { in: connectionActivityIds },
activityTypeXid: { in: activityTypeIds },
isActive: true,
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
@@ -3013,6 +3236,7 @@ export class UserService {
3⃣ NEW ARRIVALS (RANKED)
===================================================== */
const newArrivalsWhere = {
id: { in: connectionActivityIds },
isActive: true,
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
@@ -3026,6 +3250,7 @@ export class UserService {
4⃣ OTHER STATES ACTIVITIES (RANKED)
===================================================== */
const otherStatesWhere: any = {
id: { in: connectionActivityIds },
isActive: true,
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
@@ -3046,6 +3271,7 @@ export class UserService {
5⃣ OVERSEAS ACTIVITIES (RANKED)
===================================================== */
const overseasWhere: any = {
id: { in: connectionActivityIds },
isActive: true,
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
@@ -3066,6 +3292,12 @@ export class UserService {
.map(p => p.sellPrice)
.filter(Boolean)
.sort((a, b) => a - b)[0] ?? null;
const distance = calculateDistance(
userLatitude,
userLongitude,
activity.checkInLat,
activity.checkInLong,
);
return {
interestXid: activity.activityType.interestXid,
@@ -3073,7 +3305,7 @@ export class UserService {
activityTitle: activity.activityTitle,
connectionInterestedCount:
connectionInterestMap.get(activity.id) ?? 0,
distance: 0,
distance,
rating: 0,
activityDurationMins: activity.activityDurationMins,
sustainabilityScore: activity.sustainabilityScore,
@@ -3092,7 +3324,7 @@ export class UserService {
);
const interestsWithActivities = await Promise.all(
networkUserInterests
userInterests
.sort((a, b) =>
a.interest.interestName.localeCompare(b.interest.interestName)
)
@@ -3122,6 +3354,8 @@ export class UserService {
citiesDiscovered: 10,
loggedInNetworkCount: 0,
citiesInNetworkCount: 0,
interestedCount: userInterestedActivityIds.length,
bucketCount: userBucketActivityIds.length,
pagination: {
page,
limit,
@@ -3695,4 +3929,56 @@ export class UserService {
};
}
async removeFromBucketInterested(
userXid: number,
isBucket: boolean,
bucketTypeName: string,
activityXid: number
) {
const activityExists = await this.prisma.activities.findFirst({
where: { id: activityXid, isActive: true },
});
if (!activityExists) {
throw new ApiError(404, 'Activity not found');
}
const existing = await this.prisma.userBucketInterested.findFirst({
where: { userXid, activityXid, isActive: true },
});
if (!existing) {
throw new ApiError(400, 'Activity not found in bucket/interested list');
}
await this.prisma.userBucketInterested.update({
where: { id: existing.id },
data: {
isActive: false,
},
});
// Get updated counts
const [bucketCount, interestedCount] = await Promise.all([
this.prisma.userBucketInterested.count({
where: {
userXid,
isBucket: true,
isActive: true,
},
}),
this.prisma.userBucketInterested.count({
where: {
userXid,
isBucket: false,
isActive: true,
},
}),
]);
return {
bucketCount,
interestedCount,
};
}
}