From be9780f9ece47a307d3cc8c48bd91113cfa94749 Mon Sep 17 00:00:00 2001 From: paritosh18 Date: Mon, 27 Apr 2026 12:51:39 +0530 Subject: [PATCH] added api for scan qr and get info of person and actvity --- serverless/functions/operator.yml | 18 + src/modules/host/dto/operator.activity.dto.ts | 54 +++ .../operator/getReservationByCheckInCode.ts | 65 +++ .../host/services/operatorActivity.service.ts | 420 ++++++++++++++++-- 4 files changed, 509 insertions(+), 48 deletions(-) create mode 100644 src/modules/host/handlers/operator/getReservationByCheckInCode.ts diff --git a/serverless/functions/operator.yml b/serverless/functions/operator.yml index 0a2e0e7..028fe29 100644 --- a/serverless/functions/operator.yml +++ b/serverless/functions/operator.yml @@ -106,3 +106,21 @@ operatorGetActivitiesByDate: - httpApi: path: /activities-by-date method: get + +operatorGetReservationByCheckInCode: + handler: src/modules/host/handlers/operator/getReservationByCheckInCode.handler + memorySize: 384 + package: + patterns: + - 'src/modules/host/handlers/operator/**' + - 'src/modules/host/services/operatorActivity.service.ts' + - 'src/modules/host/dto/operator.activity.dto.ts' + - 'src/common/**' + - ${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: /reservation-by-checkin-code + method: get diff --git a/src/modules/host/dto/operator.activity.dto.ts b/src/modules/host/dto/operator.activity.dto.ts index 9fbb2ca..a220606 100644 --- a/src/modules/host/dto/operator.activity.dto.ts +++ b/src/modules/host/dto/operator.activity.dto.ts @@ -2,6 +2,10 @@ export class GetActivitiesByDateRequestDTO { activityDate?: string; // ISO date format: YYYY-MM-DD (optional, defaults to today) } +export class GetReservationByCheckInCodeRequestDTO { + checkInCode!: string; +} + export class DateBreakdownDTO { date: string; count: number; @@ -24,3 +28,53 @@ export class GetActivitiesByDateResponseDTO { totalCount: number; }; } + +export class OperatorReservationPersonalDetailsDTO { + fullName: string; + firstName: string | null; + lastName: string | null; + role: string | null; + mobileNumber: string | null; + profileImage: string | null; + profileImagePreSignedUrl: string | null; + tags: string[]; +} + +export class OperatorReservationBookingInformationDTO { + activityName: string | null; + slot: string | null; + startTime: string | null; + endTime: string | null; + track: string | null; + trackLabel: string | null; + date: string | null; + dateLabel: string | null; + bookedOn: string | null; + bookedOnLabel: string | null; +} + +export class OperatorReservationBookingIncludedDTO { + food: string; + selectedFoodTypes: string[]; + equipment: string; + selectedEquipments: string[]; + trainerOrGuide: string; + pickupLocation: string | null; +} + +export class OperatorReservationByCheckInCodeDTO { + itineraryHeaderXid: number; + itineraryActivityXid: number; + bookingId: string | null; + checkInCode: string | null; + reservationStatus: string | null; + personalDetails: OperatorReservationPersonalDetailsDTO; + bookingInformation: OperatorReservationBookingInformationDTO; + bookingIncluded: OperatorReservationBookingIncludedDTO; +} + +export class GetReservationByCheckInCodeResponseDTO { + success: boolean; + message: string; + data: OperatorReservationByCheckInCodeDTO; +} diff --git a/src/modules/host/handlers/operator/getReservationByCheckInCode.ts b/src/modules/host/handlers/operator/getReservationByCheckInCode.ts new file mode 100644 index 0000000..8a5c8ff --- /dev/null +++ b/src/modules/host/handlers/operator/getReservationByCheckInCode.ts @@ -0,0 +1,65 @@ +import { + APIGatewayProxyEvent, + APIGatewayProxyResult, + Context, +} from 'aws-lambda'; +import { prismaClient } from '../../../../common/database/prisma.lambda.service'; +import { verifyOperatorToken } from '../../../../common/middlewares/jwt/authForHost'; +import { safeHandler } from '../../../../common/utils/handlers/safeHandler'; +import ApiError from '../../../../common/utils/helper/ApiError'; +import { GetReservationByCheckInCodeRequestDTO } from '../../dto/operator.activity.dto'; +import { OperatorActivityService } from '../../services/operatorActivity.service'; + +const operatorActivityService = new OperatorActivityService(prismaClient); + +export const handler = safeHandler( + async ( + event: APIGatewayProxyEvent, + context?: Context, + ): Promise => { + 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 operatorInfo = await verifyOperatorToken(token); + const operatorId = Number(operatorInfo.id); + + if (!operatorId || Number.isNaN(operatorId)) { + throw new ApiError(400, 'Invalid operator ID'); + } + + const requestDTO: GetReservationByCheckInCodeRequestDTO = { + checkInCode: + event.queryStringParameters?.checkInCode?.trim() || + event.queryStringParameters?.offlineCode?.trim() || + '', + }; + + if (!requestDTO.checkInCode) { + throw new ApiError(400, 'checkInCode is required.'); + } + + const result = await operatorActivityService.getReservationByCheckInCode( + operatorId, + requestDTO.checkInCode, + ); + + return { + statusCode: 200, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + body: JSON.stringify({ + success: true, + message: 'Reservation details fetched successfully', + data: result, + }), + }; + }, +); diff --git a/src/modules/host/services/operatorActivity.service.ts b/src/modules/host/services/operatorActivity.service.ts index ad406ea..4f1819b 100644 --- a/src/modules/host/services/operatorActivity.service.ts +++ b/src/modules/host/services/operatorActivity.service.ts @@ -3,12 +3,63 @@ import { PrismaClient } from '@prisma/client'; import { getPresignedUrl } from '../../../common/middlewares/aws/getPreSignedUrl'; import ApiError from '../../../common/utils/helper/ApiError'; import config from '../../../config/config'; -import { ActivitySummaryDTO } from '../dto/operator.activity.dto'; +import { + ActivitySummaryDTO, + OperatorReservationByCheckInCodeDTO, +} from '../dto/operator.activity.dto'; + +const formatDateOnly = (date: Date): string => date.toISOString().split('T')[0]; + +const formatReadableDateTime = (date: Date): string => + new Intl.DateTimeFormat('en-US', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + hour: 'numeric', + minute: '2-digit', + hour12: true, + }).format(date); + +const formatReadableDate = (date: Date): string => + new Intl.DateTimeFormat('en-US', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + }).format(date); + +const getDateLabel = (date: Date): string => { + const today = new Date(); + return formatDateOnly(today) === formatDateOnly(date) + ? 'Today' + : formatReadableDate(date); +}; + +const buildFullName = ( + firstName?: string | null, + lastName?: string | null, +): string => `${firstName ?? ''} ${lastName ?? ''}`.trim(); @Injectable() export class OperatorActivityService { constructor(private prisma: PrismaClient) {} + private async attachPresignedUrl( + fileName?: string | null, + ): Promise { + if (!fileName) { + return null; + } + + try { + return await getPresignedUrl(config.aws.bucketName, fileName); + } catch (error) { + console.error(`Failed to generate presigned URL for ${fileName}:`, error); + return null; + } + } + async getActivitiesByDate( operatorId: number, activityDate?: string, @@ -41,9 +92,7 @@ export class OperatorActivityService { const hostXids = hostMembers.map((m) => m.hostXid); // Use today's date if not provided - const queryDate = activityDate - ? new Date(activityDate) - : new Date(); + const queryDate = activityDate ? new Date(activityDate) : new Date(); // Validate date format if (isNaN(queryDate.getTime())) { @@ -59,47 +108,48 @@ export class OperatorActivityService { // Get all schedule occurrences from today onwards (not just query date) // This includes future dates to show all scheduled dates - const allScheduleOccurrences = await this.prisma.scheduleOccurences.findMany({ - where: { - occurenceDate: { - gte: queryDate, - }, - isActive: true, - scheduleHeader: { - activity: { - hostXid: { - in: hostXids, + const allScheduleOccurrences = + await this.prisma.scheduleOccurences.findMany({ + where: { + occurenceDate: { + gte: queryDate, + }, + isActive: true, + scheduleHeader: { + activity: { + hostXid: { + in: hostXids, + }, }, }, }, - }, - include: { - scheduleHeader: { - include: { - activity: { - select: { - id: true, - activityTitle: true, - isActive: true, - ActivitiesMedia: { - where: { - isCoverImage: true, - isActive: true, + include: { + scheduleHeader: { + include: { + activity: { + select: { + id: true, + activityTitle: true, + isActive: true, + ActivitiesMedia: { + where: { + isCoverImage: true, + isActive: true, + }, + select: { + mediaFileName: true, + }, + take: 1, }, - select: { - mediaFileName: true, - }, - take: 1, }, }, }, }, }, - }, - orderBy: { - occurenceDate: 'asc', - }, - }); + orderBy: { + occurenceDate: 'asc', + }, + }); if (allScheduleOccurrences.length === 0) { return { @@ -124,7 +174,7 @@ export class OperatorActivityService { // Process all schedule occurrences to build activity list with all scheduled dates for (const occurrence of allScheduleOccurrences) { const activity = occurrence.scheduleHeader.activity; - + if (!activityMap.has(activity.id)) { let coverImage: string | null = null; let coverImageUrl: string | null = null; @@ -156,12 +206,16 @@ export class OperatorActivityService { } else { // Add to scheduled dates if not already present const existingActivity = activityMap.get(activity.id)!; - const occurrenceDateStr = new Date(occurrence.occurenceDate).toISOString().split('T')[0]; + const occurrenceDateStr = new Date(occurrence.occurenceDate) + .toISOString() + .split('T')[0]; const dateExists = existingActivity.scheduledDates.some( - (d) => d.toISOString().split('T')[0] === occurrenceDateStr + (d) => d.toISOString().split('T')[0] === occurrenceDateStr, ); if (!dateExists) { - existingActivity.scheduledDates.push(new Date(occurrence.occurenceDate)); + existingActivity.scheduledDates.push( + new Date(occurrence.occurenceDate), + ); } } } @@ -200,22 +254,22 @@ export class OperatorActivityService { number, Map> >(); - + allBookings.forEach((booking) => { if (booking.activityXid) { if (!bookingsByActivityAndDate.has(booking.activityXid)) { bookingsByActivityAndDate.set(booking.activityXid, new Map()); } - + const dateStr = new Date(booking.occurenceDate) .toISOString() .split('T')[0]; const dateMap = bookingsByActivityAndDate.get(booking.activityXid)!; - + if (!dateMap.has(dateStr)) { dateMap.set(dateStr, new Set()); } - + dateMap.get(dateStr)!.add(booking.itineraryHeaderXid); } }); @@ -224,8 +278,10 @@ export class OperatorActivityService { const activities: ActivitySummaryDTO[] = Array.from( activityMap.values(), ).map((activity) => { - const activityBookings = bookingsByActivityAndDate.get(activity.activityId); - + const activityBookings = bookingsByActivityAndDate.get( + activity.activityId, + ); + // Get bookings for each scheduled date const dateBreakdown = Array.from(allScheduledDates) .sort() @@ -239,7 +295,8 @@ export class OperatorActivityService { // Count for the query date only const queryDateStr = queryDate.toISOString().split('T')[0]; - const countForQueryDate = activityBookings?.get(queryDateStr)?.size || 0; + const countForQueryDate = + activityBookings?.get(queryDateStr)?.size || 0; return { activityName: activity.activityTitle, @@ -253,7 +310,9 @@ export class OperatorActivityService { // Total count is bookings for the requested date only const queryDateStr = queryDate.toISOString().split('T')[0]; const totalCount = allBookings.filter( - (b) => new Date(b.occurenceDate).toISOString().split('T')[0] === queryDateStr + (b) => + new Date(b.occurenceDate).toISOString().split('T')[0] === + queryDateStr, ).length; return { @@ -271,4 +330,269 @@ export class OperatorActivityService { ); } } + + async getReservationByCheckInCode( + operatorId: number, + checkInCode: string, + ): Promise { + try { + const normalizedCheckInCode = checkInCode.trim(); + if (!normalizedCheckInCode) { + throw new ApiError(400, 'checkInCode is required'); + } + + const hostMembers = await this.prisma.hostMembers.findMany({ + where: { + userXid: operatorId, + isActive: true, + memberStatus: 'accepted', + deletedAt: null, + }, + select: { + hostXid: true, + }, + }); + + const hostXids = hostMembers.map((member) => member.hostXid); + if (hostXids.length === 0) { + throw new ApiError(404, 'Reservation not found for this check-in code'); + } + + const reservation = await this.prisma.itineraryDetails.findFirst({ + where: { + offlineCode: normalizedCheckInCode, + itineraryKind: 'ACTIVITY', + isActive: true, + deletedAt: null, + itineraryActivity: { + itineraryType: 'ACTIVITY', + isActive: true, + deletedAt: null, + activity: { + hostXid: { + in: hostXids, + }, + isActive: true, + deletedAt: null, + }, + }, + }, + select: { + id: true, + itineraryMemberXid: true, + offlineCode: true, + activityStatus: true, + createdAt: true, + paidOn: true, + itineraryMember: { + select: { + id: true, + memberRole: true, + member: { + select: { + id: true, + firstName: true, + lastName: true, + mobileNumber: true, + profileImage: true, + }, + }, + }, + }, + itineraryActivity: { + select: { + id: true, + occurenceDate: true, + startTime: true, + endTime: true, + bookingStatus: true, + venue: { + select: { + id: true, + venueName: true, + venueLabel: true, + }, + }, + itineraryHeader: { + select: { + id: true, + itineraryNo: true, + createdAt: true, + }, + }, + activity: { + select: { + id: true, + activityTitle: true, + checkInAddress: true, + ActivityPickUpDetails: { + where: { + isActive: true, + deletedAt: null, + }, + select: { + id: true, + isPickUp: true, + locationAddress: true, + }, + }, + }, + }, + itineraryActivitySelections: { + where: { + isActive: true, + deletedAt: null, + }, + select: { + id: true, + itineraryMemberXid: true, + isFoodOpted: true, + isTrainerOpted: true, + selectedFoodTypes: { + where: { + isActive: true, + deletedAt: null, + }, + select: { + id: true, + activityFoodType: { + select: { + foodType: { + select: { + foodTypeName: true, + }, + }, + }, + }, + }, + }, + selectedEquipments: { + where: { + isActive: true, + deletedAt: null, + }, + select: { + id: true, + activityEquipment: { + select: { + equipmentName: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }); + + if (!reservation) { + throw new ApiError(404, 'Reservation not found for this check-in code'); + } + + const activitySelection = + reservation.itineraryActivity.itineraryActivitySelections.find( + (selection) => + selection.itineraryMemberXid === reservation.itineraryMemberXid, + ) ?? null; + + const selectedFoodTypes = (activitySelection?.selectedFoodTypes ?? []) + .map( + (selectedFoodType) => + selectedFoodType.activityFoodType.foodType.foodTypeName, + ) + .filter(Boolean); + const selectedEquipments = (activitySelection?.selectedEquipments ?? []) + .map( + (selectedEquipment) => + selectedEquipment.activityEquipment.equipmentName, + ) + .filter(Boolean); + + const pickupLocation = + reservation.itineraryActivity.activity.ActivityPickUpDetails.find( + (detail) => detail.isPickUp && detail.locationAddress, + )?.locationAddress ?? + reservation.itineraryActivity.activity.ActivityPickUpDetails.find( + (detail) => detail.locationAddress, + )?.locationAddress ?? + reservation.itineraryActivity.activity.checkInAddress ?? + null; + + const member = reservation.itineraryMember.member; + const fullName = + buildFullName(member.firstName, member.lastName) || 'Guest'; + const profileImagePreSignedUrl = await this.attachPresignedUrl( + member.profileImage, + ); + const occurenceDate = new Date( + reservation.itineraryActivity.occurenceDate, + ); + const bookedOnDate = + reservation.paidOn ?? + reservation.createdAt ?? + reservation.itineraryActivity.itineraryHeader.createdAt; + + return { + itineraryHeaderXid: reservation.itineraryActivity.itineraryHeader.id, + itineraryActivityXid: reservation.itineraryActivity.id, + bookingId: reservation.itineraryActivity.itineraryHeader.itineraryNo, + checkInCode: reservation.offlineCode, + reservationStatus: + reservation.activityStatus ?? + reservation.itineraryActivity.bookingStatus, + personalDetails: { + fullName, + firstName: member.firstName, + lastName: member.lastName, + role: reservation.itineraryMember.memberRole, + mobileNumber: member.mobileNumber, + profileImage: member.profileImage, + profileImagePreSignedUrl, + tags: [], + }, + bookingInformation: { + activityName: reservation.itineraryActivity.activity.activityTitle, + slot: + reservation.itineraryActivity.startTime && + reservation.itineraryActivity.endTime + ? `${reservation.itineraryActivity.startTime} - ${reservation.itineraryActivity.endTime}` + : null, + startTime: reservation.itineraryActivity.startTime, + endTime: reservation.itineraryActivity.endTime, + track: reservation.itineraryActivity.venue?.venueName ?? null, + trackLabel: reservation.itineraryActivity.venue?.venueLabel ?? null, + date: formatDateOnly(occurenceDate), + dateLabel: getDateLabel(occurenceDate), + bookedOn: bookedOnDate ? bookedOnDate.toISOString() : null, + bookedOnLabel: bookedOnDate + ? formatReadableDateTime(bookedOnDate) + : null, + }, + bookingIncluded: { + food: + selectedFoodTypes.length > 0 ? selectedFoodTypes.join(', ') : 'No', + selectedFoodTypes, + equipment: selectedEquipments.length > 0 ? 'Yes' : 'No', + selectedEquipments, + trainerOrGuide: activitySelection?.isTrainerOpted ? 'Yes' : 'No', + pickupLocation, + }, + // description1: null, + // description2: null, + }; + } catch (error) { + if (error instanceof ApiError) { + throw error; + } + + throw new ApiError( + 500, + error instanceof Error + ? error.message + : 'Error fetching reservation details', + ); + } + } }