Files
MinglarBackendNestJS/src/modules/user/services/itinerary.service.ts
2026-04-27 20:02:21 +05:30

5046 lines
169 KiB
TypeScript

import { Injectable } from '@nestjs/common';
import { Prisma, PrismaClient } from '@prisma/client';
import crypto from 'crypto';
import { getPresignedUrl } from '../../../common/middlewares/aws/getPreSignedUrl';
import {
ACTIVITY_AM_INTERNAL_STATUS,
ACTIVITY_INTERNAL_STATUS,
} from '../../../common/utils/constants/host.constant';
import ApiError from '../../../common/utils/helper/ApiError';
import config from '@/config/config';
const bucket = config.aws.bucketName;
const WEEKDAY_NAMES = [
'SUNDAY',
'MONDAY',
'TUESDAY',
'WEDNESDAY',
'THURSDAY',
'FRIDAY',
'SATURDAY',
] as const;
const attachPresignedUrl = async (file: string | null | undefined) => {
if (!file) return null;
const key = file.startsWith('http')
? new URL(file).pathname.replace(/^\/+/, '')
: file;
return getPresignedUrl(bucket, key);
};
const generateCheckInCode = () => `#${crypto.randomInt(1000000, 10000000)}`;
type CheckoutTaxRow = {
id: number;
taxName: string;
taxPer: number;
taxAmount: number;
};
type CheckoutChargeItem = {
id: number;
baseAmount: number;
totalAmount: number | null;
taxes: CheckoutTaxRow[];
};
type CheckoutChargeSummary = {
items: Array<{
id: number;
baseAmount: number;
totalAmount: number;
taxAmount: number;
taxes: CheckoutTaxRow[];
}>;
baseAmount: number;
taxAmount: number;
totalAmount: number;
};
const normalizeCheckoutKind = (kind?: string | null) =>
(kind ?? '')
.trim()
.toUpperCase()
.replace(/\s+/g, '_');
const sumCheckoutValues = (values: Array<number | null | undefined>) =>
values.reduce((acc, value) => acc + (Number(value) || 0), 0);
const mapCheckoutTaxes = (rows: any[] = []): CheckoutTaxRow[] =>
rows.map((row) => ({
id: row.id,
taxName: row.taxes?.taxName ?? '',
taxPer: Number(row.taxPer) || Number(row.taxes?.taxPer) || 0,
taxAmount: Number(row.taxAmount) || 0,
}));
const summarizeCheckoutRows = (
rows: CheckoutChargeItem[],
): CheckoutChargeSummary => {
const items = rows.map((row) => {
const taxAmount = sumCheckoutValues(row.taxes.map((tax) => tax.taxAmount));
const totalAmount = row.totalAmount ?? row.baseAmount + taxAmount;
return {
id: row.id,
baseAmount: row.baseAmount,
totalAmount,
taxAmount,
taxes: row.taxes,
};
});
return {
items,
baseAmount: sumCheckoutValues(items.map((item) => item.baseAmount)),
taxAmount: sumCheckoutValues(items.map((item) => item.taxAmount)),
totalAmount: sumCheckoutValues(items.map((item) => item.totalAmount)),
};
};
const pickCheckoutSummary = (
groups: Map<string, CheckoutChargeItem[]>,
aliases: string[],
) => {
for (const alias of aliases) {
const rows = groups.get(alias);
if (rows?.length) {
return summarizeCheckoutRows(rows);
}
}
return null;
};
const attachMediaWithPresignedUrl = async (
mediaArr: Array<{
id: number;
mediaType: string;
mediaFileName: string;
isCoverImage: boolean;
displayOrder: number;
}> = [],
) => {
return Promise.all(
mediaArr.map(async (media) => ({
id: media.id,
mediaType: media.mediaType,
mediaFileName: media.mediaFileName,
isCoverImage: media.isCoverImage,
displayOrder: media.displayOrder,
presignedUrl: await attachPresignedUrl(media.mediaFileName),
})),
);
};
const calculateDistance = (
lat1: number | null,
lon1: number | null,
lat2: number | null,
lon2: number | null,
) => {
if (
lat1 === null ||
lon1 === null ||
lat2 === null ||
lon2 === null ||
Number.isNaN(lat1) ||
Number.isNaN(lon1) ||
Number.isNaN(lat2) ||
Number.isNaN(lon2)
) {
return null;
}
const R = 6371;
const dLat = ((lat2 - lat1) * Math.PI) / 180;
const dLon = ((lon2 - lon1) * Math.PI) / 180;
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos((lat1 * Math.PI) / 180) *
Math.cos((lat2 * Math.PI) / 180) *
Math.sin(dLon / 2) *
Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return Number((R * c).toFixed(2));
};
const parseDateValue = (value: string | Date) => {
if (value instanceof Date) {
return new Date(value.getTime());
}
const trimmedValue = value.trim();
const isoMatch = trimmedValue.match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (isoMatch) {
const [, year, month, day] = isoMatch;
return new Date(Number(year), Number(month) - 1, Number(day));
}
const slashMatch = trimmedValue.match(/^(\d{4})\/(\d{2})\/(\d{2})$/);
if (slashMatch) {
const [, year, month, day] = slashMatch;
return new Date(Number(year), Number(month) - 1, Number(day));
}
const parsed = new Date(trimmedValue);
return parsed;
};
const parseTimeValue = (value: string) => {
const trimmedValue = value.trim().toUpperCase().replace(/\s+/g, ' ');
const match = trimmedValue.match(
/^(\d{1,2})(?::(\d{2}))?(?::(\d{2}))?\s*(AM|PM)?$/,
);
if (!match) {
return null;
}
let hours = Number(match[1]);
const minutes = Number(match[2] ?? '0');
const seconds = Number(match[3] ?? '0');
const meridiem = match[4];
if (minutes > 59 || seconds > 59) {
return null;
}
if (meridiem) {
if (hours < 1 || hours > 12) {
return null;
}
if (meridiem === 'AM') {
hours = hours === 12 ? 0 : hours;
} else {
hours = hours === 12 ? 12 : hours + 12;
}
} else if (hours > 23) {
return null;
}
return { hours, minutes, seconds };
};
const normalizeTimeValue = (value: string) => {
const parsed = parseTimeValue(value);
if (!parsed) {
return null;
}
return `${String(parsed.hours).padStart(2, '0')}:${String(
parsed.minutes,
).padStart(2, '0')}:${String(parsed.seconds).padStart(2, '0')}`;
};
const combineDateAndTime = (dateValue: string | Date, timeValue: string) => {
const date = parseDateValue(dateValue);
const time = parseTimeValue(timeValue);
if (Number.isNaN(date.getTime()) || !time) {
return null;
}
date.setHours(time.hours, time.minutes, time.seconds, 0);
return date;
};
const startOfDay = (date: Date) => {
const value = new Date(date.getTime());
value.setHours(0, 0, 0, 0);
return value;
};
const formatDateKey = (date: Date) => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
};
const addMinutes = (date: Date, minutes: number) =>
new Date(date.getTime() + minutes * 60 * 1000);
const formatTicketTime = (value?: string | null) => {
if (!value) {
return null;
}
return value.trim().toUpperCase().replace(/\s+(AM|PM)$/i, '$1');
};
const formatTicketTimeRange = (
startTime?: string | null,
endTime?: string | null,
) => {
const formattedStartTime = formatTicketTime(startTime);
const formattedEndTime = formatTicketTime(endTime);
if (formattedStartTime && formattedEndTime) {
return `${formattedStartTime} - ${formattedEndTime}`;
}
return formattedStartTime ?? formattedEndTime ?? null;
};
const getDateRange = (fromDate: Date, toDate: Date) => {
const dates: Date[] = [];
const cursor = startOfDay(fromDate);
const end = startOfDay(toDate);
while (cursor <= end) {
dates.push(new Date(cursor.getTime()));
cursor.setDate(cursor.getDate() + 1);
}
return dates;
};
const getUniqueDatesForScheduleDetail = (
slot: {
occurenceDate: Date | null;
weekDay: string | null;
dayOfMonth: number | null;
},
fromDate: Date,
toDate: Date,
) => {
if (slot.occurenceDate) {
const occurrenceDay = startOfDay(slot.occurenceDate);
if (
occurrenceDay >= startOfDay(fromDate) &&
occurrenceDay <= startOfDay(toDate)
) {
return [occurrenceDay];
}
return [];
}
const dates: Date[] = [];
for (const currentDate of getDateRange(fromDate, toDate)) {
const weekDayName = WEEKDAY_NAMES[currentDate.getDay()];
if (slot.weekDay && slot.weekDay !== weekDayName) {
continue;
}
if (
slot.dayOfMonth !== null &&
slot.dayOfMonth !== undefined &&
slot.dayOfMonth !== currentDate.getDate()
) {
continue;
}
dates.push(currentDate);
}
return dates;
};
@Injectable()
export class ItineraryService {
constructor(private prisma: PrismaClient) { }
async getUserItineraryDetails(userXid: number) {
const [userLocation, activityEntries, travellerType, energyLevel] = await Promise.all([
this.prisma.userAddressDetails.findFirst({
where: {
userXid,
isActive: true,
deletedAt: null,
},
orderBy: {
createdAt: 'desc',
},
select: {
id: true,
address1: true,
address2: true,
pinCode: true,
locationName: true,
locationAddress: true,
locationLat: true,
locationLong: true,
countryXid: true,
stateXid: true,
cityXid: true,
country: {
select: {
id: true,
countryName: true,
countryCode: true,
},
},
states: {
select: {
id: true,
stateName: true,
},
},
cities: {
select: {
id: true,
cityName: true,
},
},
},
}),
this.prisma.userBucketInterested.findMany({
where: {
userXid,
isActive: true,
deletedAt: null,
Activities: {
isActive: true,
deletedAt: null,
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
},
},
orderBy: {
createdAt: 'desc',
},
select: {
id: true,
isBucket: true,
bucketTypeName: true,
activityStatus: true,
createdAt: true,
activityXid: true,
Activities: {
select: {
id: true,
activityTitle: true,
activityDescription: true,
activityDurationMins: true,
checkInAddress: true,
checkInLat: true,
checkInLong: true,
activityType: {
select: {
energyLevel: {
select: {
id: true,
energyLevelName: true,
energyIcon: true,
},
},
},
},
ActivitiesMedia: {
where: {
isActive: true,
deletedAt: null,
},
orderBy: {
displayOrder: 'asc',
},
select: {
id: true,
mediaType: true,
mediaFileName: true,
isCoverImage: true,
displayOrder: true,
},
},
},
},
},
}),
this.prisma.allowedEntryTypes.findMany({
where: {
isActive: true,
deletedAt: null,
},
select: {
id: true,
allowedEntryTypeName: true,
},
orderBy: {
id: 'asc',
},
}),
this.prisma.energyLevels.findMany({
where: {
isActive: true,
deletedAt: null,
},
select: {
id: true,
energyLevelName: true,
energyIcon: true,
energyColor: true,
},
orderBy: {
id: 'asc',
}
})
]);
const formattedActivities = await Promise.all(
activityEntries.map(async (entry) => {
const coverImage =
entry.Activities?.ActivitiesMedia.find((media) => media.isCoverImage) ??
entry.Activities?.ActivitiesMedia[0] ??
null;
return {
id: entry.id,
activityXid: entry.activityXid,
isBucket: entry.isBucket,
bucketTypeName: entry.bucketTypeName,
activityStatus: entry.activityStatus,
addedOn: entry.createdAt,
activityDetails: {
id: entry.Activities?.id ?? null,
activityTitle: entry.Activities?.activityTitle ?? null,
activityDescription: entry.Activities?.activityDescription ?? null,
activityDurationMins: entry.Activities?.activityDurationMins ?? null,
checkInAddress: entry.Activities?.checkInAddress ?? null,
checkInLat: entry.Activities?.checkInLat ?? null,
checkInLong: entry.Activities?.checkInLong ?? null,
coverImage: coverImage?.mediaFileName ?? null,
coverImagePresignedUrl: await attachPresignedUrl(
coverImage?.mediaFileName,
),
energyLevel: entry.Activities?.activityType?.energyLevel
? {
id: entry.Activities.activityType.energyLevel.id,
energyLevelName:
entry.Activities.activityType.energyLevel.energyLevelName,
energyIcon:
entry.Activities.activityType.energyLevel.energyIcon,
energyIconPresignedUrl: await attachPresignedUrl(
entry.Activities.activityType.energyLevel.energyIcon,
),
}
: null,
media: await attachMediaWithPresignedUrl(
entry.Activities?.ActivitiesMedia ?? [],
),
},
};
}),
);
const latestAddedActivity = formattedActivities[0] ?? null;
const formattedMasterEnergyLevels = await Promise.all(
energyLevel.map(async (item) => ({
...item,
energyIconPresignedUrl: await attachPresignedUrl(item.energyIcon),
})),
);
return {
userLocation,
travellerType,
energyLevel: formattedMasterEnergyLevels,
bucketCount: formattedActivities.filter((item) => item.isBucket).length,
interestedCount: formattedActivities.filter((item) => !item.isBucket).length,
latestAddedActivityCoverImage:
latestAddedActivity?.activityDetails.coverImage ?? null,
latestAddedActivityCoverImagePresignedUrl:
latestAddedActivity?.activityDetails.coverImagePresignedUrl ?? null,
bucketActivities: formattedActivities.filter((item) => item.isBucket),
interestedActivities: formattedActivities.filter((item) => !item.isBucket),
};
}
async getItineraryCheckoutDetails(userXid: number, itineraryHeaderXid: number) {
const itinerary = await this.prisma.itineraryHeader.findFirst({
where: {
id: itineraryHeaderXid,
ownerXid: userXid,
isActive: true,
deletedAt: null,
},
select: {
id: true,
itineraryNo: true,
title: true,
fromDate: true,
fromTime: true,
toDate: true,
toTime: true,
itineraryStatus: true,
ItineraryActivities: {
where: { isActive: true, deletedAt: null },
orderBy: { displayOrder: 'asc' },
select: {
id: true,
displayOrder: true,
itineraryType: true,
activityXid: true,
venueXid: true,
scheduledHeaderXid: true,
occurenceDate: true,
startTime: true,
endTime: true,
endDate: true,
paxCount: true,
totalAmount: true,
bookingStatus: true,
activity: {
select: {
id: true,
activityTitle: true,
activityDescription: true,
ActivitiesMedia: {
where: { isActive: true, isCoverImage: true, deletedAt: null },
orderBy: { displayOrder: 'asc' },
take: 1,
select: { id: true, mediaFileName: true, mediaType: true, displayOrder: true },
},
activityFoodTypes: {
where: { isActive: true, deletedAt: null },
select: {
id: true,
foodTypeXid: true,
foodType: { select: { id: true, foodTypeName: true } },
},
},
ActivityFoodCost: {
where: { isActive: true, deletedAt: null },
select: {
id: true,
foodTypesId: true,
baseAmount: true,
totalAmount: true,
ActivityFoodTaxes: {
where: { isActive: true, deletedAt: null },
select: {
id: true,
taxPer: true,
taxAmount: true,
taxes: { select: { id: true, taxName: true, taxPer: true } },
},
},
},
},
ActivityTrainers: {
where: { isActive: true, deletedAt: null },
select: {
id: true,
baseAmount: true,
totalAmount: true,
ActivityTrainerTaxes: {
where: { isActive: true, deletedAt: null },
select: {
id: true,
taxPer: true,
taxAmount: true,
taxes: { select: { id: true, taxName: true, taxPer: true } },
},
},
},
},
ActivityNavigationModes: {
where: { isActive: true, deletedAt: null },
select: {
id: true,
navigationModeName: true,
navigationModesBasePrice: true,
navigationModesTotalPrice: true,
ActivityNavigationModesTaxes: {
where: { isActive: true, deletedAt: null },
select: {
id: true,
taxPer: true,
taxAmount: true,
taxes: { select: { id: true, taxName: true, taxPer: true } },
},
},
},
},
ActivityPickUpDetails: {
where: { isActive: true, deletedAt: null },
select: {
id: true,
isPickUp: true,
locationLat: true,
locationLong: true,
locationAddress: true,
transportBasePrice: true,
transportTotalPrice: true,
activityPickUpTransportTaxes: {
where: { isActive: true, deletedAt: null },
select: {
id: true,
taxPer: true,
taxAmount: true,
taxes: { select: { id: true, taxName: true, taxPer: true } },
},
},
},
},
activityPickUpTransports: {
where: { isActive: true, deletedAt: null },
select: {
id: true,
transportModeXid: true,
transportMode: {
select: {
id: true,
transportModeName: true,
transportModeIcon: true,
},
},
},
},
ActivityEquipments: {
where: { isActive: true, deletedAt: null },
select: {
id: true,
equipmentName: true,
equipmentBasePrice: true,
equipmentTotalPrice: true,
ActivityEquipmentTaxes: {
where: { isActive: true, deletedAt: null },
select: {
id: true,
taxPer: true,
taxAmount: true,
taxes: { select: { id: true, taxName: true, taxPer: true } },
},
},
},
},
ActivityVenues: {
where: { isActive: true, deletedAt: null },
select: {
id: true,
venueName: true,
venueLabel: true,
venueCapacity: true,
availableSeats: true,
ActivityPrices: {
where: { isActive: true, deletedAt: null },
orderBy: { createdAt: 'asc' },
select: {
id: true,
noOfSession: true,
isPackage: true,
sessionValidity: true,
sessionValidityFrequency: true,
basePrice: true,
sellPrice: true,
ActivityPriceTaxes: {
where: { isActive: true, deletedAt: null },
select: {
id: true,
taxXid: true,
taxPer: true,
taxAmount: true,
taxes: {
select: {
id: true,
taxName: true,
taxPer: true,
},
},
},
},
},
},
},
},
},
},
itineraryActivitySelections: {
where: { isActive: true, deletedAt: null },
select: {
id: true,
itineraryMemberXid: true,
isFoodOpted: true,
isTrainerOpted: true,
isInActivityNavigationOpted: true,
itineraryMember: {
select: {
id: true,
memberXid: true,
memberRole: true,
memberStatus: true,
member: {
select: {
id: true,
firstName: true,
lastName: true,
mobileNumber: true,
},
},
},
},
selectedFoodTypes: {
where: { isActive: true, deletedAt: null },
select: {
id: true,
activityFoodTypeXid: true,
activityFoodType: {
select: {
id: true,
foodTypeXid: true,
foodType: {
select: {
id: true,
foodTypeName: true,
},
},
},
},
},
},
activityNavigationMode: {
select: {
id: true,
navigationModeName: true,
navigationModesBasePrice: true,
navigationModesTotalPrice: true,
ActivityNavigationModesTaxes: {
where: { isActive: true, deletedAt: null },
select: {
id: true,
taxPer: true,
taxAmount: true,
taxes: { select: { id: true, taxName: true, taxPer: true } },
},
},
},
},
selectedEquipments: {
where: { isActive: true, deletedAt: null },
select: {
id: true,
activityEquipmentXid: true,
activityEquipment: {
select: {
id: true,
equipmentName: true,
equipmentBasePrice: true,
equipmentTotalPrice: true,
ActivityEquipmentTaxes: {
where: { isActive: true, deletedAt: null },
select: {
id: true,
taxPer: true,
taxAmount: true,
taxes: { select: { id: true, taxName: true, taxPer: true } },
},
},
},
},
},
},
},
},
ItineraryDetails: {
where: { isActive: true, deletedAt: null },
select: {
id: true,
itineraryMemberXid: true,
itineraryKind: true,
hasOpted: true,
baseAmount: true,
totalAmount: true,
description1: true,
description2: true,
offlineCode: true,
activityStatus: true,
isChargeable: true,
itineraryMember: {
select: {
id: true,
memberXid: true,
memberRole: true,
memberStatus: true,
member: {
select: {
id: true,
firstName: true,
lastName: true,
mobileNumber: true,
},
},
},
},
ItineraryDetailTaxes: {
where: { isActive: true, deletedAt: null },
select: {
id: true,
taxPer: true,
taxAmount: true,
taxes: { select: { id: true, taxName: true, taxPer: true } },
},
},
},
},
},
},
},
});
if (!itinerary) {
throw new ApiError(404, 'Itinerary not found.');
}
const activities = await Promise.all(
(itinerary.ItineraryActivities ?? []).map(async (item) => {
const coverImage = item.activity?.ActivitiesMedia?.[0]?.mediaFileName ?? null;
const coverImagePresignedUrl = await attachPresignedUrl(coverImage);
const selectedVenue = item.activity?.ActivityVenues?.find(
(venue) => venue.id === item.venueXid,
) ?? null;
const venueForPricing =
selectedVenue ?? item.activity?.ActivityVenues?.[0] ?? null;
const venuePrices = venueForPricing?.ActivityPrices ?? [];
const activityPriceDetails = venuePrices.reduce(
(lowest, current) =>
!lowest || current.sellPrice < lowest.sellPrice ? current : lowest,
null as (typeof venuePrices)[number] | null,
);
const selectedVenueCharge = activityPriceDetails
? summarizeCheckoutRows([
{
id: activityPriceDetails.id,
baseAmount: Number(activityPriceDetails.basePrice) || 0,
totalAmount:
activityPriceDetails.sellPrice === null
? null
: Number(activityPriceDetails.sellPrice),
taxes: mapCheckoutTaxes(activityPriceDetails.ActivityPriceTaxes ?? []),
},
])
: { items: [], baseAmount: 0, taxAmount: 0, totalAmount: 0 };
const details = item.ItineraryDetails ?? [];
const memberSelectionGroups = new Map<number, any[]>();
for (const detail of details) {
const list = memberSelectionGroups.get(detail.itineraryMemberXid) ?? [];
list.push(detail);
memberSelectionGroups.set(detail.itineraryMemberXid, list);
}
const memberSelections = (item.itineraryActivitySelections ?? []).map((selection) => {
const memberDetails = memberSelectionGroups.get(selection.itineraryMemberXid) ?? [];
const detailGroups = new Map<string, CheckoutChargeItem[]>();
for (const detail of memberDetails) {
const key = normalizeCheckoutKind(detail.itineraryKind);
const list = detailGroups.get(key) ?? [];
list.push({
id: detail.id,
baseAmount: Number(detail.baseAmount) || 0,
totalAmount: detail.totalAmount === null ? null : Number(detail.totalAmount),
taxes: mapCheckoutTaxes(detail.ItineraryDetailTaxes ?? []),
});
detailGroups.set(key, list);
}
const activityCharge =
pickCheckoutSummary(detailGroups, ['ACTIVITY']) ??
selectedVenueCharge;
const foodCharge =
pickCheckoutSummary(detailGroups, ['FOOD']) ??
summarizeCheckoutRows(
selection.isFoodOpted
? selection.selectedFoodTypes
.map((selectedFoodType) => {
const matchedCost =
item.activity?.ActivityFoodCost.find(
(cost) => cost.foodTypesId === selectedFoodType.activityFoodType.foodTypeXid,
) ?? item.activity?.ActivityFoodCost?.[0];
return matchedCost
? {
id: matchedCost.id,
baseAmount: Number(matchedCost.baseAmount) || 0,
totalAmount:
matchedCost.totalAmount === null
? null
: Number(matchedCost.totalAmount),
taxes: mapCheckoutTaxes(matchedCost.ActivityFoodTaxes ?? []),
}
: null;
})
.filter(Boolean) as CheckoutChargeItem[]
: [],
);
const trainerCharge =
pickCheckoutSummary(detailGroups, ['TRAINER']) ??
(selection.isTrainerOpted && item.activity?.ActivityTrainers?.[0]
? summarizeCheckoutRows([
{
id: item.activity.ActivityTrainers[0].id,
baseAmount: Number(item.activity.ActivityTrainers[0].baseAmount) || 0,
totalAmount:
item.activity.ActivityTrainers[0].totalAmount === null
? null
: Number(item.activity.ActivityTrainers[0].totalAmount),
taxes: mapCheckoutTaxes(
item.activity.ActivityTrainers[0].ActivityTrainerTaxes ?? [],
),
},
])
: { items: [], baseAmount: 0, taxAmount: 0, totalAmount: 0 });
const navigationCharge =
pickCheckoutSummary(detailGroups, ['NAVIGATION', 'IN_ACTIVITY_NAVIGATION']) ??
(selection.isInActivityNavigationOpted && selection.activityNavigationMode
? summarizeCheckoutRows([
{
id: selection.activityNavigationMode.id,
baseAmount:
Number(selection.activityNavigationMode.navigationModesBasePrice) || 0,
totalAmount:
selection.activityNavigationMode.navigationModesTotalPrice === null
? null
: Number(selection.activityNavigationMode.navigationModesTotalPrice),
taxes: mapCheckoutTaxes(
selection.activityNavigationMode.ActivityNavigationModesTaxes ?? [],
),
},
])
: { items: [], baseAmount: 0, taxAmount: 0, totalAmount: 0 });
const pickupCharge =
pickCheckoutSummary(detailGroups, ['PICKUP', 'PICK_UP', 'PICKUP_DROP']) ??
summarizeCheckoutRows(
(item.activity?.ActivityPickUpDetails ?? []).map((pickUpDetail) => ({
id: pickUpDetail.id,
baseAmount: Number(pickUpDetail.transportBasePrice) || 0,
totalAmount:
pickUpDetail.transportTotalPrice === null
? null
: Number(pickUpDetail.transportTotalPrice),
taxes: mapCheckoutTaxes(pickUpDetail.activityPickUpTransportTaxes ?? []),
})),
);
const equipmentCharge =
pickCheckoutSummary(detailGroups, ['EQUIPMENT']) ??
summarizeCheckoutRows(
selection.selectedEquipments.map((selectedEquipment) => ({
id: selectedEquipment.activityEquipment.id,
baseAmount: Number(selectedEquipment.activityEquipment.equipmentBasePrice) || 0,
totalAmount:
selectedEquipment.activityEquipment.equipmentTotalPrice === null
? null
: Number(selectedEquipment.activityEquipment.equipmentTotalPrice),
taxes: mapCheckoutTaxes(
selectedEquipment.activityEquipment.ActivityEquipmentTaxes ?? [],
),
})),
);
return {
id: selection.id,
itineraryMemberXid: selection.itineraryMemberXid,
member: selection.itineraryMember,
selectedFoodTypes: selection.selectedFoodTypes.map((selectedFoodType) => ({
id: selectedFoodType.id,
activityFoodTypeXid: selectedFoodType.activityFoodTypeXid,
foodTypeXid: selectedFoodType.activityFoodType.foodTypeXid,
foodTypeName: selectedFoodType.activityFoodType.foodType.foodTypeName,
})),
selectedNavigationMode: selection.activityNavigationMode
? {
id: selection.activityNavigationMode.id,
navigationModeName: selection.activityNavigationMode.navigationModeName,
baseAmount:
Number(selection.activityNavigationMode.navigationModesBasePrice) || 0,
totalAmount:
Number(selection.activityNavigationMode.navigationModesTotalPrice) || 0,
taxes: mapCheckoutTaxes(
selection.activityNavigationMode.ActivityNavigationModesTaxes ?? [],
),
}
: null,
selectedEquipments: selection.selectedEquipments.map((selectedEquipment) => ({
id: selectedEquipment.id,
activityEquipmentXid: selectedEquipment.activityEquipmentXid,
equipmentName: selectedEquipment.activityEquipment.equipmentName,
baseAmount: Number(selectedEquipment.activityEquipment.equipmentBasePrice) || 0,
totalAmount: Number(selectedEquipment.activityEquipment.equipmentTotalPrice) || 0,
taxes: mapCheckoutTaxes(
selectedEquipment.activityEquipment.ActivityEquipmentTaxes ?? [],
),
})),
pricing: {
activity: activityCharge,
food: foodCharge,
trainer: trainerCharge,
navigation: navigationCharge,
pickup: pickupCharge,
equipment: equipmentCharge,
totalAmount:
activityCharge.totalAmount +
foodCharge.totalAmount +
trainerCharge.totalAmount +
navigationCharge.totalAmount +
pickupCharge.totalAmount +
equipmentCharge.totalAmount,
},
pricingSource: memberDetails.length ? 'itinerary_details' : 'fallback',
detailRows: memberDetails.map((detail) => ({
id: detail.id,
itineraryKind: detail.itineraryKind,
hasOpted: detail.hasOpted,
baseAmount: detail.baseAmount,
totalAmount: detail.totalAmount,
description1: detail.description1,
description2: detail.description2,
offlineCode: detail.offlineCode,
activityStatus: detail.activityStatus,
isChargeable: detail.isChargeable,
taxes: mapCheckoutTaxes(detail.ItineraryDetailTaxes ?? []),
member: detail.itineraryMember,
})),
};
});
const activityTotalAmount = Number(selectedVenueCharge.totalAmount) || 0;
return {
itineraryActivityXid: item.id,
displayOrder: item.displayOrder,
itineraryType: item.itineraryType,
activityXid: item.activityXid,
venueXid: item.venueXid,
scheduledHeaderXid: item.scheduledHeaderXid,
occurenceDate: item.occurenceDate,
startTime: item.startTime,
endTime: item.endTime,
endDate: item.endDate,
paxCount: item.paxCount,
bookingStatus: item.bookingStatus,
activity: {
id: item.activity?.id ?? null,
activityTitle: item.activity?.activityTitle ?? null,
activityDescription: item.activity?.activityDescription ?? null,
coverImage,
coverImagePresignedUrl,
foodOptions: (item.activity?.activityFoodTypes ?? []).map((foodType) => ({
id: foodType.id,
foodTypeXid: foodType.foodTypeXid,
foodTypeName: foodType.foodType.foodTypeName,
})),
trainerOptions: (item.activity?.ActivityTrainers ?? []).map((trainer) => ({
id: trainer.id,
baseAmount: trainer.baseAmount,
totalAmount: trainer.totalAmount,
taxes: mapCheckoutTaxes(trainer.ActivityTrainerTaxes ?? []),
})),
navigationOptions: (item.activity?.ActivityNavigationModes ?? []).map((navigationMode) => ({
id: navigationMode.id,
navigationModeName: navigationMode.navigationModeName,
navigationModesBasePrice: navigationMode.navigationModesBasePrice,
navigationModesTotalPrice: navigationMode.navigationModesTotalPrice,
taxes: mapCheckoutTaxes(navigationMode.ActivityNavigationModesTaxes ?? []),
})),
pickupOptions: (item.activity?.ActivityPickUpDetails ?? []).map((pickUpDetail) => ({
id: pickUpDetail.id,
isPickUp: pickUpDetail.isPickUp,
locationLat: pickUpDetail.locationLat,
locationLong: pickUpDetail.locationLong,
locationAddress: pickUpDetail.locationAddress,
transportBasePrice: pickUpDetail.transportBasePrice,
transportTotalPrice: pickUpDetail.transportTotalPrice,
taxes: mapCheckoutTaxes(pickUpDetail.activityPickUpTransportTaxes ?? []),
})),
pickupTransportModes: (item.activity?.activityPickUpTransports ?? []).map((transport) => ({
id: transport.id,
transportModeXid: transport.transportModeXid,
transportModeName: transport.transportMode.transportModeName,
transportModeIcon: transport.transportMode.transportModeIcon,
})),
equipmentOptions: (item.activity?.ActivityEquipments ?? []).map((equipment) => ({
id: equipment.id,
equipmentName: equipment.equipmentName,
equipmentBasePrice: equipment.equipmentBasePrice,
equipmentTotalPrice: equipment.equipmentTotalPrice,
taxes: mapCheckoutTaxes(equipment.ActivityEquipmentTaxes ?? []),
})),
},
memberSelections,
activityTotalAmount,
};
}),
);
const summary = {
activityTotal: sumCheckoutValues(activities.map((activity) => activity.activityTotalAmount)),
foodTotal: sumCheckoutValues(
activities.flatMap((activity) => activity.memberSelections.map((selection) => selection.pricing.food.totalAmount)),
),
trainerTotal: sumCheckoutValues(
activities.flatMap((activity) => activity.memberSelections.map((selection) => selection.pricing.trainer.totalAmount)),
),
navigationTotal: sumCheckoutValues(
activities.flatMap((activity) => activity.memberSelections.map((selection) => selection.pricing.navigation.totalAmount)),
),
pickupTotal: sumCheckoutValues(
activities.flatMap((activity) => activity.memberSelections.map((selection) => selection.pricing.pickup.totalAmount)),
),
equipmentTotal: sumCheckoutValues(
activities.flatMap((activity) => activity.memberSelections.map((selection) => selection.pricing.equipment.totalAmount)),
),
taxTotal: sumCheckoutValues(
activities.flatMap((activity) =>
activity.memberSelections.flatMap((selection) => [
selection.pricing.activity.taxAmount,
selection.pricing.food.taxAmount,
selection.pricing.trainer.taxAmount,
selection.pricing.navigation.taxAmount,
selection.pricing.pickup.taxAmount,
selection.pricing.equipment.taxAmount,
]),
),
),
};
const grandTotal =
summary.activityTotal +
summary.foodTotal +
summary.trainerTotal +
summary.navigationTotal +
summary.pickupTotal +
summary.equipmentTotal;
return {
itineraryHeaderXid: itinerary.id,
itineraryNo: itinerary.itineraryNo,
title: itinerary.title,
fromDate: itinerary.fromDate,
fromTime: itinerary.fromTime,
toDate: itinerary.toDate,
toTime: itinerary.toTime,
itineraryStatus: itinerary.itineraryStatus,
summary: {
...summary,
grandTotal,
},
activities,
};
}
async saveUserItinerary(
ownerXid: number,
payload: {
title?: string;
startDate: string;
endDate: string;
startTime: string;
endTime: string;
startLocationAddress: unknown;
startLocationLat: number;
startLocationLong: number;
endLocationAddress: unknown;
endLocationLat: number;
endLocationLong: number;
activities: Array<{
activityXid?: number;
venueXid?: number;
scheduleHeaderXid?: number;
modeOfTravel: string;
travelTimeBetweenPointsMins: number;
kmForNextPoint?: number;
startDate?: string;
endDate?: string;
occurenceDate?: string;
selectedStartTime?: string;
selectedEndTime?: string;
itineraryType?: string;
paxCount?: number;
totalAmount?: number;
locationLat?: number;
locationLong?: number;
locationAddress?: unknown;
}>;
},
) {
const itineraryStartDate = parseDateValue(payload.startDate);
const itineraryEndDate = parseDateValue(payload.endDate);
const itineraryStartDateTime = combineDateAndTime(
payload.startDate,
payload.startTime,
);
const itineraryEndDateTime = combineDateAndTime(
payload.endDate,
payload.endTime,
);
if (
Number.isNaN(itineraryStartDate.getTime()) ||
Number.isNaN(itineraryEndDate.getTime()) ||
!itineraryStartDateTime ||
!itineraryEndDateTime
) {
throw new ApiError(400, 'Invalid itinerary start or end date/time.');
}
if (itineraryStartDateTime >= itineraryEndDateTime) {
throw new ApiError(
400,
'Itinerary start date and time must be earlier than itinerary end date and time.',
);
}
if (!payload.activities.length) {
throw new ApiError(400, 'At least one itinerary activity is required.');
}
const itineraryNo = `ITN-${Date.now()}`;
const itineraryTitle = payload.title?.trim() || itineraryNo;
return this.prisma.$transaction(async (tx) => {
const itineraryHeader = await tx.itineraryHeader.create({
data: {
itineraryNo,
title: itineraryTitle,
ownerXid,
fromDate: itineraryStartDate,
fromTime: payload.startTime,
toDate: itineraryEndDate,
toTime: payload.endTime,
itineraryStatus: 'draft',
isActive: true,
},
});
const ownerMember = await tx.itineraryMembers.create({
data: {
itineraryHeaderXid: itineraryHeader.id,
memberXid: ownerXid,
memberRole: 'OWNER',
memberStatus: 'accepted',
invitedByXid: ownerXid,
isActive: true,
},
});
const startLocationDetails = await tx.itineraryStartStopDetails.create({
data: {
itineraryHeaderXid: itineraryHeader.id,
itineraryMemberXid: ownerMember.id,
dateValue: itineraryStartDate,
timeValue: payload.startTime,
isStartPoint: true,
locationLat: payload.startLocationLat,
locationLong: payload.startLocationLong,
locationAddress:
payload.startLocationAddress as Prisma.InputJsonValue,
travelMode: null,
kmForNextPoint: null,
timeForNextPointMins: null,
isActive: true,
},
});
const endLocationDetails = await tx.itineraryStartStopDetails.create({
data: {
itineraryHeaderXid: itineraryHeader.id,
itineraryMemberXid: ownerMember.id,
dateValue: itineraryEndDate,
timeValue: payload.endTime,
isStartPoint: false,
locationLat: payload.endLocationLat,
locationLong: payload.endLocationLong,
locationAddress: payload.endLocationAddress as Prisma.InputJsonValue,
travelMode: null,
kmForNextPoint: null,
timeForNextPointMins: null,
isActive: true,
},
});
const createdActivities = await Promise.all(
payload.activities.map(async (activityItem, activityIndex) => {
const itineraryType =
activityItem.itineraryType?.trim().toUpperCase() || 'ACTIVITY';
const isCustomItineraryType =
itineraryType === 'STAY' || itineraryType === 'FREE_TIME';
if (isCustomItineraryType) {
const customStartDate =
activityItem.startDate || activityItem.occurenceDate;
const customEndDate =
activityItem.endDate || activityItem.occurenceDate;
if (
!customStartDate ||
!customEndDate ||
!activityItem.selectedStartTime ||
!activityItem.selectedEndTime
) {
throw new ApiError(
400,
`${itineraryType} items must include startDate, endDate, selectedStartTime and selectedEndTime.`,
);
}
const customStartDateTime = combineDateAndTime(
customStartDate,
activityItem.selectedStartTime,
);
const customEndDateTime = combineDateAndTime(
customEndDate,
activityItem.selectedEndTime,
);
if (!customStartDateTime || !customEndDateTime) {
throw new ApiError(
400,
`Invalid date or time supplied for ${itineraryType}.`,
);
}
if (customStartDateTime >= customEndDateTime) {
throw new ApiError(
400,
`${itineraryType} selectedStartTime must be earlier than selectedEndTime.`,
);
}
if (
customStartDateTime < itineraryStartDateTime ||
customEndDateTime > itineraryEndDateTime
) {
throw new ApiError(
400,
`${itineraryType} must fall inside the itinerary date range.`,
);
}
const customActivityData: Prisma.ItineraryActivitiesCreateInput = {
itineraryHeader: {
connect: { id: itineraryHeader.id },
},
displayOrder: activityIndex,
itineraryType,
occurenceDate: startOfDay(customStartDateTime),
startTime: activityItem.selectedStartTime,
endTime: activityItem.selectedEndTime,
endDate: customEndDateTime,
locationLat: activityItem.locationLat ?? null,
locationLong: activityItem.locationLong ?? null,
locationAddress:
(activityItem.locationAddress as Prisma.InputJsonValue | null) ??
null,
travelMode: activityItem.modeOfTravel,
kmForNextPoint: activityItem.kmForNextPoint,
timeForNextPointMins: activityItem.travelTimeBetweenPointsMins,
paxCount: activityItem.paxCount ?? 1,
totalAmount: activityItem.totalAmount ?? null,
bookingStatus: 'pending',
isActive: true,
};
return tx.itineraryActivities.create({
data: customActivityData,
select: {
id: true,
displayOrder: true,
itineraryType: true,
activityXid: true,
scheduledHeaderXid: true,
venueXid: true,
occurenceDate: true,
startTime: true,
endTime: true,
endDate: true,
},
});
}
if (
!activityItem.activityXid ||
!activityItem.scheduleHeaderXid ||
!activityItem.venueXid
) {
throw new ApiError(
400,
'ACTIVITY items must include activityXid, scheduleHeaderXid and venueXid.',
);
}
const scheduleHeader = await tx.scheduleHeader.findFirst({
where: {
id: activityItem.scheduleHeaderXid,
activityXid: activityItem.activityXid,
activityVenueXid: activityItem.venueXid,
isActive: true,
deletedAt: null,
},
select: {
id: true,
startDate: true,
endDate: true,
activityVenueXid: true,
activity: {
select: {
id: true,
activityTitle: true,
activityDurationMins: true,
checkInLat: true,
checkInLong: true,
checkInAddress: true,
},
},
activityVenue: {
select: {
id: true,
venueName: true,
venueLabel: true,
},
},
ScheduleDetails: {
where: {
isActive: true,
deletedAt: null,
maxCapacity: { gt: 0 },
},
select: {
id: true,
occurenceDate: true,
weekDay: true,
dayOfMonth: true,
startTime: true,
endTime: true,
maxCapacity: true,
},
},
Cancellations: {
where: {
isActive: true,
deletedAt: null,
},
select: {
occurenceDate: true,
startTime: true,
endTime: true,
},
},
},
});
if (!scheduleHeader) {
throw new ApiError(
404,
`Schedule header ${activityItem.scheduleHeaderXid} not found for activity ${activityItem.activityXid}.`,
);
}
const scheduleRangeStart =
scheduleHeader.startDate > itineraryStartDate
? scheduleHeader.startDate
: itineraryStartDate;
const scheduleRangeEnd =
(scheduleHeader.endDate ?? itineraryEndDate) < itineraryEndDate
? (scheduleHeader.endDate ?? itineraryEndDate)
: itineraryEndDate;
if (scheduleRangeStart > scheduleRangeEnd) {
throw new ApiError(
400,
`Selected schedule for activity ${activityItem.activityXid} does not fall inside the itinerary date range.`,
);
}
const cancelledSlots = new Set(
scheduleHeader.Cancellations.map((cancellation) => {
if (!cancellation.occurenceDate) {
return null;
}
return `${formatDateKey(cancellation.occurenceDate)}|${cancellation.startTime}|${cancellation.endTime}`;
}).filter(Boolean) as string[],
);
const requestedOccurrenceDate = activityItem.occurenceDate
? formatDateKey(parseDateValue(activityItem.occurenceDate))
: null;
const requestedStartTime = activityItem.selectedStartTime
? normalizeTimeValue(activityItem.selectedStartTime)
: null;
const requestedEndTime = activityItem.selectedEndTime
? normalizeTimeValue(activityItem.selectedEndTime)
: null;
const expandedSlots = scheduleHeader.ScheduleDetails.flatMap((slot) =>
getUniqueDatesForScheduleDetail(
{
occurenceDate: slot.occurenceDate,
weekDay: slot.weekDay,
dayOfMonth: slot.dayOfMonth,
},
scheduleRangeStart,
scheduleRangeEnd,
).map((slotDate) => {
const slotStart = combineDateAndTime(slotDate, slot.startTime);
const slotEnd = slotStart
? combineDateAndTime(slotDate, slot.endTime) ??
(scheduleHeader.activity.activityDurationMins
? addMinutes(
slotStart,
scheduleHeader.activity.activityDurationMins,
)
: null)
: null;
const normalizedSlotDate = formatDateKey(slotDate);
const normalizedSlotStartTime = normalizeTimeValue(slot.startTime);
const normalizedSlotEndTime = normalizeTimeValue(slot.endTime);
const cancellationKey = `${normalizedSlotDate}|${slot.startTime}|${slot.endTime}`;
const mismatchReasons: string[] = [];
if (!slotStart) {
mismatchReasons.push('invalid_slot_start_time');
}
if (!slotEnd) {
mismatchReasons.push('invalid_slot_end_time');
}
if (slotStart && slotEnd) {
if (
slotStart < itineraryStartDateTime ||
slotEnd > itineraryEndDateTime
) {
mismatchReasons.push('outside_itinerary_window');
}
}
if (cancelledSlots.has(cancellationKey)) {
mismatchReasons.push('slot_cancelled');
}
if (
requestedOccurrenceDate &&
normalizedSlotDate !== requestedOccurrenceDate
) {
mismatchReasons.push('occurrence_date_mismatch');
}
if (
requestedStartTime &&
normalizedSlotStartTime !== requestedStartTime
) {
mismatchReasons.push('start_time_mismatch');
}
return {
slotId: slot.id,
occurenceDate: startOfDay(slotDate),
startTime: slot.startTime,
endTime: slot.endTime,
endDate: slotEnd,
debug: {
slotDate: normalizedSlotDate,
normalizedStartTime: normalizedSlotStartTime,
normalizedEndTime: normalizedSlotEndTime,
mismatchReasons,
},
};
}),
);
const candidateSlots = expandedSlots
.filter((slot) => slot.endDate && slot.debug.mismatchReasons.length === 0)
.map(({ debug, ...slot }) => slot);
if (!candidateSlots.length) {
if (
requestedOccurrenceDate ||
requestedStartTime ||
requestedEndTime
) {
const availableSlots = expandedSlots
.filter(
(slot) =>
slot.endDate &&
!slot.debug.mismatchReasons.includes('outside_itinerary_window') &&
!slot.debug.mismatchReasons.includes('slot_cancelled'),
)
.map((slot) => ({
slotId: slot.slotId,
occurenceDate: formatDateKey(slot.occurenceDate),
startTime: slot.startTime,
endTime: slot.endTime,
}));
throw new ApiError(
400,
`Requested slot does not exist for activity ${activityItem.activityXid}. Please choose a valid occurenceDate/startTime/endTime combination.`,
[],
true,
undefined,
undefined,
{
activityXid: activityItem.activityXid,
venueXid: activityItem.venueXid,
scheduleHeaderXid: activityItem.scheduleHeaderXid,
requestedSlot: {
occurenceDate: activityItem.occurenceDate ?? null,
selectedStartTime: activityItem.selectedStartTime ?? null,
selectedEndTime: activityItem.selectedEndTime ?? null,
},
availableSlots,
},
);
}
throw new ApiError(
400,
`No valid slot found for activity ${activityItem.activityXid} in the selected itinerary range.`,
);
}
if (!activityItem.occurenceDate && candidateSlots.length > 1) {
throw new ApiError(
400,
`Multiple slots are available for activity ${activityItem.activityXid}. Please send occurenceDate and selectedStartTime for the chosen slot.`,
);
}
const selectedSlot = candidateSlots[0]!;
const activityData: Prisma.ItineraryActivitiesCreateInput = {
itineraryHeader: {
connect: { id: itineraryHeader.id },
},
displayOrder: activityIndex,
itineraryType,
activity: {
connect: { id: activityItem.activityXid },
},
scheduledHeader: {
connect: { id: activityItem.scheduleHeaderXid },
},
venue: {
connect: { id: activityItem.venueXid },
},
occurenceDate: selectedSlot.occurenceDate,
startTime: selectedSlot.startTime,
endTime: selectedSlot.endTime,
endDate: selectedSlot.endDate,
locationLat:
activityItem.locationLat ??
scheduleHeader.activity.checkInLat ??
null,
locationLong:
activityItem.locationLong ??
scheduleHeader.activity.checkInLong ??
null,
locationAddress:
(activityItem.locationAddress as Prisma.InputJsonValue | undefined) ??
((scheduleHeader.activity.checkInAddress as Prisma.InputJsonValue | null) ??
undefined),
travelMode: activityItem.modeOfTravel,
kmForNextPoint: activityItem.kmForNextPoint,
timeForNextPointMins: activityItem.travelTimeBetweenPointsMins,
paxCount: activityItem.paxCount ?? 1,
totalAmount: activityItem.totalAmount ?? null,
bookingStatus: 'pending',
isActive: true,
};
return tx.itineraryActivities.create({
data: activityData,
select: {
id: true,
displayOrder: true,
activityXid: true,
scheduledHeaderXid: true,
venueXid: true,
occurenceDate: true,
startTime: true,
endTime: true,
endDate: true,
},
});
}),
);
return {
itineraryHeaderXid: itineraryHeader.id,
itineraryNo: itineraryHeader.itineraryNo,
itineraryTitle: itineraryHeader.title,
title: itineraryHeader.title,
itineraryStatus: itineraryHeader.itineraryStatus,
ownerMemberXid: ownerMember.id,
membersCount: 1,
activitiesCount: createdActivities.length,
members: [
{
id: ownerMember.id,
memberXid: ownerMember.memberXid,
memberRole: ownerMember.memberRole,
memberStatus: ownerMember.memberStatus,
},
],
startLocationDetails,
endLocationDetails,
activities: createdActivities,
};
});
}
async getAllUserSavedItineraries(
userXid: number,
itineraryHeaderXid?: number,
startDate?: Date,
) {
const itineraries = await this.prisma.itineraryHeader.findMany({
where: {
...(itineraryHeaderXid ? { id: itineraryHeaderXid } : {}),
...(startDate
? {
fromDate: {
gte: startDate,
},
}
: {}),
isActive: true,
deletedAt: null,
OR: [
{ ownerXid: userXid },
{
ItineraryMembers: {
some: {
memberXid: userXid,
isActive: true,
deletedAt: null,
},
},
},
],
},
orderBy: {
createdAt: 'desc',
},
select: {
id: true,
itineraryNo: true,
title: true,
ownerXid: true,
fromDate: true,
fromTime: true,
toDate: true,
toTime: true,
itineraryStatus: true,
createdAt: true,
owner: {
select: {
id: true,
firstName: true,
lastName: true,
profileImage: true,
},
},
ItineraryMembers: {
where: {
isActive: true,
deletedAt: null,
},
orderBy: {
createdAt: 'asc',
},
select: {
id: true,
memberXid: true,
memberRole: true,
memberStatus: true,
invitedByXid: true,
member: {
select: {
id: true,
firstName: true,
lastName: true,
profileImage: true,
},
},
},
},
ItineraryStartStopDetails: {
where: {
isActive: true,
deletedAt: null,
},
orderBy: {
createdAt: 'asc',
},
select: {
id: true,
itineraryMemberXid: true,
dateValue: true,
timeValue: true,
isStartPoint: true,
locationLat: true,
locationLong: true,
locationAddress: true,
travelMode: true,
kmForNextPoint: true,
timeForNextPointMins: true,
createdAt: true,
},
},
ItineraryActivities: {
where: {
isActive: true,
deletedAt: null,
},
orderBy: [
{ displayOrder: 'asc' },
{ createdAt: 'asc' },
],
select: {
id: true,
displayOrder: true,
itineraryType: true,
activityXid: true,
scheduledHeaderXid: true,
occurenceDate: true,
startTime: true,
endTime: true,
endDate: true,
venueXid: true,
locationLat: true,
locationLong: true,
locationAddress: true,
travelMode: true,
kmForNextPoint: true,
timeForNextPointMins: true,
paxCount: true,
totalAmount: true,
bookingStatus: true,
itineraryActivitySelections: {
where: {
isActive: true,
deletedAt: null,
itineraryMember: {
memberXid: userXid,
isActive: true,
deletedAt: null,
},
},
take: 1,
select: {
id: true,
itineraryMemberXid: true,
isFoodOpted: true,
isTrainerOpted: true,
isInActivityNavigationOpted: true,
activityNavigationModeXid: true,
selectedFoodTypes: {
where: {
isActive: true,
deletedAt: null,
},
select: {
id: true,
activityFoodTypeXid: true,
activityFoodType: {
select: {
id: true,
foodTypeXid: true,
foodType: {
select: {
id: true,
foodTypeName: true,
},
},
},
},
},
},
activityNavigationMode: {
select: {
id: true,
navigationModeName: true,
isInActivityChargeable: true,
navigationModesBasePrice: true,
navigationModesTotalPrice: true,
},
},
selectedEquipments: {
where: {
isActive: true,
deletedAt: null,
},
select: {
id: true,
activityEquipmentXid: true,
activityEquipment: {
select: {
id: true,
equipmentName: true,
isEquipmentChargeable: true,
equipmentBasePrice: true,
equipmentTotalPrice: true,
},
},
},
},
},
},
activity: {
select: {
id: true,
activityTitle: true,
activityDescription: true,
activityDurationMins: true,
checkInAddress: true,
checkInLat: true,
checkInLong: true,
checkInCity: {
select: {
id: true,
cityName: true,
},
},
checkOutAddress: true,
checkOutLat: true,
checkOutLong: true,
checkOutCity: {
select: {
id: true,
cityName: true,
},
},
foodAvailable: true,
foodIsChargeable: true,
trainerAvailable: true,
trainerIsChargeable: true,
inActivityAvailable: true,
inActivityIsChargeable: true,
pickUpDropAvailable: true,
pickUpDropIsChargeable: true,
equipmentAvailable: true,
equipmentIsChargeable: true,
activityFoodTypes: {
where: {
isActive: true,
deletedAt: null,
},
select: {
id: true,
foodTypeXid: true,
foodType: {
select: {
id: true,
foodTypeName: true,
},
},
},
},
ActivityFoodCost: {
where: {
isActive: true,
deletedAt: null,
},
select: {
id: true,
baseAmount: true,
totalAmount: true,
},
},
ActivityTrainers: {
where: {
isActive: true,
deletedAt: null,
},
select: {
id: true,
baseAmount: true,
totalAmount: true,
},
},
ActivityNavigationModes: {
where: {
isActive: true,
deletedAt: null,
},
select: {
id: true,
navigationModeName: true,
isInActivityChargeable: true,
navigationModesBasePrice: true,
navigationModesTotalPrice: true,
},
},
ActivityEquipments: {
where: {
isActive: true,
deletedAt: null,
},
select: {
id: true,
equipmentName: true,
isEquipmentChargeable: true,
equipmentBasePrice: true,
equipmentTotalPrice: true,
},
},
ActivityPickUpDetails: {
where: {
isActive: true,
deletedAt: null,
},
select: {
id: true,
isPickUp: true,
locationLat: true,
locationLong: true,
locationAddress: true,
transportBasePrice: true,
transportTotalPrice: true,
},
},
activityPickUpTransports: {
where: {
isActive: true,
deletedAt: null,
},
select: {
id: true,
transportModeXid: true,
transportMode: {
select: {
id: true,
transportModeName: true,
transportModeIcon: true,
},
},
},
},
ActivityVenues: {
where: {
isActive: true,
deletedAt: null,
},
select: {
id: true,
venueName: true,
venueLabel: true,
ActivityPrices: {
where: {
isActive: true,
deletedAt: null,
},
orderBy: {
createdAt: 'asc',
},
select: {
id: true,
noOfSession: true,
isPackage: true,
sessionValidity: true,
sessionValidityFrequency: true,
basePrice: true,
sellPrice: true,
},
},
},
},
ActivitiesMedia: {
where: {
isActive: true,
deletedAt: null,
},
orderBy: {
displayOrder: 'asc',
},
select: {
id: true,
mediaFileName: true,
mediaType: true,
isCoverImage: true,
displayOrder: true,
},
},
},
},
venue: {
select: {
id: true,
venueName: true,
venueLabel: true,
venueCapacity: true,
availableSeats: true,
},
},
scheduledHeader: {
select: {
id: true,
scheduleType: true,
startDate: true,
endDate: true,
},
},
},
},
},
});
const formattedItineraries = await Promise.all(
itineraries.map(async (itinerary) => {
const ownerFullName = `${itinerary.owner.firstName ?? ''} ${itinerary.owner.lastName ?? ''
}`.trim();
const members = await Promise.all(
itinerary.ItineraryMembers.map(async (member) => ({
id: member.id,
memberXid: member.memberXid,
memberRole: member.memberRole,
memberStatus: member.memberStatus,
invitedByXid: member.invitedByXid,
fullName: `${member.member.firstName ?? ''} ${member.member.lastName ?? ''
}`.trim(),
firstName: member.member.firstName,
lastName: member.member.lastName,
profileImage: member.member.profileImage,
profileImagePresignedUrl: await attachPresignedUrl(
member.member.profileImage,
),
})),
);
const activities = await Promise.all(
itinerary.ItineraryActivities.map(async (item) => {
const coverImage = item.activity
? item.activity.ActivitiesMedia.find((media) => media.isCoverImage) ??
item.activity.ActivitiesMedia[0] ??
null
: null;
const userSelection = item.itineraryActivitySelections[0] ?? null;
const selectedVenue = item.activity?.ActivityVenues.find(
(venue) => venue.id === item.venueXid,
);
const venueForPricing =
selectedVenue ?? item.activity?.ActivityVenues[0] ?? null;
const venuePrices = venueForPricing?.ActivityPrices ?? [];
const activityPriceDetails = venuePrices.reduce(
(lowest, current) =>
!lowest || current.sellPrice < lowest.sellPrice ? current : lowest,
null as (typeof venuePrices)[number] | null,
);
return {
id: item.id,
displayOrder: item.displayOrder,
itineraryType: item.itineraryType,
activityXid: item.activityXid,
scheduledHeaderXid: item.scheduledHeaderXid,
occurenceDate: item.occurenceDate,
startTime: item.startTime,
endTime: item.endTime,
endDate: item.endDate,
venueXid: item.venueXid,
locationLat: item.locationLat,
locationLong: item.locationLong,
locationAddress: item.locationAddress,
travelMode: item.travelMode,
kmForNextPoint: item.kmForNextPoint,
timeForNextPointMins: item.timeForNextPointMins,
paxCount: item.paxCount,
totalAmount: item.totalAmount,
bookingStatus: item.bookingStatus,
activity: item.activity
? {
id: item.activity.id,
activityTitle: item.activity.activityTitle,
activityDescription: item.activity.activityDescription,
activityDurationMins: item.activity.activityDurationMins,
activityPrice: activityPriceDetails?.sellPrice ?? null,
activityPriceDetails,
checkInAddress: item.activity.checkInAddress,
checkInLat: item.activity.checkInLat,
checkInLong: item.activity.checkInLong,
checkInCityName: item.activity.checkInCity?.cityName ?? null,
checkOutAddress: item.activity.checkOutAddress,
checkOutLat: item.activity.checkOutLat,
checkOutLong: item.activity.checkOutLong,
checkOutCityName:
item.activity.checkOutCity?.cityName ?? null,
pickUpLocation: item.activity.pickUpDropAvailable
? {
address: item.activity.checkInAddress,
cityName: item.activity.checkInCity?.cityName ?? null,
lat: item.activity.checkInLat,
long: item.activity.checkInLong,
}
: null,
dropLocation: item.activity.pickUpDropAvailable
? {
address: item.activity.checkOutAddress,
cityName: item.activity.checkOutCity?.cityName ?? null,
lat: item.activity.checkOutLat,
long: item.activity.checkOutLong,
}
: null,
coverImage: coverImage?.mediaFileName ?? null,
coverImagePresignedUrl: await attachPresignedUrl(
coverImage?.mediaFileName,
),
media: await attachMediaWithPresignedUrl(
item.activity.ActivitiesMedia,
),
foodDetails: {
foodAvailable: item.activity.foodAvailable,
foodIsChargeable: item.activity.foodIsChargeable,
foodTypes: item.activity.activityFoodTypes.map((foodType) => ({
id: foodType.id,
foodTypeXid: foodType.foodTypeXid,
foodType: foodType.foodType,
})),
foodCost: item.activity.ActivityFoodCost.map((foodCost) => ({
id: foodCost.id,
baseAmount: foodCost.baseAmount,
totalAmount: foodCost.totalAmount,
})),
},
trainerDetails: {
trainerAvailable: item.activity.trainerAvailable,
trainerIsChargeable: item.activity.trainerIsChargeable,
trainerCost: item.activity.ActivityTrainers.map((trainer) => ({
id: trainer.id,
baseAmount: trainer.baseAmount,
totalAmount: trainer.totalAmount,
})),
},
navigationDetails: {
inActivityAvailable: item.activity.inActivityAvailable,
inActivityIsChargeable: item.activity.inActivityIsChargeable,
navigationModes: item.activity.ActivityNavigationModes.map(
(navigationMode) => ({
id: navigationMode.id,
navigationModeName: navigationMode.navigationModeName,
isInActivityChargeable:
navigationMode.isInActivityChargeable,
navigationModesBasePrice:
navigationMode.navigationModesBasePrice,
navigationModesTotalPrice:
navigationMode.navigationModesTotalPrice,
}),
),
},
equipmentDetails: {
equipmentAvailable: item.activity.equipmentAvailable,
equipmentIsChargeable: item.activity.equipmentIsChargeable,
equipments: item.activity.ActivityEquipments.map((equipment) => ({
id: equipment.id,
equipmentName: equipment.equipmentName,
isEquipmentChargeable: equipment.isEquipmentChargeable,
equipmentBasePrice: equipment.equipmentBasePrice,
equipmentTotalPrice: equipment.equipmentTotalPrice,
})),
},
pickUpDetails: {
pickUpDropAvailable: item.activity.pickUpDropAvailable,
pickUpDropIsChargeable:
item.activity.pickUpDropIsChargeable,
details: item.activity.ActivityPickUpDetails.map(
(pickUpDetail) => ({
id: pickUpDetail.id,
isPickUp: pickUpDetail.isPickUp,
locationLat: pickUpDetail.locationLat,
locationLong: pickUpDetail.locationLong,
locationAddress: pickUpDetail.locationAddress,
cityName: pickUpDetail.isPickUp
? item.activity.checkInCity?.cityName ?? null
: item.activity.checkOutCity?.cityName ?? null,
transportBasePrice: pickUpDetail.transportBasePrice,
transportTotalPrice: pickUpDetail.transportTotalPrice,
}),
),
transportModes: await Promise.all(
item.activity.activityPickUpTransports.map(
async (transport) => ({
id: transport.id,
transportModeXid: transport.transportModeXid,
transportMode: {
id: transport.transportMode.id,
transportModeName:
transport.transportMode.transportModeName,
transportModeIcon:
transport.transportMode.transportModeIcon,
transportModeIconPresignedUrl:
await attachPresignedUrl(
transport.transportMode.transportModeIcon,
),
},
}),
),
),
},
}
: null,
userSelections: userSelection
? {
id: userSelection.id,
itineraryMemberXid: userSelection.itineraryMemberXid,
isFoodOpted: userSelection.isFoodOpted,
selectedFoodTypeIds: userSelection.selectedFoodTypes.map(
(foodType) => foodType.activityFoodTypeXid,
),
selectedFoodTypes: userSelection.selectedFoodTypes.map(
(foodType) => ({
id: foodType.id,
activityFoodTypeXid: foodType.activityFoodTypeXid,
activityFoodType: foodType.activityFoodType,
}),
),
isTrainerOpted: userSelection.isTrainerOpted,
isInActivityNavigationOpted:
userSelection.isInActivityNavigationOpted,
selectedNavigationModeXid:
userSelection.activityNavigationModeXid,
selectedNavigationMode:
userSelection.activityNavigationMode ?? null,
selectedEquipmentIds: userSelection.selectedEquipments.map(
(equipment) => equipment.activityEquipmentXid,
),
selectedEquipments: userSelection.selectedEquipments.map(
(equipment) => ({
id: equipment.id,
activityEquipmentXid: equipment.activityEquipmentXid,
activityEquipment: equipment.activityEquipment,
}),
),
}
: null,
venue: item.venue,
scheduleHeader: item.scheduledHeader,
};
}),
);
const startStopDetails = itinerary.ItineraryStartStopDetails.map(
(detail) => ({
id: detail.id,
itineraryMemberXid: detail.itineraryMemberXid,
dateValue: detail.dateValue,
timeValue: detail.timeValue,
isStartPoint: detail.isStartPoint,
locationLat: detail.locationLat,
locationLong: detail.locationLong,
locationAddress: detail.locationAddress,
travelMode: detail.travelMode,
kmForNextPoint: detail.kmForNextPoint,
timeForNextPointMins: detail.timeForNextPointMins,
createdAt: detail.createdAt,
}),
);
return {
itineraryHeaderXid: itinerary.id,
itineraryNo: itinerary.itineraryNo,
title: itinerary.title,
ownerXid: itinerary.ownerXid,
owner: {
id: itinerary.owner.id,
fullName: ownerFullName,
firstName: itinerary.owner.firstName,
lastName: itinerary.owner.lastName,
profileImage: itinerary.owner.profileImage,
profileImagePresignedUrl: await attachPresignedUrl(
itinerary.owner.profileImage,
),
},
fromDate: itinerary.fromDate,
fromTime: itinerary.fromTime,
toDate: itinerary.toDate,
toTime: itinerary.toTime,
itineraryStatus: itinerary.itineraryStatus,
createdAt: itinerary.createdAt,
membersCount: members.length,
activitiesCount: activities.length,
members,
startStopDetails,
activities,
};
}),
);
return {
count: formattedItineraries.length,
itineraries: formattedItineraries,
};
}
async cancelUserItinerary(
userXid: number,
itineraryHeaderXid: number,
cancellationReason: string,
) {
return this.prisma.$transaction(async (tx) => {
const itinerary = await tx.itineraryHeader.findFirst({
where: {
id: itineraryHeaderXid,
ownerXid: userXid,
isActive: true,
deletedAt: null,
},
select: {
id: true,
itineraryNo: true,
title: true,
itineraryStatus: true,
},
});
if (!itinerary) {
throw new ApiError(
404,
'Active itinerary not found for the logged-in user.',
);
}
const itineraryActivityIds = (
await tx.itineraryActivities.findMany({
where: {
itineraryHeaderXid,
isActive: true,
deletedAt: null,
},
select: {
id: true,
},
})
).map((item) => item.id);
const itineraryDetailIds = itineraryActivityIds.length
? (
await tx.itineraryDetails.findMany({
where: {
itineraryActivityXid: {
in: itineraryActivityIds,
},
isActive: true,
deletedAt: null,
},
select: {
id: true,
},
})
).map((item) => item.id)
: [];
const itinerarySelectionIds = itineraryActivityIds.length
? (
await tx.itineraryActivitySelection.findMany({
where: {
itineraryActivityXid: {
in: itineraryActivityIds,
},
isActive: true,
deletedAt: null,
},
select: {
id: true,
},
})
).map((item) => item.id)
: [];
if (itineraryDetailIds.length) {
await tx.itineraryDetailTaxes.updateMany({
where: {
itineraryDetailXid: {
in: itineraryDetailIds,
},
isActive: true,
deletedAt: null,
},
data: {
isActive: false,
},
});
await tx.itineraryDetails.updateMany({
where: {
id: {
in: itineraryDetailIds,
},
isActive: true,
deletedAt: null,
},
data: {
isActive: false,
itineraryStatus: 'cancelled',
},
});
}
if (itinerarySelectionIds.length) {
await tx.itineraryActivitySelectionFoodType.updateMany({
where: {
itineraryActivitySelectionXid: {
in: itinerarySelectionIds,
},
isActive: true,
deletedAt: null,
},
data: {
isActive: false,
},
});
await tx.itineraryActivitySelectionEquipment.updateMany({
where: {
itineraryActivitySelectionXid: {
in: itinerarySelectionIds,
},
isActive: true,
deletedAt: null,
},
data: {
isActive: false,
},
});
await tx.itineraryActivitySelection.updateMany({
where: {
id: {
in: itinerarySelectionIds,
},
isActive: true,
deletedAt: null,
},
data: {
isActive: false,
},
});
}
await tx.itineraryActivities.updateMany({
where: {
itineraryHeaderXid,
isActive: true,
deletedAt: null,
},
data: {
isActive: false,
bookingStatus: 'cancelled',
},
});
await tx.itineraryStartStopDetails.updateMany({
where: {
itineraryHeaderXid,
isActive: true,
deletedAt: null,
},
data: {
isActive: false,
},
});
await tx.itineraryMembers.updateMany({
where: {
itineraryHeaderXid,
isActive: true,
deletedAt: null,
},
data: {
isActive: false,
memberStatus: 'cancelled',
},
});
await tx.$executeRaw`
UPDATE "itn"."itinerary_header"
SET
"is_active" = false,
"itinerary_status" = 'cancelled',
"cancellation_reason" = ${cancellationReason},
"updated_at" = NOW()
WHERE "id" = ${itineraryHeaderXid}
`;
return {
itineraryHeaderXid: itinerary.id,
itineraryNo: itinerary.itineraryNo,
title: itinerary.title,
itineraryStatus: 'cancelled',
cancellationReason,
isActive: false,
};
});
}
async bookItineraryAfterPayment(
tx: Prisma.TransactionClient,
userXid: number,
itineraryHeaderXid: number,
) {
const itinerary = await tx.itineraryHeader.findFirst({
where: {
id: itineraryHeaderXid,
ownerXid: userXid,
isActive: true,
deletedAt: null,
},
select: {
id: true,
itineraryNo: true,
title: true,
ownerXid: true,
fromDate: true,
toDate: true,
ItineraryMembers: {
where: {
memberXid: userXid,
isActive: true,
deletedAt: null,
},
select: {
id: true,
memberXid: true,
memberRole: true,
memberStatus: true,
},
},
ItineraryActivities: {
where: {
isActive: true,
deletedAt: null,
},
orderBy: [
{ displayOrder: 'asc' },
{ createdAt: 'asc' },
],
select: {
id: true,
displayOrder: true,
itineraryType: true,
activityXid: true,
scheduledHeaderXid: true,
occurenceDate: true,
startTime: true,
endTime: true,
paxCount: true,
totalAmount: true,
bookingStatus: true,
activity: {
select: {
id: true,
activityTitle: true,
ActivityFoodCost: {
where: { isActive: true, deletedAt: null },
select: {
id: true,
foodTypesId: true,
baseAmount: true,
totalAmount: true,
ActivityFoodTaxes: {
where: { isActive: true, deletedAt: null },
select: {
id: true,
taxPer: true,
taxAmount: true,
taxes: {
select: {
id: true,
taxName: true,
taxPer: true,
},
},
},
},
},
},
ActivityTrainers: {
where: { isActive: true, deletedAt: null },
select: {
id: true,
baseAmount: true,
totalAmount: true,
ActivityTrainerTaxes: {
where: { isActive: true, deletedAt: null },
select: {
id: true,
taxPer: true,
taxAmount: true,
taxes: {
select: {
id: true,
taxName: true,
taxPer: true,
},
},
},
},
},
},
ActivityNavigationModes: {
where: { isActive: true, deletedAt: null },
select: {
id: true,
navigationModeName: true,
navigationModesBasePrice: true,
navigationModesTotalPrice: true,
ActivityNavigationModesTaxes: {
where: { isActive: true, deletedAt: null },
select: {
id: true,
taxPer: true,
taxAmount: true,
taxes: {
select: {
id: true,
taxName: true,
taxPer: true,
},
},
},
},
},
},
ActivityPickUpDetails: {
where: { isActive: true, deletedAt: null },
select: {
id: true,
isPickUp: true,
locationLat: true,
locationLong: true,
locationAddress: true,
transportBasePrice: true,
transportTotalPrice: true,
activityPickUpTransportTaxes: {
where: { isActive: true, deletedAt: null },
select: {
id: true,
taxPer: true,
taxAmount: true,
taxes: {
select: {
id: true,
taxName: true,
taxPer: true,
},
},
},
},
},
},
activityPickUpTransports: {
where: { isActive: true, deletedAt: null },
select: {
id: true,
transportModeXid: true,
transportMode: {
select: {
id: true,
transportModeName: true,
transportModeIcon: true,
},
},
},
},
ActivityEquipments: {
where: { isActive: true, deletedAt: null },
select: {
id: true,
equipmentName: true,
equipmentBasePrice: true,
equipmentTotalPrice: true,
ActivityEquipmentTaxes: {
where: { isActive: true, deletedAt: null },
select: {
id: true,
taxPer: true,
taxAmount: true,
taxes: {
select: {
id: true,
taxName: true,
taxPer: true,
},
},
},
},
},
},
},
},
venue: {
select: {
id: true,
venueName: true,
venueLabel: true,
},
},
scheduledHeader: {
select: {
id: true,
startDate: true,
endDate: true,
activity: {
select: {
activityDurationMins: true,
},
},
ScheduleDetails: {
where: {
isActive: true,
deletedAt: null,
},
select: {
id: true,
occurenceDate: true,
weekDay: true,
dayOfMonth: true,
startTime: true,
endTime: true,
maxCapacity: true,
},
},
Cancellations: {
where: {
isActive: true,
deletedAt: null,
},
select: {
occurenceDate: true,
startTime: true,
endTime: true,
},
},
},
},
itineraryActivitySelections: {
where: { isActive: true, deletedAt: null },
select: {
id: true,
itineraryMemberXid: true,
isFoodOpted: true,
isTrainerOpted: true,
isInActivityNavigationOpted: true,
activityNavigationMode: {
select: {
id: true,
navigationModeName: true,
navigationModesBasePrice: true,
navigationModesTotalPrice: true,
ActivityNavigationModesTaxes: {
where: { isActive: true, deletedAt: null },
select: {
id: true,
taxPer: true,
taxAmount: true,
taxes: {
select: {
id: true,
taxName: true,
taxPer: true,
},
},
},
},
},
},
selectedFoodTypes: {
where: { isActive: true, deletedAt: null },
select: {
id: true,
activityFoodTypeXid: true,
activityFoodType: {
select: {
id: true,
foodTypeXid: true,
foodType: {
select: {
id: true,
foodTypeName: true,
},
},
},
},
},
},
selectedEquipments: {
where: { isActive: true, deletedAt: null },
select: {
id: true,
activityEquipmentXid: true,
activityEquipment: {
select: {
id: true,
equipmentName: true,
equipmentBasePrice: true,
equipmentTotalPrice: true,
ActivityEquipmentTaxes: {
where: { isActive: true, deletedAt: null },
select: {
id: true,
taxPer: true,
taxAmount: true,
taxes: {
select: {
id: true,
taxName: true,
taxPer: true,
},
},
},
},
},
},
},
},
},
},
},
},
},
});
if (!itinerary) {
throw new ApiError(404, 'Itinerary not found for booking.');
}
const itineraryMember = itinerary.ItineraryMembers[0] ?? null;
if (!itineraryMember) {
throw new ApiError(404, 'Itinerary member record not found for booking.');
}
const bookedActivities = await Promise.all(
itinerary.ItineraryActivities.map(async (activity) => {
const activitySelection =
activity.itineraryActivitySelections?.find(
(selection) => selection.itineraryMemberXid === itineraryMember.id,
) ?? null;
const selectedFoodTypes = activitySelection?.selectedFoodTypes ?? [];
const selectedEquipments = activitySelection?.selectedEquipments ?? [];
const selectedNavigationMode =
activitySelection?.activityNavigationMode ?? null;
const foodDetails = selectedFoodTypes.map((selectedFoodType) => {
const matchedCost =
activity.activity?.ActivityFoodCost.find(
(cost) => cost.foodTypesId === selectedFoodType.activityFoodType.foodTypeXid,
) ?? activity.activity?.ActivityFoodCost?.[0] ?? null;
return matchedCost
? {
id: selectedFoodType.id,
activityFoodTypeXid: selectedFoodType.activityFoodTypeXid,
foodTypeXid: selectedFoodType.activityFoodType.foodTypeXid,
foodTypeName: selectedFoodType.activityFoodType.foodType.foodTypeName,
baseAmount: Number(matchedCost.baseAmount) || 0,
totalAmount: Number(matchedCost.totalAmount) || 0,
taxes: mapCheckoutTaxes(matchedCost.ActivityFoodTaxes ?? []),
}
: {
id: selectedFoodType.id,
activityFoodTypeXid: selectedFoodType.activityFoodTypeXid,
foodTypeXid: selectedFoodType.activityFoodType.foodTypeXid,
foodTypeName: selectedFoodType.activityFoodType.foodType.foodTypeName,
baseAmount: 0,
totalAmount: 0,
taxes: [],
};
});
const equipmentDetails = selectedEquipments.map((selectedEquipment) => ({
id: selectedEquipment.id,
activityEquipmentXid: selectedEquipment.activityEquipmentXid,
equipmentName: selectedEquipment.activityEquipment.equipmentName,
baseAmount: Number(selectedEquipment.activityEquipment.equipmentBasePrice) || 0,
totalAmount: Number(selectedEquipment.activityEquipment.equipmentTotalPrice) || 0,
taxes: mapCheckoutTaxes(
selectedEquipment.activityEquipment.ActivityEquipmentTaxes ?? [],
),
}));
const navigationDetails = selectedNavigationMode
? {
id: selectedNavigationMode.id,
navigationModeName: selectedNavigationMode.navigationModeName,
baseAmount: Number(selectedNavigationMode.navigationModesBasePrice) || 0,
totalAmount: Number(selectedNavigationMode.navigationModesTotalPrice) || 0,
taxes: mapCheckoutTaxes(
selectedNavigationMode.ActivityNavigationModesTaxes ?? [],
),
}
: null;
const pickupDetails = (activity.activity?.ActivityPickUpDetails ?? []).map(
(pickUpDetail) => ({
id: pickUpDetail.id,
isPickUp: pickUpDetail.isPickUp,
locationLat: pickUpDetail.locationLat,
locationLong: pickUpDetail.locationLong,
locationAddress: pickUpDetail.locationAddress,
baseAmount: Number(pickUpDetail.transportBasePrice) || 0,
totalAmount: Number(pickUpDetail.transportTotalPrice) || 0,
taxes: mapCheckoutTaxes(
pickUpDetail.activityPickUpTransportTaxes ?? [],
),
}),
);
if (
activity.itineraryType !== 'ACTIVITY' ||
!activity.activityXid ||
!activity.scheduledHeaderXid ||
!activity.scheduledHeader
) {
return {
itineraryActivityXid: activity.id,
activityName: activity.activity?.activityTitle ?? null,
activityVenue: activity.venue?.venueName ?? null,
activityTime: {
occurenceDate: activity.occurenceDate,
startTime: activity.startTime,
endTime: activity.endTime,
},
foodDetails,
equipmentDetails,
navigationDetails,
pickupDetails,
skipped: true,
reason: 'not_bookable_activity',
};
}
const existingDetail = await tx.itineraryDetails.findFirst({
where: {
itineraryActivityXid: activity.id,
itineraryMemberXid: itineraryMember.id,
itineraryKind: 'ACTIVITY',
isActive: true,
deletedAt: null,
},
select: {
id: true,
offlineCode: true,
},
});
if (existingDetail) {
return {
itineraryActivityXid: activity.id,
activityName: activity.activity?.activityTitle ?? null,
activityVenue: activity.venue?.venueName ?? null,
activityTime: {
occurenceDate: activity.occurenceDate,
startTime: activity.startTime,
endTime: activity.endTime,
},
foodDetails,
equipmentDetails,
navigationDetails,
pickupDetails,
skipped: false,
alreadyBooked: true,
checkInCode: existingDetail.offlineCode,
remainingCapacity: null,
};
}
const scheduleRangeStart =
activity.scheduledHeader.startDate > itinerary.fromDate
? activity.scheduledHeader.startDate
: itinerary.fromDate;
const scheduleRangeEnd =
(activity.scheduledHeader.endDate ?? itinerary.toDate) < itinerary.toDate
? (activity.scheduledHeader.endDate ?? itinerary.toDate)
: itinerary.toDate;
const cancelledSlots = new Set(
activity.scheduledHeader.Cancellations.map((cancellation) => {
if (!cancellation.occurenceDate) {
return null;
}
return `${formatDateKey(cancellation.occurenceDate)}|${cancellation.startTime}|${cancellation.endTime}`;
}).filter(Boolean) as string[],
);
const requestedOccurrenceDate = formatDateKey(activity.occurenceDate);
const requestedStartTime = normalizeTimeValue(activity.startTime);
const requestedEndTime = normalizeTimeValue(activity.endTime);
const expandedSlots = activity.scheduledHeader.ScheduleDetails.flatMap((slot) =>
getUniqueDatesForScheduleDetail(
{
occurenceDate: slot.occurenceDate,
weekDay: slot.weekDay,
dayOfMonth: slot.dayOfMonth,
},
scheduleRangeStart,
scheduleRangeEnd,
).map((slotDate) => {
const slotStart = combineDateAndTime(slotDate, slot.startTime);
const slotEnd = slotStart
? combineDateAndTime(slotDate, slot.endTime) ??
(activity.scheduledHeader?.activity?.activityDurationMins
? addMinutes(
slotStart,
activity.scheduledHeader.activity.activityDurationMins,
)
: null)
: null;
const normalizedSlotDate = formatDateKey(slotDate);
const normalizedSlotStartTime = normalizeTimeValue(slot.startTime);
const normalizedSlotEndTime = normalizeTimeValue(slot.endTime);
const cancellationKey = `${normalizedSlotDate}|${slot.startTime}|${slot.endTime}`;
const mismatchReasons: string[] = [];
if (!slotStart || !slotEnd) {
mismatchReasons.push('invalid_slot_time');
}
if (cancelledSlots.has(cancellationKey)) {
mismatchReasons.push('slot_cancelled');
}
if (normalizedSlotDate !== requestedOccurrenceDate) {
mismatchReasons.push('occurrence_date_mismatch');
}
if (requestedStartTime && normalizedSlotStartTime !== requestedStartTime) {
mismatchReasons.push('start_time_mismatch');
}
if (requestedEndTime && normalizedSlotEndTime !== requestedEndTime) {
mismatchReasons.push('end_time_mismatch');
}
return {
slotId: slot.id,
occurenceDate: startOfDay(slotDate),
startTime: slot.startTime,
endTime: slot.endTime,
maxCapacity: slot.maxCapacity,
debug: {
mismatchReasons,
},
};
}),
);
const selectedSlot = expandedSlots.find(
(slot) => slot.debug.mismatchReasons.length === 0,
);
if (!selectedSlot) {
throw new ApiError(
400,
`Unable to match a valid schedule slot for itinerary activity ${activity.id}.`,
);
}
const bookedSeats = Math.max(1, activity.paxCount ?? 1);
if (selectedSlot.maxCapacity < bookedSeats) {
throw new ApiError(
409,
`Insufficient capacity for itinerary activity ${activity.id}.`,
);
}
const capacityUpdate = await tx.scheduleDetails.updateMany({
where: {
id: selectedSlot.slotId,
maxCapacity: {
gte: bookedSeats,
},
isActive: true,
deletedAt: null,
},
data: {
maxCapacity: {
decrement: bookedSeats,
},
},
});
if (!capacityUpdate.count) {
throw new ApiError(
409,
`Unable to reserve capacity for itinerary activity ${activity.id}.`,
);
}
const refreshedSlot = await tx.scheduleDetails.findUnique({
where: {
id: selectedSlot.slotId,
},
select: {
maxCapacity: true,
},
});
let checkInCode = existingDetail?.offlineCode ?? null;
if (!checkInCode) {
for (let attempt = 0; attempt < 5; attempt += 1) {
const candidate = generateCheckInCode();
const duplicate = await tx.itineraryDetails.findFirst({
where: {
offlineCode: candidate,
isActive: true,
deletedAt: null,
},
select: {
id: true,
},
});
if (!duplicate) {
checkInCode = candidate;
break;
}
}
}
if (!checkInCode) {
throw new ApiError(500, 'Unable to generate a check-in code.');
}
const detail = existingDetail
? await tx.itineraryDetails.update({
where: {
id: existingDetail.id,
},
data: {
hasOpted: true,
updatedOn: new Date(),
itineraryKind: 'ACTIVITY',
offlineCode: checkInCode,
activityStatus: 'booked',
isChargeable: Number(activity.totalAmount) > 0,
baseAmount: Number(activity.totalAmount) || 0,
totalAmount: Number(activity.totalAmount) || 0,
itineraryStatus: 'paid',
isPaid: true,
paidByXid: userXid,
paidOn: new Date(),
},
select: {
id: true,
offlineCode: true,
},
})
: await tx.itineraryDetails.create({
data: {
itineraryActivityXid: activity.id,
itineraryMemberXid: itineraryMember.id,
hasOpted: true,
updatedOn: new Date(),
itineraryKind: 'ACTIVITY',
offlineCode: checkInCode,
activityStatus: 'booked',
isChargeable: Number(activity.totalAmount) > 0,
baseAmount: Number(activity.totalAmount) || 0,
totalAmount: Number(activity.totalAmount) || 0,
itineraryStatus: 'paid',
isPaid: true,
paidByXid: userXid,
paidOn: new Date(),
isActive: true,
},
select: {
id: true,
offlineCode: true,
},
});
await tx.itineraryActivities.update({
where: {
id: activity.id,
},
data: {
bookingStatus: 'booked',
},
});
return {
itineraryActivityXid: activity.id,
activityName: activity.activity?.activityTitle ?? null,
activityVenue: activity.venue?.venueName ?? null,
activityTime: {
occurenceDate: activity.occurenceDate,
startTime: activity.startTime,
endTime: activity.endTime,
},
foodDetails,
equipmentDetails,
navigationDetails,
pickupDetails,
skipped: false,
alreadyBooked: false,
checkInCode: detail.offlineCode,
bookedSeats,
remainingCapacity: refreshedSlot?.maxCapacity ?? null,
};
}),
);
return {
itineraryHeaderXid: itinerary.id,
itineraryNo: itinerary.itineraryNo,
itineraryTitle: itinerary.title,
bookedActivities,
};
}
async saveItineraryActivitySelections(
userXid: number,
payload: Array<{
itineraryActivityXid: number;
isFoodOpted?: boolean;
selectedFoodTypeIds?: number[];
isTrainerOpted?: boolean;
isInActivityNavigationOpted?: boolean;
selectedNavigationModeXid?: number | null;
selectedEquipmentIds?: number[];
}>,
) {
return this.prisma.$transaction(async (tx) => {
const result = await Promise.all(
payload.map(async (item, index) => {
const selectedFoodTypeIds = Array.from(
new Set((item.selectedFoodTypeIds ?? []).map(Number)),
);
const selectedEquipmentIds = Array.from(
new Set((item.selectedEquipmentIds ?? []).map(Number)),
);
const itineraryActivity = await tx.itineraryActivities.findFirst({
where: {
id: item.itineraryActivityXid,
isActive: true,
deletedAt: null,
itineraryHeader: {
ItineraryMembers: {
some: {
memberXid: userXid,
isActive: true,
deletedAt: null,
},
},
},
},
select: {
id: true,
itineraryHeaderXid: true,
itineraryType: true,
activityXid: true,
activity: {
select: {
id: true,
foodAvailable: true,
trainerAvailable: true,
inActivityAvailable: true,
equipmentAvailable: true,
activityFoodTypes: {
where: {
isActive: true,
deletedAt: null,
},
select: {
id: true,
foodTypeXid: true,
foodType: {
select: {
id: true,
foodTypeName: true,
},
},
},
},
ActivityNavigationModes: {
where: {
isActive: true,
deletedAt: null,
},
select: {
id: true,
navigationModeName: true,
isInActivityChargeable: true,
navigationModesBasePrice: true,
navigationModesTotalPrice: true,
},
},
ActivityEquipments: {
where: {
isActive: true,
deletedAt: null,
},
select: {
id: true,
equipmentName: true,
isEquipmentChargeable: true,
equipmentBasePrice: true,
equipmentTotalPrice: true,
},
},
},
},
},
});
if (!itineraryActivity) {
throw new ApiError(
404,
`activities[${index}] with itineraryActivityXid=${item.itineraryActivityXid} was not found.`,
);
}
if (itineraryActivity.itineraryType !== 'ACTIVITY') {
throw new ApiError(
400,
`activities[${index}] with itineraryActivityXid=${item.itineraryActivityXid} is not an ACTIVITY row. Actual itineraryType=${itineraryActivity.itineraryType}.`,
);
}
if (!itineraryActivity.activityXid) {
throw new ApiError(
400,
`activities[${index}] with itineraryActivityXid=${item.itineraryActivityXid} is missing activityXid.`,
);
}
if (!itineraryActivity.activity) {
throw new ApiError(
400,
`activities[${index}] with itineraryActivityXid=${item.itineraryActivityXid} could not load linked activity details.`,
);
}
const itineraryMember = await tx.itineraryMembers.findFirst({
where: {
itineraryHeaderXid: itineraryActivity.itineraryHeaderXid,
memberXid: userXid,
isActive: true,
deletedAt: null,
},
select: {
id: true,
},
});
if (!itineraryMember) {
throw new ApiError(404, 'Itinerary member record not found.');
}
if (selectedEquipmentIds.some((id) => !Number.isInteger(id) || id <= 0)) {
throw new ApiError(
400,
`activities[${index}].selectedEquipmentIds must contain valid ids.`,
);
}
if (selectedFoodTypeIds.some((id) => !Number.isInteger(id) || id <= 0)) {
throw new ApiError(
400,
`activities[${index}].selectedFoodTypeIds must contain valid ids.`,
);
}
const isFoodOpted = Boolean(item.isFoodOpted);
const isTrainerOpted = Boolean(item.isTrainerOpted);
const isInActivityNavigationOpted = Boolean(
item.isInActivityNavigationOpted,
);
const selectedNavigationModeXid =
item.selectedNavigationModeXid === undefined ||
item.selectedNavigationModeXid === null
? null
: Number(item.selectedNavigationModeXid);
if (
selectedNavigationModeXid !== null &&
(!Number.isInteger(selectedNavigationModeXid) ||
selectedNavigationModeXid <= 0)
) {
throw new ApiError(
400,
`activities[${index}].selectedNavigationModeXid must be a valid id.`,
);
}
const availableFoodTypes = itineraryActivity.activity.activityFoodTypes.map((entry) => ({
id: entry.id,
name: entry.foodType.foodTypeName,
}));
const availableFoodTypeIds = new Set(
availableFoodTypes.map((entry) => entry.id),
);
const availableNavigationModeIds = new Set(
itineraryActivity.activity.ActivityNavigationModes.map((entry) => entry.id),
);
const availableEquipments = itineraryActivity.activity.ActivityEquipments.map((entry) => ({
id: entry.id,
name: entry.equipmentName,
}));
const availableEquipmentIds = new Set(
availableEquipments.map((entry) => entry.id),
);
if (isFoodOpted) {
if (!itineraryActivity.activity.foodAvailable) {
throw new ApiError(400, `activities[${index}]: Food is not available for this activity.`);
}
if (
itineraryActivity.activity.activityFoodTypes.length > 0 &&
!selectedFoodTypeIds.length
) {
throw new ApiError(
400,
`activities[${index}].selectedFoodTypeIds is required when food is opted.`,
);
}
const invalidFoodTypeIds = selectedFoodTypeIds.filter(
(id) => !availableFoodTypeIds.has(id),
);
if (invalidFoodTypeIds.length) {
throw new ApiError(
400,
`activities[${index}] with itineraryActivityXid=${item.itineraryActivityXid} has invalid selectedFoodTypeIds=${invalidFoodTypeIds.join(', ')}. Allowed food types for this activity are: ${availableFoodTypes.length ? availableFoodTypes.map((entry) => `${entry.id}:${entry.name}`).join(', ') : 'none'}.`,
);
}
} else if (selectedFoodTypeIds.length) {
throw new ApiError(
400,
`activities[${index}].selectedFoodTypeIds cannot be sent when food is not opted.`,
);
}
if (isTrainerOpted && !itineraryActivity.activity.trainerAvailable) {
throw new ApiError(
400,
`activities[${index}]: Trainer is not available for this activity.`,
);
}
if (isInActivityNavigationOpted) {
if (!itineraryActivity.activity.inActivityAvailable) {
throw new ApiError(
400,
`activities[${index}]: In-activity navigation is not available for this activity.`,
);
}
if (
itineraryActivity.activity.ActivityNavigationModes.length > 0 &&
!selectedNavigationModeXid
) {
throw new ApiError(
400,
`activities[${index}].selectedNavigationModeXid is required when navigation is opted.`,
);
}
if (
selectedNavigationModeXid &&
!availableNavigationModeIds.has(selectedNavigationModeXid)
) {
throw new ApiError(
400,
`activities[${index}]: Selected navigation mode does not belong to this activity.`,
);
}
} else if (selectedNavigationModeXid) {
throw new ApiError(
400,
`activities[${index}].selectedNavigationModeXid cannot be sent when navigation is not opted.`,
);
}
if (selectedEquipmentIds.length) {
if (!itineraryActivity.activity.equipmentAvailable) {
throw new ApiError(
400,
`activities[${index}]: Equipment is not available for this activity.`,
);
}
const invalidEquipmentIds = selectedEquipmentIds.filter(
(id) => !availableEquipmentIds.has(id),
);
if (invalidEquipmentIds.length) {
throw new ApiError(
400,
`activities[${index}] with itineraryActivityXid=${item.itineraryActivityXid} has invalid selectedEquipmentIds=${invalidEquipmentIds.join(', ')}. Allowed equipments for this activity are: ${availableEquipments.length ? availableEquipments.map((entry) => `${entry.id}:${entry.name}`).join(', ') : 'none'}.`,
);
}
}
const selection = await tx.itineraryActivitySelection.upsert({
where: {
itineraryActivityXid_itineraryMemberXid: {
itineraryActivityXid: itineraryActivity.id,
itineraryMemberXid: itineraryMember.id,
},
},
create: {
itineraryActivityXid: itineraryActivity.id,
itineraryMemberXid: itineraryMember.id,
isFoodOpted,
isTrainerOpted,
isInActivityNavigationOpted,
activityNavigationModeXid: isInActivityNavigationOpted
? selectedNavigationModeXid
: null,
isActive: true,
},
update: {
isFoodOpted,
isTrainerOpted,
isInActivityNavigationOpted,
activityNavigationModeXid: isInActivityNavigationOpted
? selectedNavigationModeXid
: null,
isActive: true,
deletedAt: null,
},
select: {
id: true,
},
});
await tx.itineraryActivitySelectionFoodType.deleteMany({
where: {
itineraryActivitySelectionXid: selection.id,
},
});
await tx.itineraryActivitySelectionEquipment.deleteMany({
where: {
itineraryActivitySelectionXid: selection.id,
},
});
if (selectedFoodTypeIds.length) {
await tx.itineraryActivitySelectionFoodType.createMany({
data: selectedFoodTypeIds.map((activityFoodTypeXid) => ({
itineraryActivitySelectionXid: selection.id,
activityFoodTypeXid,
})),
});
}
if (selectedEquipmentIds.length) {
await tx.itineraryActivitySelectionEquipment.createMany({
data: selectedEquipmentIds.map((activityEquipmentXid) => ({
itineraryActivitySelectionXid: selection.id,
activityEquipmentXid,
})),
});
}
return tx.itineraryActivitySelection.findUnique({
where: {
id: selection.id,
},
select: {
id: true,
itineraryActivityXid: true,
itineraryMemberXid: true,
isFoodOpted: true,
isTrainerOpted: true,
isInActivityNavigationOpted: true,
activityNavigationModeXid: true,
selectedFoodTypes: {
where: {
isActive: true,
deletedAt: null,
},
select: {
id: true,
activityFoodTypeXid: true,
activityFoodType: {
select: {
id: true,
foodTypeXid: true,
foodType: {
select: {
id: true,
foodTypeName: true,
},
},
},
},
},
},
activityNavigationMode: {
select: {
id: true,
navigationModeName: true,
isInActivityChargeable: true,
navigationModesBasePrice: true,
navigationModesTotalPrice: true,
},
},
selectedEquipments: {
where: {
isActive: true,
deletedAt: null,
},
select: {
id: true,
activityEquipmentXid: true,
activityEquipment: {
select: {
id: true,
equipmentName: true,
isEquipmentChargeable: true,
equipmentBasePrice: true,
equipmentTotalPrice: true,
},
},
},
},
},
});
}),
);
return result;
});
}
async getMatchingBucketInterestedActivities(
userXid: number,
payload: {
userLat: number;
userLong: number;
startDate: string;
endDate: string;
startTime: string;
endTime: string;
energyLevelXid?: number;
entryTypeXid: number;
groupCount?: number;
page: number;
limit: number;
},
) {
const requestedStart = combineDateAndTime(
payload.startDate,
payload.startTime,
);
const requestedEnd = combineDateAndTime(payload.endDate, payload.endTime);
if (!requestedStart || !requestedEnd) {
throw new ApiError(400, 'Invalid start/end date or time values.');
}
if (requestedStart >= requestedEnd) {
throw new ApiError(
400,
'Start date and time must be earlier than end date and time.',
);
}
if (payload.groupCount !== undefined) {
if (!Number.isInteger(payload.groupCount) || payload.groupCount <= 0) {
throw new ApiError(400, 'groupCount must be a positive integer.');
}
}
const rangeStartDay = startOfDay(requestedStart);
const rangeEndDay = startOfDay(requestedEnd);
const requestedEntryType = await this.prisma.allowedEntryTypes.findFirst({
where: {
id: payload.entryTypeXid,
isActive: true,
deletedAt: null,
},
select: {
id: true,
allowedEntryTypeName: true,
},
});
if (!requestedEntryType) {
throw new ApiError(404, 'Selected entry type not found.');
}
const isGroupEntryType =
requestedEntryType.allowedEntryTypeName.trim().toLowerCase() === 'group';
if (isGroupEntryType && payload.groupCount === undefined) {
throw new ApiError(
400,
'groupCount is required when entryTypeXid is for group.',
);
}
const activityEntries = await this.prisma.userBucketInterested.findMany({
where: {
userXid,
isActive: true,
deletedAt: null,
Activities: {
isActive: true,
deletedAt: null,
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
activityType: {
isActive: true,
deletedAt: null,
...(payload.energyLevelXid !== undefined
? { energyLevelXid: payload.energyLevelXid }
: {}),
},
ActivityAllowedEntry: {
some: {
isActive: true,
deletedAt: null,
allowedEntryTypeXid: payload.entryTypeXid,
},
},
ActivityVenues: {
some: {
isActive: true,
deletedAt: null,
ScheduleHeader: {
some: {
isActive: true,
deletedAt: null,
startDate: { lte: rangeEndDay },
OR: [
{ endDate: null },
{ endDate: { gte: rangeStartDay } },
],
},
},
},
},
},
},
orderBy: {
createdAt: 'desc',
},
select: {
id: true,
isBucket: true,
bucketTypeName: true,
activityXid: true,
Activities: {
select: {
id: true,
activityTitle: true,
activityDurationMins: true,
activityDescription: true,
checkInLat: true,
checkInLong: true,
checkInAddress: true,
checkInCity: {
select: {
id: true,
cityName: true,
},
},
checkInState: {
select: {
id: true,
stateName: true,
},
},
checkInCountry: {
select: {
id: true,
countryName: true,
countryCode: true,
},
},
activityType: {
select: {
activityTypeName: true,
interestXid: true,
interests: {
select: {
id: true,
interestName: true,
},
},
id: true,
energyLevelXid: true,
energyLevel: {
select: {
id: true,
energyLevelName: true,
energyIcon: true,
},
},
},
},
ActivityAllowedEntry: {
where: {
isActive: true,
deletedAt: null,
allowedEntryTypeXid: payload.entryTypeXid,
},
select: {
allowedEntryTypeXid: true,
allowedEntryType: {
select: {
id: true,
allowedEntryTypeName: true,
},
},
},
},
ActivitiesMedia: {
where: {
isActive: true,
deletedAt: null,
},
orderBy: {
displayOrder: 'asc',
},
select: {
id: true,
mediaType: true,
mediaFileName: true,
isCoverImage: true,
displayOrder: true,
},
},
ActivityVenues: {
where: {
isActive: true,
deletedAt: null,
ScheduleHeader: {
some: {
isActive: true,
deletedAt: null,
startDate: { lte: rangeEndDay },
OR: [
{ endDate: null },
{ endDate: { gte: rangeStartDay } },
],
},
},
},
select: {
id: true,
venueName: true,
venueLabel: true,
venueCapacity: true,
ActivityPrices: {
where: {
isActive: true,
deletedAt: null,
},
orderBy: {
createdAt: 'asc',
},
select: {
id: true,
noOfSession: true,
isPackage: true,
sessionValidity: true,
sessionValidityFrequency: true,
basePrice: true,
sellPrice: true,
},
},
ActivityVenueArtifacts: {
where: {
isActive: true,
deletedAt: null,
},
select: {
id: true,
mediaType: true,
mediaFileName: true,
},
},
availableSeats: true,
ScheduleHeader: {
where: {
isActive: true,
deletedAt: null,
startDate: { lte: rangeEndDay },
OR: [
{ endDate: null },
{ endDate: { gte: rangeStartDay } },
],
},
select: {
id: true,
scheduleType: true,
startDate: true,
endDate: true,
ScheduleDetails: {
where: {
isActive: true,
deletedAt: null,
maxCapacity:
isGroupEntryType && payload.groupCount !== undefined
? { gte: payload.groupCount }
: { gt: 0 },
},
select: {
id: true,
occurenceDate: true,
weekDay: true,
dayOfMonth: true,
startTime: true,
endTime: true,
maxCapacity: true,
},
},
Cancellations: {
where: {
isActive: true,
deletedAt: null,
occurenceDate: {
gte: rangeStartDay,
lte: rangeEndDay,
},
},
select: {
occurenceDate: true,
startTime: true,
endTime: true,
},
},
},
},
},
},
},
},
},
});
const formattedActivities = await Promise.all(
activityEntries.map(async (entry) => {
const activity = entry.Activities;
const activityDurationMins = activity?.activityDurationMins ?? 0;
const distance = calculateDistance(
payload.userLat,
payload.userLong,
activity?.checkInLat ?? null,
activity?.checkInLong ?? null,
);
const availableSlots = activity.ActivityVenues.flatMap((venue) =>
venue.ScheduleHeader.flatMap((header) => {
const effectiveRangeStart =
header.startDate > rangeStartDay ? header.startDate : rangeStartDay;
const headerEndDate = header.endDate ?? rangeEndDay;
const effectiveRangeEnd =
headerEndDate < rangeEndDay ? headerEndDate : rangeEndDay;
if (effectiveRangeStart > effectiveRangeEnd) {
return [];
}
const cancelledSlots = new Set(
header.Cancellations.map((cancellation) => {
if (!cancellation.occurenceDate) {
return null;
}
return `${formatDateKey(cancellation.occurenceDate)}|${cancellation.startTime}|${cancellation.endTime}`;
}).filter(Boolean) as string[],
);
return header.ScheduleDetails.flatMap((slot) => {
const slotDates: Date[] = [];
if (slot.occurenceDate) {
const occurrenceDay = startOfDay(slot.occurenceDate);
if (
occurrenceDay >= startOfDay(effectiveRangeStart) &&
occurrenceDay <= startOfDay(effectiveRangeEnd)
) {
slotDates.push(occurrenceDay);
}
} else {
for (const currentDate of getDateRange(
effectiveRangeStart,
effectiveRangeEnd,
)) {
const weekDayName = WEEKDAY_NAMES[currentDate.getDay()];
if (slot.weekDay && slot.weekDay !== weekDayName) {
continue;
}
if (
slot.dayOfMonth !== null &&
slot.dayOfMonth !== undefined &&
slot.dayOfMonth !== currentDate.getDate()
) {
continue;
}
slotDates.push(currentDate);
}
}
return slotDates
.map((slotDate) => {
const slotStart = combineDateAndTime(slotDate, slot.startTime);
if (!slotStart) {
return null;
}
const slotEnd = activityDurationMins
? addMinutes(slotStart, activityDurationMins)
: combineDateAndTime(slotDate, slot.endTime);
if (!slotEnd) {
return null;
}
const cancellationKey = `${formatDateKey(slotDate)}|${slot.startTime}|${slot.endTime}`;
if (cancelledSlots.has(cancellationKey)) {
return null;
}
if (slotStart < requestedStart || slotEnd > requestedEnd) {
return null;
}
if (
isGroupEntryType &&
payload.groupCount !== undefined &&
slot.maxCapacity < payload.groupCount
) {
return null;
}
return {
scheduleHeaderXid: header.id,
slotId: slot.id,
venueXid: venue.id,
venueName: venue.venueName,
venueLabel: venue.venueLabel,
activityPrices: venue.ActivityPrices,
mediaFileName: venue.ActivityVenueArtifacts[0]?.mediaFileName ?? null,
mediaType: venue.ActivityVenueArtifacts[0]?.mediaType ?? null,
venueCapacity: venue.venueCapacity,
availableSeats: venue.availableSeats,
slotDate: formatDateKey(slotDate),
startTime: slot.startTime,
endTime: slotEnd.toLocaleTimeString('en-US', {
hour: 'numeric',
minute: '2-digit',
hour12: true,
}),
startDateTime: slotStart.toISOString(),
endDateTime: slotEnd.toISOString(),
maxCapacity: slot.maxCapacity,
};
})
.filter(Boolean);
});
}),
);
const sanitizedAvailableSlots = availableSlots.filter(
(
slot,
): slot is NonNullable<(typeof availableSlots)[number]> =>
Boolean(slot),
);
const availableSlotsWithPresignedUrl = await Promise.all(
sanitizedAvailableSlots.map(async (slot) => ({
...slot,
mediaFileNamePresignedUrl: await attachPresignedUrl(
slot.mediaFileName,
),
})),
);
if (!availableSlotsWithPresignedUrl.length) {
return null;
}
availableSlotsWithPresignedUrl.sort(
(first, second) =>
new Date(first!.startDateTime).getTime() -
new Date(second!.startDateTime).getTime(),
);
const venuePriceDetails = activity.ActivityVenues.map((venue) => {
const prices = venue.ActivityPrices.map((price) => ({
id: price.id,
noOfSession: price.noOfSession,
isPackage: price.isPackage,
sessionValidity: price.sessionValidity,
sessionValidityFrequency: price.sessionValidityFrequency,
basePrice: price.basePrice,
sellPrice: price.sellPrice,
}));
const lowestPrice = venue.ActivityPrices.reduce(
(lowest, current) =>
!lowest || current.sellPrice < lowest.sellPrice ? current : lowest,
null as (typeof venue.ActivityPrices)[number] | null,
);
return {
venueXid: venue.id,
venueName: venue.venueName,
venueLabel: venue.venueLabel,
venueCapacity: venue.venueCapacity,
availableSeats: venue.availableSeats,
activityPrice: lowestPrice?.sellPrice ?? null,
activityBasePrice: lowestPrice?.basePrice ?? null,
activityPriceDetails: lowestPrice,
prices,
};
});
const lowestPriceVenue = venuePriceDetails.reduce(
(lowest, current) => {
if (!lowest) {
return current;
}
if (lowest.activityPrice === null) {
return current;
}
if (current.activityPrice === null) {
return lowest;
}
return current.activityPrice < lowest.activityPrice ? current : lowest;
},
null as (typeof venuePriceDetails)[number] | null,
);
const coverImage =
activity.ActivitiesMedia.find((media) => media.isCoverImage) ??
activity.ActivitiesMedia[0] ??
null;
const energyLevel = activity.activityType?.energyLevel ?? null;
const lowestActivityPrice = lowestPriceVenue?.activityPrice ?? null;
const lowestActivityBasePrice = lowestPriceVenue?.activityBasePrice ?? null;
const lowestActivityPriceDetails = lowestPriceVenue?.activityPriceDetails ?? null;
return {
userBucketInterestedXid: entry.id,
activityXid: entry.activityXid,
isBucket: entry.isBucket,
bucketTypeName: entry.bucketTypeName,
distance,
activityPrice: lowestActivityPrice,
activityBasePrice: lowestActivityBasePrice,
activityPriceDetails: lowestActivityPriceDetails,
lowestActivityPrice,
lowestActivityBasePrice,
lowestActivityPriceDetails,
activityTitle: activity.activityTitle,
activityDescription: activity.activityDescription,
activityDurationMins,
activityCoverImage: coverImage?.mediaFileName ?? null,
activityCoverImagePresignedUrl: await attachPresignedUrl(
coverImage?.mediaFileName,
),
venue: lowestPriceVenue
? {
venueXid: lowestPriceVenue.venueXid,
venueName: lowestPriceVenue.venueName,
venueLabel: lowestPriceVenue.venueLabel,
venueCapacity: lowestPriceVenue.venueCapacity,
availableSeats: lowestPriceVenue.availableSeats,
activityPrice: lowestPriceVenue.activityPrice,
activityBasePrice: lowestPriceVenue.activityBasePrice,
activityPriceDetails: lowestPriceVenue.activityPriceDetails,
}
: null,
venuePrices: venuePriceDetails,
availableSlots: availableSlotsWithPresignedUrl,
entryType: activity.ActivityAllowedEntry[0]?.allowedEntryType ?? null,
energyLevel: energyLevel
? {
energyLevelXid: energyLevel.id,
energyLevelName: energyLevel.energyLevelName,
energyLevelIcon: energyLevel.energyIcon,
energyLevelIconPresignedUrl: await attachPresignedUrl(
energyLevel.energyIcon,
),
}
: null,
checkInAddress: activity.checkInAddress,
checkInCity: activity.checkInCity,
checkInState: activity.checkInState,
checkInCountry: activity.checkInCountry,
checkInLat: activity.checkInLat,
checkInLong: activity.checkInLong,
interest: activity.activityType?.interests
? {
id: activity.activityType.interests.id,
interestName: activity.activityType.interests.interestName,
}
: null,
};
}),
);
const activities = formattedActivities
.filter(Boolean)
.sort((first, second) => {
const firstDistance =
first!.distance === null ? Number.POSITIVE_INFINITY : first!.distance;
const secondDistance =
second!.distance === null ? Number.POSITIVE_INFINITY : second!.distance;
if (firstDistance !== secondDistance) {
return firstDistance - secondDistance;
}
return first!.activityXid - second!.activityXid;
});
const uniqueById = <T extends { id: number }>(items: T[]) =>
Array.from(new Map(items.map((item) => [item.id, item])).values());
const distinctInterests = uniqueById(
activities
.map((activity) => activity!.interest)
.filter((item): item is NonNullable<(typeof activities)[number]['interest']> =>
Boolean(item),
),
).sort((first, second) => first.interestName.localeCompare(second.interestName));
const distinctBucketTypeNames = Array.from(
new Set(
activities
.map((activity) => activity!.bucketTypeName)
.filter((item): item is string => Boolean(item?.trim())),
),
).sort((first, second) => first.localeCompare(second));
const distinctEnergyLevels = uniqueById(
activities
.map((activity) => activity!.energyLevel)
.filter((item): item is NonNullable<(typeof activities)[number]['energyLevel']> =>
Boolean(item),
)
.map((item) => ({
id: item.energyLevelXid,
energyLevelName: item.energyLevelName,
energyIcon: item.energyLevelIcon,
energyIconPresignedUrl: item.energyLevelIconPresignedUrl,
})),
).sort((first, second) =>
first.energyLevelName.localeCompare(second.energyLevelName),
);
const distinctCities = uniqueById(
activities
.map((activity) => activity!.checkInCity)
.filter((item): item is NonNullable<(typeof activities)[number]['checkInCity']> =>
Boolean(item),
),
).sort((first, second) => first.cityName.localeCompare(second.cityName));
const distinctStates = uniqueById(
activities
.map((activity) => activity!.checkInState)
.filter((item): item is NonNullable<(typeof activities)[number]['checkInState']> =>
Boolean(item),
),
).sort((first, second) => first.stateName.localeCompare(second.stateName));
const distinctCountries = uniqueById(
activities
.map((activity) => activity!.checkInCountry)
.filter(
(item): item is NonNullable<(typeof activities)[number]['checkInCountry']> =>
Boolean(item),
),
).sort((first, second) => first.countryName.localeCompare(second.countryName));
const durations = activities
.map((activity) => activity!.activityDurationMins)
.filter((item): item is number => typeof item === 'number');
const totalCount = activities.length;
const sanitizedLimit = Math.min(Math.max(Math.floor(payload.limit || 20), 1), 20);
const sanitizedPage = Math.max(Math.floor(payload.page), 1);
const totalPages = totalCount ? Math.ceil(totalCount / sanitizedLimit) : 0;
const startIndex = (sanitizedPage - 1) * sanitizedLimit;
const paginatedActivities = activities.slice(
startIndex,
startIndex + sanitizedLimit,
);
return {
filters: {
userLat: payload.userLat,
userLong: payload.userLong,
startDate: payload.startDate,
endDate: payload.endDate,
startTime: payload.startTime,
endTime: payload.endTime,
energyLevelXid: payload.energyLevelXid,
entryTypeXid: payload.entryTypeXid,
groupCount: payload.groupCount,
page: sanitizedPage,
limit: sanitizedLimit,
interestTypes: distinctInterests,
bucketTypeNames: distinctBucketTypeNames,
energyLevels: distinctEnergyLevels,
cities: distinctCities,
states: distinctStates,
countries: distinctCountries,
minActivityDurationMins: durations.length ? Math.min(...durations) : null,
maxActivityDurationMins: durations.length ? Math.max(...durations) : null,
},
pagination: {
page: sanitizedPage,
limit: sanitizedLimit,
totalCount,
totalPages,
hasNextPage: sanitizedPage < totalPages,
hasPreviousPage: sanitizedPage > 1 && totalPages > 0,
},
count: paginatedActivities.length,
activityCount: totalCount,
totalCount,
activities: paginatedActivities,
};
}
async getActivityDetailsAfterBooking(
userXid: number,
itineraryHeaderXid: number,
) {
const itineraryHeader = await this.prisma.itineraryHeader.findFirst({
where: {
id: itineraryHeaderXid,
isActive: true,
deletedAt: null,
OR: [
{
ownerXid: userXid,
},
{
ItineraryMembers: {
some: {
memberXid: userXid,
isActive: true,
deletedAt: null,
},
},
},
],
},
select: {
id: true,
itineraryNo: true,
title: true,
fromDate: true,
fromTime: true,
toDate: true,
toTime: true,
itineraryStatus: true,
ownerXid: true,
owner: {
select: {
id: true,
firstName: true,
lastName: true,
emailAddress: true,
mobileNumber: true,
},
},
ItineraryMembers: {
where: {
memberXid: userXid,
isActive: true,
deletedAt: null,
},
select: {
id: true,
memberXid: true,
memberRole: true,
memberStatus: true,
member: {
select: {
id: true,
firstName: true,
lastName: true,
emailAddress: true,
mobileNumber: true,
profileImage: true,
},
},
},
},
ItineraryActivities: {
where: {
activityXid: {
not: null,
},
isActive: true,
deletedAt: null,
},
orderBy: [
{
occurenceDate: 'asc',
},
{
startTime: 'asc',
},
{
displayOrder: 'asc',
},
],
select: {
id: true,
displayOrder: true,
occurenceDate: true,
startTime: true,
endTime: true,
paxCount: true,
totalAmount: true,
bookingStatus: true,
venue: {
select: {
id: true,
venueName: true,
venueLabel: true,
},
},
ItineraryDetails: {
where: {
itineraryKind: 'ACTIVITY',
isActive: true,
deletedAt: null,
itineraryMember: {
is: {
memberXid: userXid,
isActive: true,
deletedAt: null,
},
},
},
orderBy: {
createdAt: 'desc',
},
select: {
id: true,
itineraryMemberXid: true,
offlineCode: true,
description1: true,
description2: true,
activityStatus: true,
itineraryStatus: true,
isPaid: true,
paidOn: true,
createdAt: true,
},
},
itineraryActivitySelections: {
where: {
isActive: true,
deletedAt: null,
itineraryMember: {
is: {
memberXid: userXid,
isActive: true,
deletedAt: null,
},
},
},
select: {
id: true,
itineraryMemberXid: true,
isFoodOpted: true,
isTrainerOpted: true,
isInActivityNavigationOpted: true,
activityNavigationMode: {
select: {
id: true,
navigationModeName: true,
},
},
selectedFoodTypes: {
where: {
isActive: true,
deletedAt: null,
},
select: {
id: true,
activityFoodTypeXid: true,
activityFoodType: {
select: {
id: true,
foodTypeXid: true,
foodType: {
select: {
id: true,
foodTypeName: true,
},
},
},
},
},
},
selectedEquipments: {
where: {
isActive: true,
deletedAt: null,
},
select: {
id: true,
activityEquipmentXid: true,
activityEquipment: {
select: {
id: true,
equipmentName: true,
},
},
},
},
},
},
activity: {
select: {
id: true,
activityTitle: true,
activityDescription: true,
checkInAddress: true,
checkInLat: true,
checkInLong: true,
activityDurationMins: true,
foodAvailable: true,
trainerAvailable: true,
equipmentAvailable: true,
pickUpDropAvailable: true,
inActivityAvailable: true,
ActivitiesMedia: {
where: {
isActive: true,
deletedAt: null,
},
orderBy: {
displayOrder: 'asc',
},
select: {
id: true,
mediaType: true,
mediaFileName: true,
isCoverImage: true,
displayOrder: true,
},
},
ActivityPickUpDetails: {
where: {
isActive: true,
deletedAt: null,
},
select: {
id: true,
isPickUp: true,
locationLat: true,
locationLong: true,
locationAddress: true,
},
},
ActivityOtherDetails: {
where: {
isActive: true,
deletedAt: null,
},
select: {
exclusiveNotes: true,
SafetyInstruction: true,
Cancellations: true,
dosNotes: true,
dontsNotes: true,
tipsNotes: true,
termsAndCondition: true,
},
take: 1,
},
},
},
},
},
},
});
if (!itineraryHeader) {
throw new ApiError(404, 'Itinerary not found');
}
const itineraryMember = itineraryHeader.ItineraryMembers[0] ?? null;
if (itineraryHeader.ItineraryActivities.length === 0) {
throw new ApiError(404, 'No activities found for this itinerary');
}
const activities = await Promise.all(
itineraryHeader.ItineraryActivities.map(async (itineraryActivity) => {
const itineraryDetail = itineraryActivity.ItineraryDetails[0] ?? null;
const activitySelection =
itineraryActivity.itineraryActivitySelections[0] ?? null;
const selectedFoodTypes = (
activitySelection?.selectedFoodTypes ?? []
).map(
(selectedFoodType) =>
selectedFoodType.activityFoodType.foodType.foodTypeName,
);
const selectedEquipments = (
activitySelection?.selectedEquipments ?? []
).map(
(selectedEquipment) =>
selectedEquipment.activityEquipment.equipmentName,
);
const selectedNavigationMode =
activitySelection?.activityNavigationMode?.navigationModeName ?? null;
const mediaWithUrls = await Promise.all(
(itineraryActivity.activity?.ActivitiesMedia ?? []).map(
async (media) => ({
...media,
mediaUrl: await attachPresignedUrl(media.mediaFileName),
}),
),
);
const coverImage =
mediaWithUrls.find((media) => media.isCoverImage) ??
mediaWithUrls[0] ??
null;
const pickUpDetail =
itineraryActivity.activity?.ActivityPickUpDetails.find(
(detail) => detail.isPickUp && detail.locationAddress,
) ??
itineraryActivity.activity?.ActivityPickUpDetails.find(
(detail) => detail.locationAddress,
) ??
null;
const dropDetail =
itineraryActivity.activity?.ActivityPickUpDetails.find(
(detail) => !detail.isPickUp && detail.locationAddress,
) ?? null;
const bookingDate = formatDateKey(itineraryActivity.occurenceDate);
const ticketTime = formatTicketTimeRange(
itineraryActivity.startTime,
itineraryActivity.endTime,
);
const venueName =
itineraryActivity.venue?.venueLabel ??
itineraryActivity.venue?.venueName ??
null;
const foodIncluded = selectedFoodTypes.length > 0 ? 'Yes' : 'No';
const equipmentIncluded = selectedEquipments.length > 0 ? 'Yes' : 'No';
const pickAndDropIncluded = itineraryActivity.activity?.pickUpDropAvailable
? 'Yes'
: 'No';
const inActivityNavigation =
selectedNavigationMode ??
(activitySelection?.isInActivityNavigationOpted ? 'Yes' : 'No');
return {
itineraryActivityId: itineraryActivity.id,
bookingId: itineraryHeader.itineraryNo,
activityId: itineraryActivity.activity?.id ?? null,
activityTitle: itineraryActivity.activity?.activityTitle ?? null,
activityDescription:
itineraryActivity.activity?.activityDescription ?? null,
occurenceDate: itineraryActivity.occurenceDate,
startTime: itineraryActivity.startTime,
endTime: itineraryActivity.endTime,
duration: itineraryActivity.activity?.activityDurationMins ?? null,
checkInCode: itineraryDetail?.offlineCode ?? null,
qrCodeValue: itineraryDetail?.offlineCode ?? null,
checkInAddress: itineraryActivity.activity?.checkInAddress ?? null,
checkInLat: itineraryActivity.activity?.checkInLat ?? null,
checkInLong: itineraryActivity.activity?.checkInLong ?? null,
bookingStatus:
itineraryDetail?.activityStatus ??
itineraryActivity.bookingStatus ??
null,
description1: itineraryDetail?.description1 ?? null,
description2: itineraryDetail?.description2 ?? null,
itineraryStatus: itineraryDetail?.itineraryStatus ?? null,
isPaid: itineraryDetail?.isPaid ?? false,
paidOn: itineraryDetail?.paidOn ?? null,
paxCount: itineraryActivity.paxCount ?? null,
totalAmount: itineraryActivity.totalAmount ?? null,
venue: {
id: itineraryActivity.venue?.id ?? null,
venueName: itineraryActivity.venue?.venueName ?? null,
venueLabel: itineraryActivity.venue?.venueLabel ?? null,
displayName: venueName,
},
images: mediaWithUrls.map((media) => ({
id: media.id,
type: media.mediaType,
fileName: media.mediaFileName,
url: media.mediaUrl,
isCover: media.isCoverImage,
order: media.displayOrder,
})),
coverImage: coverImage
? {
id: coverImage.id,
fileName: coverImage.mediaFileName,
url: coverImage.mediaUrl,
}
: null,
bookingInformation: {
activityName: itineraryActivity.activity?.activityTitle ?? null,
date: bookingDate,
venue: venueName,
venueName: itineraryActivity.venue?.venueName ?? null,
venueLabel: itineraryActivity.venue?.venueLabel ?? null,
time: ticketTime,
startTime: formatTicketTime(itineraryActivity.startTime),
endTime: formatTicketTime(itineraryActivity.endTime),
},
bookingIncluded: {
food: foodIncluded,
selectedFoodTypes,
equipment: equipmentIncluded,
selectedEquipments,
pickAndDrop: pickAndDropIncluded,
pickupLocation:
pickUpDetail?.locationAddress ??
itineraryActivity.activity?.checkInAddress ??
null,
dropLocation: dropDetail?.locationAddress ?? null,
inActivityNavigation: inActivityNavigation ?? 'No',
trainerOrGuide: activitySelection?.isTrainerOpted ? 'Yes' : 'No',
},
ticketCard: {
checkInCode: itineraryDetail?.offlineCode ?? null,
activityTitle: itineraryActivity.activity?.activityTitle ?? null,
date: bookingDate,
venue: venueName,
time: ticketTime,
food: foodIncluded,
equipments: equipmentIncluded,
pickAndDrop: pickAndDropIncluded,
inActivityNavigation: inActivityNavigation ?? 'No',
},
userSelections: {
isFoodOpted: activitySelection?.isFoodOpted ?? false,
selectedFoodTypes,
isTrainerOpted: activitySelection?.isTrainerOpted ?? false,
isInActivityNavigationOpted:
activitySelection?.isInActivityNavigationOpted ?? false,
selectedNavigationMode: selectedNavigationMode,
selectedEquipments,
},
pickupDetails: {
pickUpDropAvailable:
itineraryActivity.activity?.pickUpDropAvailable ?? false,
pickUpLocation: pickUpDetail
? {
id: pickUpDetail.id,
locationAddress: pickUpDetail.locationAddress,
locationLat: pickUpDetail.locationLat,
locationLong: pickUpDetail.locationLong,
}
: null,
dropLocation: dropDetail
? {
id: dropDetail.id,
locationAddress: dropDetail.locationAddress,
locationLat: dropDetail.locationLat,
locationLong: dropDetail.locationLong,
}
: null,
},
activityInfo: {
exclusiveNotes:
itineraryActivity.activity?.ActivityOtherDetails[0]
?.exclusiveNotes || null,
safetyInstructions:
itineraryActivity.activity?.ActivityOtherDetails[0]
?.SafetyInstruction || null,
cancellationPolicy:
itineraryActivity.activity?.ActivityOtherDetails[0]
?.Cancellations || null,
dosAndDonts: {
dos:
itineraryActivity.activity?.ActivityOtherDetails[0]?.dosNotes ||
null,
donts:
itineraryActivity.activity?.ActivityOtherDetails[0]
?.dontsNotes || null,
tips:
itineraryActivity.activity?.ActivityOtherDetails[0]?.tipsNotes ||
null,
},
termsAndConditions:
itineraryActivity.activity?.ActivityOtherDetails[0]
?.termsAndCondition || null,
},
};
}),
);
// Return itinerary object with activities inside
return {
itinerary: {
id: itineraryHeader.id,
itineraryNo: itineraryHeader.itineraryNo,
title: itineraryHeader.title,
status: itineraryHeader.itineraryStatus,
fromDate: itineraryHeader.fromDate,
fromTime: itineraryHeader.fromTime,
toDate: itineraryHeader.toDate,
toTime: itineraryHeader.toTime,
owner: {
id: itineraryHeader.owner?.id,
firstName: itineraryHeader.owner?.firstName,
lastName: itineraryHeader.owner?.lastName,
email: itineraryHeader.owner?.emailAddress,
phone: itineraryHeader.owner?.mobileNumber,
},
viewer: itineraryMember
? {
itineraryMemberXid: itineraryMember.id,
memberXid: itineraryMember.memberXid,
memberRole: itineraryMember.memberRole,
memberStatus: itineraryMember.memberStatus,
firstName: itineraryMember.member.firstName,
lastName: itineraryMember.member.lastName,
email: itineraryMember.member.emailAddress,
phone: itineraryMember.member.mobileNumber,
profileImage: itineraryMember.member.profileImage,
profileImagePresignedUrl: await attachPresignedUrl(
itineraryMember.member.profileImage,
),
}
: null,
totalActivities: activities.length,
ticketCards: activities.map((activity) => activity.ticketCard),
activities,
},
};
}
}