Merge branch 'paritosh-main1' of http://git.wdipl.com/Mayank.Mishra/MinglarBackendNestJS into mayankSprint2
This commit is contained in:
@@ -316,4 +316,34 @@ viewMoreActivitiesUpperSection:
|
||||
events:
|
||||
- httpApi:
|
||||
path: /user/activities/view-more-activities-upper-section
|
||||
method: get
|
||||
|
||||
getRandomActiveActivity:
|
||||
handler: src/modules/user/handlers/activities/getRandomActiveActivity.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/get-random-active-activity
|
||||
method: get
|
||||
|
||||
getNearbyActivities:
|
||||
handler: src/modules/user/handlers/activities/getNearbyActivities.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/get-nearby-activities
|
||||
method: get
|
||||
71
src/modules/user/handlers/activities/getNearbyActivities.ts
Normal file
71
src/modules/user/handlers/activities/getNearbyActivities.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
|
||||
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
|
||||
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
|
||||
import ApiError from '../../../../common/utils/helper/ApiError';
|
||||
import { UserService } from '../../services/user.service';
|
||||
import { verifyUserToken } from '../../../../common/middlewares/jwt/authForUser';
|
||||
|
||||
const userService = new UserService(prismaClient);
|
||||
|
||||
export const handler = safeHandler(async (
|
||||
event: APIGatewayProxyEvent,
|
||||
context?: Context
|
||||
): Promise<APIGatewayProxyResult> => {
|
||||
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.');
|
||||
}
|
||||
|
||||
const userInfo = await verifyUserToken(token);
|
||||
const userId = Number(userInfo.id);
|
||||
|
||||
if (!userId || isNaN(userId)) {
|
||||
throw new ApiError(400, 'Invalid user ID');
|
||||
}
|
||||
|
||||
const latParam = event.queryStringParameters?.lat ?? event.queryStringParameters?.latitude;
|
||||
const longParam = event.queryStringParameters?.long ?? event.queryStringParameters?.lng ?? event.queryStringParameters?.longitude;
|
||||
const radiusParam = event.queryStringParameters?.radiusKm ?? event.queryStringParameters?.radius;
|
||||
|
||||
if (!latParam || !longParam || !radiusParam) {
|
||||
throw new ApiError(400, 'lat, long and radiusKm (in km) are required as query parameters');
|
||||
}
|
||||
|
||||
const userLat = Number(latParam);
|
||||
const userLong = Number(longParam);
|
||||
const radiusKm = Number(radiusParam);
|
||||
|
||||
if (Number.isNaN(userLat) || Number.isNaN(userLong) || Number.isNaN(radiusKm)) {
|
||||
throw new ApiError(400, 'lat, long and radiusKm must be valid numbers');
|
||||
}
|
||||
|
||||
const page = Number(event.queryStringParameters?.page ?? 1);
|
||||
const limit = Number(event.queryStringParameters?.limit ?? 20);
|
||||
|
||||
if (page < 1 || limit < 1) {
|
||||
throw new ApiError(400, 'Invalid pagination values');
|
||||
}
|
||||
|
||||
const result = await userService.getNearbyActivities(
|
||||
userId,
|
||||
userLat,
|
||||
userLong,
|
||||
radiusKm,
|
||||
page,
|
||||
limit,
|
||||
);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Nearby activities retrieved successfully',
|
||||
data: result,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
|
||||
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
|
||||
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
|
||||
import ApiError from '../../../../common/utils/helper/ApiError';
|
||||
import { UserService } from '../../services/user.service';
|
||||
import { verifyUserToken } from '../../../../common/middlewares/jwt/authForUser';
|
||||
|
||||
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.');
|
||||
}
|
||||
|
||||
// Verify token and get user info
|
||||
const userInfo = await verifyUserToken(token);
|
||||
const userId = Number(userInfo.id);
|
||||
|
||||
if (!userId || Number.isNaN(userId)) {
|
||||
throw new ApiError(400, 'Invalid user ID');
|
||||
}
|
||||
|
||||
// Fetch 50 random active activities
|
||||
const result = await userService.getRandomActiveActivity();
|
||||
|
||||
if (!result || result.length === 0) {
|
||||
return {
|
||||
statusCode: 404,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
success: false,
|
||||
message: 'No active activities found',
|
||||
data: [],
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Random active activities retrieved successfully',
|
||||
data: result,
|
||||
count: result.length,
|
||||
}),
|
||||
};
|
||||
});
|
||||
@@ -30,7 +30,7 @@ export const handler = safeHandler(
|
||||
const userInfo = await verifyUserToken(token);
|
||||
const userId = Number(userInfo.id);
|
||||
|
||||
if (!userId || isNaN(userId)) {
|
||||
if (!userId || Number.isNaN(userId)) {
|
||||
throw new ApiError(400, 'Invalid user ID');
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ export const handler = safeHandler(
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Cities found successfully',
|
||||
data: results,
|
||||
data: results, // already capped at 50 in DB query
|
||||
count: results.length,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -157,6 +157,32 @@ const attachMediaWithPresignedUrl = async (mediaArr = []) => {
|
||||
).filter(Boolean);
|
||||
};
|
||||
|
||||
function deg2rad(deg: number): number {
|
||||
return deg * (Math.PI / 180);
|
||||
}
|
||||
|
||||
function getDistanceFromLatLon(
|
||||
userLat1: number,
|
||||
userLon1: number,
|
||||
activityLat2: number,
|
||||
activityLon2: number,
|
||||
): number {
|
||||
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 bucket = config.aws.bucketName;
|
||||
|
||||
/* =====================================================
|
||||
@@ -1849,6 +1875,147 @@ export class UserService {
|
||||
return activitiesWithCounts;
|
||||
}
|
||||
|
||||
async getNearbyActivities(
|
||||
userId: number,
|
||||
userLat: number,
|
||||
userLong: number,
|
||||
radiusKm: number,
|
||||
page: number,
|
||||
limit: number,
|
||||
) {
|
||||
if (userLat === undefined || userLong === undefined || radiusKm === undefined) {
|
||||
throw new ApiError(
|
||||
400,
|
||||
'Latitude, longitude and radius are required to find nearby activities',
|
||||
);
|
||||
}
|
||||
|
||||
if (radiusKm <= 0) {
|
||||
throw new ApiError(400, 'Radius must be greater than 0');
|
||||
}
|
||||
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
// Rough bounding box in degrees to reduce DB scan
|
||||
const earthRadiusKm = 6371;
|
||||
const latDelta = (radiusKm / earthRadiusKm) * (180 / Math.PI);
|
||||
const lonDelta =
|
||||
(radiusKm / (earthRadiusKm * Math.cos(deg2rad(userLat)))) *
|
||||
(180 / Math.PI);
|
||||
|
||||
const candidates = await this.prisma.activities.findMany({
|
||||
where: {
|
||||
isActive: true,
|
||||
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
checkInLat: {
|
||||
not: null,
|
||||
gte: userLat - latDelta,
|
||||
lte: userLat + latDelta,
|
||||
},
|
||||
checkInLong: {
|
||||
not: null,
|
||||
gte: userLong - lonDelta,
|
||||
lte: userLong + lonDelta,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
activityTitle: true,
|
||||
activityDurationMins: true,
|
||||
sustainabilityScore: true,
|
||||
checkInLat: true,
|
||||
checkInLong: true,
|
||||
activityType: {
|
||||
select: {
|
||||
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 withDistance = candidates
|
||||
.map((activity: any) => {
|
||||
const distanceKm = getDistanceFromLatLon(
|
||||
userLat,
|
||||
userLong,
|
||||
activity.checkInLat,
|
||||
activity.checkInLong,
|
||||
);
|
||||
|
||||
return {
|
||||
...activity,
|
||||
distanceKm,
|
||||
};
|
||||
})
|
||||
.filter((a) => a.distanceKm <= radiusKm)
|
||||
.sort((a, b) => a.distanceKm - b.distanceKm);
|
||||
|
||||
const totalCount = withDistance.length;
|
||||
const paged = withDistance.slice(skip, skip + limit);
|
||||
|
||||
const formattedActivities = await Promise.all(
|
||||
paged.map(async (activity: any) => {
|
||||
const prices = activity.ActivityVenues.flatMap((v: any) =>
|
||||
v.ActivityPrices.map((p: any) => p.sellPrice),
|
||||
).filter((p: any) => p !== null) as number[];
|
||||
|
||||
const cheapestPrice = prices.length > 0 ? Math.min(...prices) : null;
|
||||
|
||||
return {
|
||||
activityId: activity.id,
|
||||
activityTitle: activity.activityTitle,
|
||||
activityDurationMins: activity.activityDurationMins,
|
||||
sustainabilityScore: activity.sustainabilityScore,
|
||||
distanceKm: activity.distanceKm,
|
||||
cheapestPrice,
|
||||
energyLevel: activity.activityType.energyLevel
|
||||
? {
|
||||
...activity.activityType.energyLevel,
|
||||
presignedUrl: await attachPresignedUrl(
|
||||
activity.activityType.energyLevel.energyIcon,
|
||||
),
|
||||
}
|
||||
: null,
|
||||
media: await attachMediaWithPresignedUrl(activity.ActivitiesMedia),
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
page,
|
||||
limit,
|
||||
totalCount,
|
||||
hasMore: skip + limit < totalCount,
|
||||
activities: formattedActivities,
|
||||
};
|
||||
}
|
||||
|
||||
// CONNECTIONS
|
||||
|
||||
async getAllConnectionDetailsOfUser(userXid: number) {
|
||||
@@ -1932,9 +2099,11 @@ export class UserService {
|
||||
id: true,
|
||||
cityName: true,
|
||||
stateXid: true,
|
||||
isActive: true,
|
||||
createdAt: true,
|
||||
},
|
||||
orderBy: {
|
||||
cityName: 'asc',
|
||||
},
|
||||
take: 50, // reduce latency by limiting results at DB level
|
||||
});
|
||||
|
||||
return results;
|
||||
@@ -2768,4 +2937,94 @@ export class UserService {
|
||||
return true;
|
||||
}
|
||||
|
||||
async getRandomActiveActivity() {
|
||||
return await this.prisma.$transaction(async (tx) => {
|
||||
// Get count of active activities
|
||||
const count = await tx.activities.count({
|
||||
where: {
|
||||
isActive: true,
|
||||
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
deletedAt: null,
|
||||
},
|
||||
});
|
||||
|
||||
if (count === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Determine how many activities to fetch (50 or less if count is smaller)
|
||||
const takeCount = Math.min(50, count);
|
||||
|
||||
// Fetch random activities - using ORDER BY RANDOM() equivalent approach
|
||||
// Get all IDs first, shuffle, then take 50
|
||||
const allActivityIds = await tx.activities.findMany({
|
||||
where: {
|
||||
isActive: true,
|
||||
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
deletedAt: null,
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
// Shuffle array and take first 50
|
||||
const shuffled = allActivityIds.sort(() => Math.random() - 0.5);
|
||||
const selectedIds = shuffled.slice(0, takeCount).map(a => a.id);
|
||||
|
||||
// Fetch activities with only activityTitle and ActivitiesMedia
|
||||
const activities = await tx.activities.findMany({
|
||||
where: {
|
||||
id: { in: selectedIds },
|
||||
isActive: true,
|
||||
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
deletedAt: null,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
activityTitle: true,
|
||||
ActivitiesMedia: {
|
||||
where: {
|
||||
isActive: true,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
mediaFileName: true,
|
||||
mediaType: true,
|
||||
},
|
||||
orderBy: {
|
||||
displayOrder: 'asc', // Get the first image by display order
|
||||
},
|
||||
take: 1, // Get only the first image
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Process activities to attach presigned URLs and format response
|
||||
const result = await Promise.all(
|
||||
activities.map(async (activity) => {
|
||||
let activityImage = null;
|
||||
let activityImagePresignedUrl = null;
|
||||
|
||||
// Get the first image and attach presigned URL
|
||||
if (Array.isArray(activity.ActivitiesMedia) && activity.ActivitiesMedia.length > 0) {
|
||||
const firstImage = activity.ActivitiesMedia[0];
|
||||
activityImage = firstImage.mediaFileName;
|
||||
activityImagePresignedUrl = await attachPresignedUrl(firstImage.mediaFileName);
|
||||
}
|
||||
|
||||
return {
|
||||
id: activity.id,
|
||||
activityName: activity.activityTitle,
|
||||
activityImage: activityImage,
|
||||
activityImagePresignedUrl: activityImagePresignedUrl,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user