feat: add getSurpriseMePageDetails API endpoint and implement user-specific activity retrieval logic
This commit is contained in:
@@ -107,6 +107,21 @@ getLandingPageDetails:
|
|||||||
path: /user/activities/get-landing-page-details
|
path: /user/activities/get-landing-page-details
|
||||||
method: get
|
method: get
|
||||||
|
|
||||||
|
getSurpriseMePageDetails:
|
||||||
|
handler: src/modules/user/handlers/activities/surpriseMePage.handler
|
||||||
|
memorySize: 384
|
||||||
|
package:
|
||||||
|
patterns:
|
||||||
|
- 'src/modules/user/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-surprise-me-page-details
|
||||||
|
method: get
|
||||||
|
|
||||||
getActivityDetailsById:
|
getActivityDetailsById:
|
||||||
handler: src/modules/user/handlers/activities/getByIdActivityDetails.handler
|
handler: src/modules/user/handlers/activities/getByIdActivityDetails.handler
|
||||||
memorySize: 384
|
memorySize: 384
|
||||||
|
|||||||
61
src/modules/user/handlers/activities/surpriseMePage.ts
Normal file
61
src/modules/user/handlers/activities/surpriseMePage.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
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 || 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 ?? '';
|
||||||
|
|
||||||
|
if (page < 1 || limit < 1) {
|
||||||
|
throw new ApiError(400, 'Invalid pagination values');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch user with their HostHeader stepper info
|
||||||
|
const result = await userService.getSurpriseMeDetails(
|
||||||
|
userId,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
countryName,
|
||||||
|
stateName,
|
||||||
|
cityName,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
message: 'Data retrieved successfully',
|
||||||
|
data: result,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
@@ -851,6 +851,335 @@ export class UserService {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getSurpriseMeDetails(
|
||||||
|
userId: number,
|
||||||
|
page: number,
|
||||||
|
limit: number,
|
||||||
|
countryName: string,
|
||||||
|
stateName: string,
|
||||||
|
cityName: string
|
||||||
|
) {
|
||||||
|
const data = await this.prisma.$transaction(async (tx) => {
|
||||||
|
|
||||||
|
/* =====================================================
|
||||||
|
1️⃣ USER LOCATION
|
||||||
|
===================================================== */
|
||||||
|
const userAddressDetails = await tx.userAddressDetails.findFirst({
|
||||||
|
where: { userXid: userId },
|
||||||
|
select: {
|
||||||
|
stateXid: true,
|
||||||
|
cityXid: true,
|
||||||
|
countryXid: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let effectiveLocation: {
|
||||||
|
countryXid?: number | null;
|
||||||
|
stateXid?: number | null;
|
||||||
|
cityXid?: number | null;
|
||||||
|
} | null = null;
|
||||||
|
|
||||||
|
if (countryName && stateName && cityName) {
|
||||||
|
effectiveLocation = await findOrCreateLocation(tx, {
|
||||||
|
countryName,
|
||||||
|
stateName,
|
||||||
|
cityName,
|
||||||
|
});
|
||||||
|
} else if (userAddressDetails) {
|
||||||
|
effectiveLocation = {
|
||||||
|
countryXid: userAddressDetails.countryXid,
|
||||||
|
stateXid: userAddressDetails.stateXid,
|
||||||
|
cityXid: userAddressDetails.cityXid,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const effectiveCountryXid = effectiveLocation?.countryXid ?? null;
|
||||||
|
const effectiveStateXid = effectiveLocation?.stateXid ?? null;
|
||||||
|
|
||||||
|
/* =====================================================
|
||||||
|
2️⃣ USER INTERESTS (TO EXCLUDE)
|
||||||
|
===================================================== */
|
||||||
|
const userInterests = await tx.userInterests.findMany({
|
||||||
|
where: { userXid: userId, isActive: true },
|
||||||
|
select: { interestXid: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const userInterestTypeIds = await tx.activityTypes.findMany({
|
||||||
|
where: {
|
||||||
|
interestXid: { in: userInterests.map(i => i.interestXid) },
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const excludedActivityTypeIds = userInterestTypeIds.map(a => a.id);
|
||||||
|
|
||||||
|
const excludeUserInterestCondition =
|
||||||
|
excludedActivityTypeIds.length > 0
|
||||||
|
? { activityTypeXid: { notIn: excludedActivityTypeIds } }
|
||||||
|
: {};
|
||||||
|
|
||||||
|
const skip = (page - 1) * limit;
|
||||||
|
|
||||||
|
/* =====================================================
|
||||||
|
3️⃣ OTHER INTERESTS (GROUPED WITH ACTIVITIES)
|
||||||
|
===================================================== */
|
||||||
|
const otherInterests = await tx.interests.findMany({
|
||||||
|
where: {
|
||||||
|
isActive: true,
|
||||||
|
id: { notIn: userInterests.map(i => i.interestXid) },
|
||||||
|
},
|
||||||
|
orderBy: { interestName: 'asc' },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
interestName: true,
|
||||||
|
interestColor: true,
|
||||||
|
interestImage: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const otherInterestActivities = await tx.activities.findMany({
|
||||||
|
where: {
|
||||||
|
isActive: true,
|
||||||
|
...excludeUserInterestCondition,
|
||||||
|
},
|
||||||
|
skip,
|
||||||
|
take: limit,
|
||||||
|
orderBy: { id: 'desc' },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
activityTitle: true,
|
||||||
|
activityType: {
|
||||||
|
select: {
|
||||||
|
interestXid: true,
|
||||||
|
energyLevel: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ActivitiesMedia: {
|
||||||
|
where: { isActive: true },
|
||||||
|
select: { id: true, mediaFileName: true, mediaType: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const formattedOtherInterestActivities = await Promise.all(
|
||||||
|
otherInterestActivities.map(async a => ({
|
||||||
|
interestXid: a.activityType.interestXid,
|
||||||
|
activityId: a.id,
|
||||||
|
activityTitle: a.activityTitle,
|
||||||
|
energyLevel: a.activityType.energyLevel,
|
||||||
|
media: await attachMediaWithPresignedUrl(a.ActivitiesMedia),
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
const interestsWithActivities = otherInterests.map(interest => ({
|
||||||
|
interestId: interest.id,
|
||||||
|
interestName: interest.interestName,
|
||||||
|
interestColor: interest.interestColor,
|
||||||
|
interestImage: interest.interestImage,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
hasMore: formattedOtherInterestActivities.length === limit,
|
||||||
|
activities: formattedOtherInterestActivities.filter(
|
||||||
|
a => a.interestXid === interest.id
|
||||||
|
).map(({ interestXid, ...rest }) => rest),
|
||||||
|
}));
|
||||||
|
|
||||||
|
/* =====================================================
|
||||||
|
4️⃣ MOST HYPED
|
||||||
|
===================================================== */
|
||||||
|
const mostHypedGrouped = await tx.userBucketInterested.groupBy({
|
||||||
|
by: ['activityXid'],
|
||||||
|
where: {
|
||||||
|
isActive: true,
|
||||||
|
isBucket: false,
|
||||||
|
Activities: excludeUserInterestCondition,
|
||||||
|
},
|
||||||
|
_count: { activityXid: true },
|
||||||
|
orderBy: { _count: { activityXid: 'desc' } },
|
||||||
|
skip,
|
||||||
|
take: limit,
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalHypedCount = (
|
||||||
|
await tx.userBucketInterested.groupBy({
|
||||||
|
by: ['activityXid'],
|
||||||
|
where: {
|
||||||
|
isActive: true,
|
||||||
|
isBucket: false,
|
||||||
|
Activities: excludeUserInterestCondition,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).length;
|
||||||
|
|
||||||
|
const hypedActivities = await tx.activities.findMany({
|
||||||
|
where: { id: { in: mostHypedGrouped.map(h => h.activityXid) } },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
activityTitle: true,
|
||||||
|
activityType: { select: { energyLevel: true } },
|
||||||
|
ActivitiesMedia: {
|
||||||
|
where: { isActive: true },
|
||||||
|
select: { id: true, mediaFileName: true, mediaType: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const mostHypedActivities = await Promise.all(
|
||||||
|
mostHypedGrouped.map(async g => {
|
||||||
|
const act = hypedActivities.find(a => a.id === g.activityXid);
|
||||||
|
if (!act) return null;
|
||||||
|
return {
|
||||||
|
activityId: act.id,
|
||||||
|
activityTitle: act.activityTitle,
|
||||||
|
hypeCount: g._count.activityXid,
|
||||||
|
energyLevel: act.activityType.energyLevel,
|
||||||
|
media: await attachMediaWithPresignedUrl(act.ActivitiesMedia),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
).then(a => a.filter(Boolean));
|
||||||
|
|
||||||
|
/* =====================================================
|
||||||
|
5️⃣ NEW ARRIVALS
|
||||||
|
===================================================== */
|
||||||
|
const newArrivalsWhere = {
|
||||||
|
isActive: true,
|
||||||
|
createdAt: { gte: new Date(Date.now() - 31 * 24 * 60 * 60 * 1000) },
|
||||||
|
...excludeUserInterestCondition,
|
||||||
|
};
|
||||||
|
|
||||||
|
const newArrivalsCount = await tx.activities.count({
|
||||||
|
where: newArrivalsWhere,
|
||||||
|
});
|
||||||
|
|
||||||
|
const newArrivalsRaw = await tx.activities.findMany({
|
||||||
|
where: newArrivalsWhere,
|
||||||
|
skip,
|
||||||
|
take: limit,
|
||||||
|
orderBy: { id: 'desc' },
|
||||||
|
select: {
|
||||||
|
activityTitle: true,
|
||||||
|
activityType: { select: { energyLevel: true } },
|
||||||
|
ActivitiesMedia: {
|
||||||
|
where: { isActive: true },
|
||||||
|
select: { id: true, mediaFileName: true, mediaType: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/* =====================================================
|
||||||
|
6️⃣ OTHER STATES & OVERSEAS
|
||||||
|
===================================================== */
|
||||||
|
const otherStatesWhere: any = {
|
||||||
|
isActive: true,
|
||||||
|
...excludeUserInterestCondition,
|
||||||
|
};
|
||||||
|
if (effectiveCountryXid) otherStatesWhere.checkInCountryXid = effectiveCountryXid;
|
||||||
|
if (effectiveStateXid) otherStatesWhere.checkInStateXid = { not: effectiveStateXid };
|
||||||
|
|
||||||
|
const overseasWhere: any = {
|
||||||
|
isActive: true,
|
||||||
|
...excludeUserInterestCondition,
|
||||||
|
};
|
||||||
|
if (effectiveCountryXid) overseasWhere.checkInCountryXid = { not: effectiveCountryXid };
|
||||||
|
|
||||||
|
const [otherStatesCount, overseasCount] = await Promise.all([
|
||||||
|
tx.activities.count({ where: otherStatesWhere }),
|
||||||
|
tx.activities.count({ where: overseasWhere }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [otherStatesRaw, overseasRaw] = await Promise.all([
|
||||||
|
tx.activities.findMany({
|
||||||
|
where: otherStatesWhere,
|
||||||
|
skip,
|
||||||
|
take: limit,
|
||||||
|
select: {
|
||||||
|
activityTitle: true,
|
||||||
|
activityType: { select: { energyLevel: true } },
|
||||||
|
ActivitiesMedia: {
|
||||||
|
where: { isActive: true },
|
||||||
|
select: { id: true, mediaFileName: true, mediaType: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
tx.activities.findMany({
|
||||||
|
where: overseasWhere,
|
||||||
|
skip,
|
||||||
|
take: limit,
|
||||||
|
select: {
|
||||||
|
activityTitle: true,
|
||||||
|
activityType: { select: { energyLevel: true } },
|
||||||
|
ActivitiesMedia: {
|
||||||
|
where: { isActive: true },
|
||||||
|
select: { id: true, mediaFileName: true, mediaType: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
/* =====================================================
|
||||||
|
7️⃣ FINAL RESPONSE
|
||||||
|
===================================================== */
|
||||||
|
return {
|
||||||
|
pagination: { page, limit },
|
||||||
|
interests: interestsWithActivities,
|
||||||
|
|
||||||
|
mostHypedActivities: {
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
totalCount: totalHypedCount,
|
||||||
|
hasMore: skip + limit < totalHypedCount,
|
||||||
|
activities: mostHypedActivities,
|
||||||
|
},
|
||||||
|
|
||||||
|
newArrivalsActivities: {
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
totalCount: newArrivalsCount,
|
||||||
|
hasMore: skip + limit < newArrivalsCount,
|
||||||
|
activities: await Promise.all(
|
||||||
|
newArrivalsRaw.map(async a => ({
|
||||||
|
activityTitle: a.activityTitle,
|
||||||
|
energyLevel: a.activityType.energyLevel,
|
||||||
|
media: await attachMediaWithPresignedUrl(a.ActivitiesMedia),
|
||||||
|
}))
|
||||||
|
),
|
||||||
|
},
|
||||||
|
|
||||||
|
otherStatesActivities: {
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
totalCount: otherStatesCount,
|
||||||
|
hasMore: skip + limit < otherStatesCount,
|
||||||
|
activities: await Promise.all(
|
||||||
|
otherStatesRaw.map(async a => ({
|
||||||
|
activityTitle: a.activityTitle,
|
||||||
|
energyLevel: a.activityType.energyLevel,
|
||||||
|
media: await attachMediaWithPresignedUrl(a.ActivitiesMedia),
|
||||||
|
}))
|
||||||
|
),
|
||||||
|
},
|
||||||
|
|
||||||
|
overSeasActivities: {
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
totalCount: overseasCount,
|
||||||
|
hasMore: skip + limit < overseasCount,
|
||||||
|
activities: await Promise.all(
|
||||||
|
overseasRaw.map(async a => ({
|
||||||
|
activityTitle: a.activityTitle,
|
||||||
|
energyLevel: a.activityType.energyLevel,
|
||||||
|
media: await attachMediaWithPresignedUrl(a.ActivitiesMedia),
|
||||||
|
}))
|
||||||
|
),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async getActivityDetailsById(
|
async getActivityDetailsById(
|
||||||
userId: number,
|
userId: number,
|
||||||
activityXid: number) {
|
activityXid: number) {
|
||||||
|
|||||||
Reference in New Issue
Block a user