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:
@@ -316,4 +316,19 @@ getRandomActiveActivity:
|
|||||||
events:
|
events:
|
||||||
- httpApi:
|
- httpApi:
|
||||||
path: /user/activities/get-random-active-activity
|
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
|
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,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
@@ -157,6 +157,32 @@ const attachMediaWithPresignedUrl = async (mediaArr = []) => {
|
|||||||
).filter(Boolean);
|
).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;
|
const bucket = config.aws.bucketName;
|
||||||
|
|
||||||
/* =====================================================
|
/* =====================================================
|
||||||
@@ -1849,6 +1875,147 @@ export class UserService {
|
|||||||
return activitiesWithCounts;
|
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
|
// CONNECTIONS
|
||||||
|
|
||||||
async getAllConnectionDetailsOfUser(userXid: number) {
|
async getAllConnectionDetailsOfUser(userXid: number) {
|
||||||
|
|||||||
Reference in New Issue
Block a user