filtered landing page for specific search api
This commit is contained in:
@@ -376,4 +376,19 @@ removeActivityFromBucketInterested:
|
||||
events:
|
||||
- httpApi:
|
||||
path: /user/activities/remove-from-bucket-interested
|
||||
method: post
|
||||
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
|
||||
@@ -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,
|
||||
}),
|
||||
};
|
||||
});
|
||||
949
src/modules/user/services/filteredLandingPage.service.ts
Normal file
949
src/modules/user/services/filteredLandingPage.service.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user