added api for scan qr and get info of person and actvity

This commit is contained in:
paritosh18
2026-04-27 12:51:39 +05:30
parent 3c56c45b01
commit be9780f9ec
4 changed files with 509 additions and 48 deletions

View File

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

View File

@@ -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;
}

View File

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

View File

@@ -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<string | null> {
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<string, Set<number>>
>();
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<OperatorReservationByCheckInCodeDTO> {
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',
);
}
}
}