From c216d128a6d672ea2888bc93a8d0035a5bed5da2 Mon Sep 17 00:00:00 2001 From: paritosh18 Date: Mon, 9 Feb 2026 15:34:13 +0530 Subject: [PATCH] feat: add checkAvailabilityDetails API endpoint and implement schedule details retrieval logic --- serverless/functions/user.yml | 15 +++ .../services/activityScheduling.service.ts | 96 ++++++++++++++- .../activities/checkAvailabilityDetails.ts | 113 ++++++++++++++++++ 3 files changed, 223 insertions(+), 1 deletion(-) create mode 100644 src/modules/user/handlers/activities/checkAvailabilityDetails.ts diff --git a/serverless/functions/user.yml b/serverless/functions/user.yml index 28c8050..6fda7ae 100644 --- a/serverless/functions/user.yml +++ b/serverless/functions/user.yml @@ -120,4 +120,19 @@ getActivityDetailsById: events: - httpApi: path: /user/activities/get-activity-details-by-id/{activity_xid} + method: get + +checkAvailabilityDetails: + handler: src/modules/user/handlers/activities/checkAvailabilityDetails.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/check-availability/{activity_xid} method: get \ No newline at end of file diff --git a/src/modules/host/services/activityScheduling.service.ts b/src/modules/host/services/activityScheduling.service.ts index 9bfbed6..f30c2e1 100644 --- a/src/modules/host/services/activityScheduling.service.ts +++ b/src/modules/host/services/activityScheduling.service.ts @@ -1,9 +1,9 @@ import { Injectable } from '@nestjs/common'; import { PrismaClient } from '@prisma/client'; +import { getPresignedUrl } from '../../../common/middlewares/aws/getPreSignedUrl'; import { ACTIVITY_AM_DISPLAY_STATUS, ACTIVITY_AM_INTERNAL_STATUS, ACTIVITY_DISPLAY_STATUS, ACTIVITY_INTERNAL_STATUS, SCHEDULING_TYPE } from '../../../common/utils/constants/host.constant'; import ApiError from '../../../common/utils/helper/ApiError'; import { ScheduleActivityDTO } from '../../../common/utils/validation/host/createSchedulingOfAct.validation'; -import { getPresignedUrl } from '../../../common/middlewares/aws/getPreSignedUrl'; import config from '../../../config/config'; @@ -317,6 +317,100 @@ export class SchedulingService { return response; } + /** + * Return full schedule header + venue + slots for a given activity and date + */ + async getScheduleDetailsForDate( + activityXid: number, + selectedDate: string + ) { + const date = new Date(selectedDate); + + if (isNaN(date.getTime())) { + throw new ApiError(400, 'Invalid date format'); + } + + const weekDay = date.toLocaleDateString('en-US', { weekday: 'long' }).toUpperCase(); + const dayOfMonth = date.getDate(); + + const scheduleHeaders = await this.prisma.scheduleHeader.findMany({ + where: { + activityXid, + isActive: true, + startDate: { lte: date }, + OR: [ + { endDate: null }, + { endDate: { gte: date } }, + ], + }, + include: { + activityVenue: { + select: { + id: true, + venueName: true, + venueLabel: true, + venueCapacity: true, + }, + }, + scheduleRecurrences: { + where: { isActive: true }, + }, + ScheduleDetails: { + where: { + isActive: true, + OR: [ + { occurenceDate: date }, + { weekDay: weekDay }, + { dayOfMonth: dayOfMonth }, + ], + }, + }, + Cancellations: { + where: { + occurenceDate: date, + isActive: true, + }, + }, + }, + }); + + if (!scheduleHeaders.length) return []; + + const response = scheduleHeaders.map((header) => { + const cancelledSlotIds = new Set(header.Cancellations.map(c => c.slotXid)); + + const slots = header.ScheduleDetails + .filter(slot => !cancelledSlotIds.has(slot.id)) + .map(slot => ({ + slotId: slot.id, + occurenceDate: slot.occurenceDate, + weekDay: slot.weekDay, + dayOfMonth: slot.dayOfMonth, + startTime: slot.startTime, + endTime: slot.endTime, + maxCapacity: slot.maxCapacity, + })); + + return { + scheduleHeaderXid: header.id, + scheduleType: header.scheduleType, + startDate: header.startDate, + endDate: header.endDate, + earlyCheckInMins: header.earlyCheckInMins, + bookingCutOffMins: header.bookingCutOffMins, + activityVenue: { + venueXid: header.activityVenue.id, + venueName: header.activityVenue.venueName, + venueLabel: header.activityVenue.venueLabel, + venueCapacity: header.activityVenue.venueCapacity, + }, + slots, + }; + }); + + return response; + } + async getVenueFromVenueXid(venueXid: number, activityXid: number) { return await this.prisma.activityVenues.findUnique({ where: { id: venueXid, activityXid: activityXid, isActive: true }, diff --git a/src/modules/user/handlers/activities/checkAvailabilityDetails.ts b/src/modules/user/handlers/activities/checkAvailabilityDetails.ts new file mode 100644 index 0000000..72c80ab --- /dev/null +++ b/src/modules/user/handlers/activities/checkAvailabilityDetails.ts @@ -0,0 +1,113 @@ +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 { SchedulingService } from '../../../host/services/activityScheduling.service'; +import { UserService } from '../../services/user.service'; + +const userService = new UserService(prismaClient); +const schedulingService = new SchedulingService(prismaClient); + +export const handler = safeHandler(async ( + event: APIGatewayProxyEvent, + context?: Context +): Promise => { + // 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 activityXid = Number(event.pathParameters?.activity_xid); + + if (!activityXid || isNaN(activityXid)) { + throw new ApiError(400, 'Valid activityXid is required'); +} + + // selected date may be passed as query param `selectedDate` + const selectedDate = + event.queryStringParameters?.selectedDate || + event.queryStringParameters?.date || + (event.body ? JSON.parse(event.body).selectedDate : undefined); + + if (!selectedDate) { + throw new ApiError(400, 'selectedDate query parameter is required'); + } + + // Fetch activity details (basic) and schedule details for the selected date + const activityDetails = await userService.getActivityDetailsById(userId, activityXid); + const scheduleDetails = await schedulingService.getScheduleDetailsForDate(activityXid, selectedDate); + // Shape response to match UI: only include fields shown in image + const activity = activityDetails.activity; + + // Rooms: combine ActivityVenues with schedule info per venue + const rooms = (activity.ActivityVenues || []).map((v: any) => { + // find schedule header for this venue + const header = scheduleDetails.find((h: any) => h.activityVenue?.venueXid === v.id); + const slotCount = header ? (header.slots || []).length : 0; + + return { + venueXid: v.id, + venueName: v.venueName, + venueLabel: v.venueLabel, + venueCapacity: v.venueCapacity, + availableSeats: v.availableSeats ?? null, + price: v.ActivityPrices?.[0]?.sellPrice ?? null, + slotsCount: slotCount, + }; + }); + + // Slots: aggregate slots across scheduleDetails + const slots: any[] = []; + for (const h of scheduleDetails) { + for (const s of h.slots || []) { + // status heuristic based on maxCapacity + let status = 'Available'; + if (s.maxCapacity === 0) status = 'Housefull'; + else if (s.maxCapacity <= 2) status = '2 Slots Left'; + else if (s.maxCapacity <= 5) status = 'Fast Filling'; + + slots.push({ + slotId: s.slotId, + startTime: s.startTime, + endTime: s.endTime, + status, + maxCapacity: s.maxCapacity, + venueXid: h.activityVenue?.venueXid, + }); + } + } + + // derive check-in/out from slot times (earliest start, latest end) + const startTimes = slots.map(s => s.startTime).filter(Boolean); + const endTimes = slots.map(s => s.endTime).filter(Boolean); + const checkInTime = startTimes.length ? startTimes.sort()[0] : null; + const checkOutTime = endTimes.length ? endTimes.sort().reverse()[0] : null; + + const responsePayload = { + selectedDate, + rooms, + slots, + checkInTime, + checkOutTime, + }; + + return { + statusCode: 200, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + body: JSON.stringify({ success: true, data: responsePayload }), + }; +}); +