Add getNearbyActivities API to retrieve activities based on user location and radius. Implemented token validation and pagination support. Enhanced UserService with distance calculation and filtering logic for nearby activities.

This commit is contained in:
paritosh18
2026-02-23 20:00:50 +05:30
parent 5ec07d4480
commit ced8bdcbad
3 changed files with 253 additions and 0 deletions

View File

@@ -316,4 +316,19 @@ getRandomActiveActivity:
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

View 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,
}),
};
});

View File

@@ -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) {