2026-03-15 20:33:45 +05:30
import { Injectable } from '@nestjs/common' ;
2026-04-02 16:03:12 +05:30
import { Prisma , PrismaClient } from '@prisma/client' ;
2026-04-13 14:00:50 +05:30
import crypto from 'crypto' ;
2026-03-15 20:33:45 +05:30
import { getPresignedUrl } from '../../../common/middlewares/aws/getPreSignedUrl' ;
2026-03-17 16:22:03 +05:30
import ApiError from '../../../common/utils/helper/ApiError' ;
2026-03-15 20:33:45 +05:30
import {
ACTIVITY_AM_INTERNAL_STATUS ,
ACTIVITY_INTERNAL_STATUS ,
} from '../../../common/utils/constants/host.constant' ;
import config from '@/config/config' ;
const bucket = config . aws . bucketName ;
2026-03-17 16:22:03 +05:30
const WEEKDAY_NAMES = [
'SUNDAY' ,
'MONDAY' ,
'TUESDAY' ,
'WEDNESDAY' ,
'THURSDAY' ,
'FRIDAY' ,
'SATURDAY' ,
] as const ;
2026-03-15 20:33:45 +05:30
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 ) ;
} ;
2026-04-13 14:00:50 +05:30
const generateCheckInCode = ( ) = > ` # ${ crypto . randomInt ( 1000000 , 10000000 ) } ` ;
2026-04-13 13:19:50 +05:30
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 ;
} ;
2026-03-15 20:33:45 +05:30
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 ) ,
} ) ) ,
) ;
} ;
2026-03-17 16:22:03 +05:30
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 } ;
} ;
2026-04-02 16:03:12 +05:30
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' ) } ` ;
} ;
2026-03-17 16:22:03 +05:30
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 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 ;
} ;
2026-03-18 19:49:04 +05:30
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 ;
} ;
2026-03-15 20:33:45 +05:30
@Injectable ( )
export class ItineraryService {
2026-03-27 11:57:05 +05:30
constructor ( private prisma : PrismaClient ) { }
2026-03-15 20:33:45 +05:30
async getUserItineraryDetails ( userXid : number ) {
2026-03-18 12:45:59 +05:30
const [ userLocation , activityEntries , travellerType , energyLevel ] = await Promise . all ( [
2026-03-15 20:33:45 +05:30
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 ,
2026-03-18 12:45:59 +05:30
activityType : {
select : {
energyLevel : {
select : {
id : true ,
energyLevelName : true ,
energyIcon : true ,
} ,
} ,
} ,
} ,
2026-03-15 20:33:45 +05:30
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' ,
} ,
} ) ,
2026-03-18 12:45:59 +05:30
this . prisma . energyLevels . findMany ( {
where : {
isActive : true ,
deletedAt : null ,
} ,
select : {
id : true ,
energyLevelName : true ,
energyIcon : true ,
energyColor : true ,
} ,
orderBy : {
id : 'asc' ,
}
} )
2026-03-15 20:33:45 +05:30
] ) ;
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 ,
) ,
2026-03-18 12:45:59 +05:30
energyLevel : entry.Activities?.activityType?.energyLevel
? {
2026-03-27 11:57:05 +05:30
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 ,
) ,
}
2026-03-18 12:45:59 +05:30
: null ,
2026-03-15 20:33:45 +05:30
media : await attachMediaWithPresignedUrl (
entry . Activities ? . ActivitiesMedia ? ? [ ] ,
) ,
} ,
} ;
} ) ,
) ;
const latestAddedActivity = formattedActivities [ 0 ] ? ? null ;
2026-03-18 12:45:59 +05:30
const formattedMasterEnergyLevels = await Promise . all (
energyLevel . map ( async ( item ) = > ( {
. . . item ,
energyIconPresignedUrl : await attachPresignedUrl ( item . energyIcon ) ,
} ) ) ,
) ;
2026-03-15 20:33:45 +05:30
return {
userLocation ,
travellerType ,
2026-03-18 12:45:59 +05:30
energyLevel : formattedMasterEnergyLevels ,
2026-03-15 20:33:45 +05:30
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 ) ,
} ;
}
2026-03-17 16:22:03 +05:30
2026-04-13 13:19:50 +05:30
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 } } ,
} ,
} ,
} ,
} ,
} ,
} ,
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 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' ] ) ? ?
{
items : [ ] ,
baseAmount : Number ( item . totalAmount ) || 0 ,
taxAmount : 0 ,
totalAmount : Number ( item . totalAmount ) || 0 ,
} ;
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 = sumCheckoutValues (
memberSelections . map ( ( selection ) = > selection . pricing . totalAmount ) ,
) ;
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 ,
} ;
}
2026-03-18 19:49:04 +05:30
async saveUserItinerary (
ownerXid : number ,
payload : {
title? : string ;
startDate : string ;
endDate : string ;
startTime : string ;
endTime : string ;
2026-04-08 15:57:36 +05:30
startLocationAddress : unknown ;
startLocationLat : number ;
startLocationLong : number ;
endLocationAddress : unknown ;
endLocationLat : number ;
endLocationLong : number ;
2026-03-18 19:49:04 +05:30
activities : Array < {
2026-04-01 16:09:04 +05:30
activityXid? : number ;
venueXid? : number ;
scheduleHeaderXid? : number ;
2026-03-18 19:49:04 +05:30
modeOfTravel : string ;
travelTimeBetweenPointsMins : number ;
kmForNextPoint? : number ;
2026-04-07 12:00:34 +05:30
startDate? : string ;
endDate? : string ;
2026-03-18 19:49:04 +05:30
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 ,
} ,
} ) ;
2026-04-08 15:57:36 +05:30
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 ,
} ,
} ) ;
2026-03-18 19:49:04 +05:30
const createdActivities = await Promise . all (
2026-04-08 19:15:47 +05:30
payload . activities . map ( async ( activityItem , activityIndex ) = > {
2026-04-01 16:09:04 +05:30
const itineraryType =
activityItem . itineraryType ? . trim ( ) . toUpperCase ( ) || 'ACTIVITY' ;
const isCustomItineraryType =
itineraryType === 'STAY' || itineraryType === 'FREE_TIME' ;
if ( isCustomItineraryType ) {
2026-04-07 12:00:34 +05:30
const customStartDate =
activityItem . startDate || activityItem . occurenceDate ;
const customEndDate =
activityItem . endDate || activityItem . occurenceDate ;
2026-04-01 16:09:04 +05:30
if (
2026-04-07 12:00:34 +05:30
! customStartDate ||
! customEndDate ||
2026-04-01 16:09:04 +05:30
! activityItem . selectedStartTime ||
! activityItem . selectedEndTime
) {
throw new ApiError (
400 ,
2026-04-07 12:00:34 +05:30
` ${ itineraryType } items must include startDate, endDate, selectedStartTime and selectedEndTime. ` ,
2026-04-01 16:09:04 +05:30
) ;
}
const customStartDateTime = combineDateAndTime (
2026-04-07 12:00:34 +05:30
customStartDate ,
2026-04-01 16:09:04 +05:30
activityItem . selectedStartTime ,
) ;
const customEndDateTime = combineDateAndTime (
2026-04-07 12:00:34 +05:30
customEndDate ,
2026-04-01 16:09:04 +05:30
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. ` ,
) ;
}
2026-04-02 16:03:12 +05:30
const customActivityData : Prisma.ItineraryActivitiesCreateInput = {
itineraryHeader : {
connect : { id : itineraryHeader.id } ,
} ,
2026-04-08 19:15:47 +05:30
displayOrder : activityIndex ,
2026-04-02 16:03:12 +05:30
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 ,
} ;
2026-04-01 16:09:04 +05:30
return tx . itineraryActivities . create ( {
2026-04-02 16:03:12 +05:30
data : customActivityData ,
2026-04-01 16:09:04 +05:30
select : {
id : true ,
2026-04-08 19:15:47 +05:30
displayOrder : true ,
2026-04-01 16:09:04 +05:30
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.' ,
) ;
}
2026-03-18 19:49:04 +05:30
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 [ ] ,
) ;
2026-04-02 16:03:12 +05:30
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 ) = >
2026-03-18 19:49:04 +05:30
getUniqueDatesForScheduleDetail (
{
occurenceDate : slot.occurenceDate ,
weekDay : slot.weekDay ,
dayOfMonth : slot.dayOfMonth ,
} ,
scheduleRangeStart ,
scheduleRangeEnd ,
2026-04-02 16:03:12 +05:30
) . map ( ( slotDate ) = > {
const slotStart = combineDateAndTime ( slotDate , slot . startTime ) ;
const slotEnd = slotStart
? combineDateAndTime ( slotDate , slot . endTime ) ? ?
( scheduleHeader . activity . activityDurationMins
2026-03-18 19:49:04 +05:30
? addMinutes (
2026-03-27 11:57:05 +05:30
slotStart ,
scheduleHeader . activity . activityDurationMins ,
)
2026-04-02 16:03:12 +05:30
: null )
: null ;
2026-03-18 19:49:04 +05:30
2026-04-02 16:03:12 +05:30
const normalizedSlotDate = formatDateKey ( slotDate ) ;
const normalizedSlotStartTime = normalizeTimeValue ( slot . startTime ) ;
const normalizedSlotEndTime = normalizeTimeValue ( slot . endTime ) ;
const cancellationKey = ` ${ normalizedSlotDate } | ${ slot . startTime } | ${ slot . endTime } ` ;
2026-03-18 19:49:04 +05:30
2026-04-02 16:03:12 +05:30
const mismatchReasons : string [ ] = [ ] ;
2026-03-18 19:49:04 +05:30
2026-04-02 16:03:12 +05:30
if ( ! slotStart ) {
mismatchReasons . push ( 'invalid_slot_start_time' ) ;
}
if ( ! slotEnd ) {
mismatchReasons . push ( 'invalid_slot_end_time' ) ;
}
2026-03-18 19:49:04 +05:30
2026-04-02 16:03:12 +05:30
if ( slotStart && slotEnd ) {
2026-03-18 19:49:04 +05:30
if (
slotStart < itineraryStartDateTime ||
slotEnd > itineraryEndDateTime
) {
2026-04-02 16:03:12 +05:30
mismatchReasons . push ( 'outside_itinerary_window' ) ;
2026-03-18 19:49:04 +05:30
}
2026-04-02 16:03:12 +05:30
}
2026-03-18 19:49:04 +05:30
2026-04-02 16:03:12 +05:30
if ( cancelledSlots . has ( cancellationKey ) ) {
mismatchReasons . push ( 'slot_cancelled' ) ;
}
2026-03-18 19:49:04 +05:30
2026-04-02 16:03:12 +05:30
if (
requestedOccurrenceDate &&
normalizedSlotDate !== requestedOccurrenceDate
) {
mismatchReasons . push ( 'occurrence_date_mismatch' ) ;
}
2026-03-18 19:49:04 +05:30
2026-04-02 16:03:12 +05:30
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 ,
} ,
} ;
} ) ,
2026-03-18 19:49:04 +05:30
) ;
2026-04-02 16:03:12 +05:30
const candidateSlots = expandedSlots
. filter ( ( slot ) = > slot . endDate && slot . debug . mismatchReasons . length === 0 )
. map ( ( { debug , . . . slot } ) = > slot ) ;
2026-03-18 19:49:04 +05:30
if ( ! candidateSlots . length ) {
2026-04-02 16:03:12 +05:30
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 ,
} ,
) ;
}
2026-03-18 19:49:04 +05:30
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 ] ! ;
2026-04-02 16:03:12 +05:30
const activityData : Prisma.ItineraryActivitiesCreateInput = {
itineraryHeader : {
connect : { id : itineraryHeader.id } ,
} ,
2026-04-08 19:15:47 +05:30
displayOrder : activityIndex ,
2026-04-02 16:03:12 +05:30
itineraryType ,
activity : {
connect : { id : activityItem.activityXid } ,
} ,
scheduledHeader : {
connect : { id : activityItem.scheduleHeaderXid } ,
2026-03-18 19:49:04 +05:30
} ,
2026-04-02 16:03:12 +05:30
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 ,
2026-03-18 19:49:04 +05:30
select : {
id : true ,
2026-04-08 19:15:47 +05:30
displayOrder : true ,
2026-03-18 19:49:04 +05:30
activityXid : true ,
scheduledHeaderXid : true ,
venueXid : true ,
occurenceDate : true ,
startTime : true ,
endTime : true ,
endDate : true ,
} ,
} ) ;
} ) ,
) ;
return {
itineraryHeaderXid : itineraryHeader.id ,
itineraryNo : itineraryHeader.itineraryNo ,
2026-04-08 14:57:49 +05:30
itineraryTitle : itineraryHeader.title ,
2026-03-18 19:49:04 +05:30
title : itineraryHeader.title ,
itineraryStatus : itineraryHeader.itineraryStatus ,
ownerMemberXid : ownerMember.id ,
2026-03-25 13:34:12 +05:30
membersCount : 1 ,
2026-03-18 19:49:04 +05:30
activitiesCount : createdActivities.length ,
members : [
{
id : ownerMember.id ,
memberXid : ownerMember.memberXid ,
memberRole : ownerMember.memberRole ,
memberStatus : ownerMember.memberStatus ,
} ,
] ,
2026-04-08 15:57:36 +05:30
startLocationDetails ,
endLocationDetails ,
2026-03-18 19:49:04 +05:30
activities : createdActivities ,
} ;
} ) ;
}
2026-04-08 14:57:49 +05:30
async getAllUserSavedItineraries (
userXid : number ,
itineraryHeaderXid? : number ,
) {
2026-03-18 19:49:04 +05:30
const itineraries = await this . prisma . itineraryHeader . findMany ( {
where : {
2026-04-08 14:57:49 +05:30
. . . ( itineraryHeaderXid ? { id : itineraryHeaderXid } : { } ) ,
2026-03-18 19:49:04 +05:30
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 ,
} ,
} ,
} ,
} ,
2026-04-08 15:57:36 +05:30
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 ,
} ,
} ,
2026-03-18 19:49:04 +05:30
ItineraryActivities : {
where : {
isActive : true ,
deletedAt : null ,
} ,
orderBy : [
2026-04-08 19:15:47 +05:30
{ displayOrder : 'asc' } ,
2026-03-18 19:49:04 +05:30
{ createdAt : 'asc' } ,
] ,
select : {
id : true ,
2026-04-08 19:15:47 +05:30
displayOrder : true ,
2026-03-18 19:49:04 +05:30
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 ,
2026-04-07 19:13:06 +05:30
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 ,
} ,
} ,
} ,
} ,
} ,
} ,
2026-03-18 19:49:04 +05:30
activity : {
select : {
id : true ,
activityTitle : true ,
activityDescription : true ,
activityDurationMins : true ,
checkInAddress : true ,
checkInLat : true ,
checkInLong : true ,
2026-04-10 17:48:20 +05:30
checkInCity : {
select : {
id : true ,
cityName : true ,
} ,
} ,
checkOutAddress : true ,
checkOutLat : true ,
checkOutLong : true ,
checkOutCity : {
select : {
id : true ,
cityName : true ,
} ,
} ,
2026-03-27 11:57:05 +05:30
foodAvailable : true ,
foodIsChargeable : true ,
trainerAvailable : true ,
trainerIsChargeable : true ,
inActivityAvailable : true ,
inActivityIsChargeable : true ,
pickUpDropAvailable : true ,
pickUpDropIsChargeable : true ,
2026-04-07 19:13:06 +05:30
equipmentAvailable : true ,
equipmentIsChargeable : true ,
2026-03-27 11:57:05 +05:30
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 ,
} ,
} ,
2026-04-07 19:13:06 +05:30
ActivityEquipments : {
where : {
isActive : true ,
deletedAt : null ,
} ,
select : {
id : true ,
equipmentName : true ,
isEquipmentChargeable : true ,
equipmentBasePrice : true ,
equipmentTotalPrice : true ,
} ,
} ,
2026-03-27 11:57:05 +05:30
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 ,
} ,
} ,
} ,
} ,
2026-04-10 18:48:50 +05:30
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 ,
} ,
} ,
} ,
} ,
2026-03-18 19:49:04 +05:30
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 ) = > {
2026-03-27 11:57:05 +05:30
const ownerFullName = ` ${ itinerary . owner . firstName ? ? '' } ${ itinerary . owner . lastName ? ? ''
} ` .trim();
2026-03-18 19:49:04 +05:30
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 ,
2026-03-27 11:57:05 +05:30
fullName : ` ${ member . member . firstName ? ? '' } ${ member . member . lastName ? ? ''
} ` .trim(),
2026-03-18 19:49:04 +05:30
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 ) = > {
2026-04-01 16:09:04 +05:30
const coverImage = item . activity
? item . activity . ActivitiesMedia . find ( ( media ) = > media . isCoverImage ) ? ?
2026-03-18 19:49:04 +05:30
item . activity . ActivitiesMedia [ 0 ] ? ?
2026-04-01 16:09:04 +05:30
null
: null ;
2026-04-07 19:13:06 +05:30
const userSelection = item . itineraryActivitySelections [ 0 ] ? ? null ;
2026-04-10 18:48:50 +05:30
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 ,
) ;
2026-03-18 19:49:04 +05:30
return {
id : item.id ,
2026-04-08 19:15:47 +05:30
displayOrder : item.displayOrder ,
2026-03-18 19:49:04 +05:30
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 ,
2026-04-01 16:09:04 +05:30
activity : item.activity
? {
id : item.activity.id ,
activityTitle : item.activity.activityTitle ,
activityDescription : item.activity.activityDescription ,
activityDurationMins : item.activity.activityDurationMins ,
2026-04-10 18:48:50 +05:30
activityPrice : activityPriceDetails?.sellPrice ? ? null ,
activityPriceDetails ,
2026-04-01 16:09:04 +05:30
checkInAddress : item.activity.checkInAddress ,
checkInLat : item.activity.checkInLat ,
checkInLong : item.activity.checkInLong ,
2026-04-10 17:48:20 +05:30
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 ,
2026-04-01 16:09:04 +05:30
coverImage : coverImage?.mediaFileName ? ? null ,
coverImagePresignedUrl : await attachPresignedUrl (
coverImage ? . mediaFileName ,
2026-03-27 11:57:05 +05:30
) ,
2026-04-01 16:09:04 +05:30
media : await attachMediaWithPresignedUrl (
item . activity . ActivitiesMedia ,
2026-03-27 11:57:05 +05:30
) ,
2026-04-01 16:09:04 +05:30
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 ,
2026-03-27 11:57:05 +05:30
} ) ,
) ,
2026-04-01 16:09:04 +05:30
} ,
2026-04-07 19:13:06 +05:30
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 ,
} ) ) ,
} ,
2026-04-01 16:09:04 +05:30
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 ,
2026-04-10 17:48:20 +05:30
cityName : pickUpDetail.isPickUp
? item . activity . checkInCity ? . cityName ? ? null
: item . activity . checkOutCity ? . cityName ? ? null ,
2026-04-01 16:09:04 +05:30
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 ,
2026-04-07 19:13:06 +05:30
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 ,
2026-03-18 19:49:04 +05:30
venue : item.venue ,
scheduleHeader : item.scheduledHeader ,
} ;
} ) ,
) ;
2026-04-08 15:57:36 +05:30
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 ,
} ) ,
) ;
2026-03-18 19:49:04 +05:30
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 ,
2026-04-08 15:57:36 +05:30
startStopDetails ,
2026-03-18 19:49:04 +05:30
activities ,
} ;
} ) ,
) ;
return {
count : formattedItineraries.length ,
itineraries : formattedItineraries ,
} ;
}
2026-04-13 14:00:50 +05:30
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 ,
} ,
} ,
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 ,
} ,
} ,
} ,
} ,
} ,
} ,
} ,
} ) ;
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 ) = > {
if (
activity . itineraryType !== 'ACTIVITY' ||
! activity . activityXid ||
! activity . scheduledHeaderXid ||
! activity . scheduledHeader
) {
return {
itineraryActivityXid : activity.id ,
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 ,
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 ,
skipped : false ,
alreadyBooked : false ,
checkInCode : detail.offlineCode ,
bookedSeats ,
remainingCapacity : refreshedSlot?.maxCapacity ? ? null ,
} ;
} ) ,
) ;
return {
itineraryHeaderXid : itinerary.id ,
itineraryNo : itinerary.itineraryNo ,
itineraryTitle : itinerary.title ,
bookedActivities ,
} ;
}
2026-04-07 19:13:06 +05:30
async saveItineraryActivitySelections (
userXid : number ,
2026-04-13 13:38:17 +05:30
payload : Array < {
2026-04-07 19:13:06 +05:30
itineraryActivityXid : number ;
isFoodOpted? : boolean ;
selectedFoodTypeIds? : number [ ] ;
isTrainerOpted? : boolean ;
isInActivityNavigationOpted? : boolean ;
selectedNavigationModeXid? : number | null ;
selectedEquipmentIds? : number [ ] ;
2026-04-13 13:38:17 +05:30
} > ,
2026-04-07 19:13:06 +05:30
) {
return this . prisma . $transaction ( async ( tx ) = > {
2026-04-13 13:38:17 +05:30
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 ,
} ,
} ,
2026-04-07 19:13:06 +05:30
} ,
} ,
select : {
id : true ,
2026-04-13 13:38:17 +05:30
itineraryHeaderXid : true ,
itineraryType : true ,
activityXid : true ,
activity : {
2026-04-07 19:13:06 +05:30
select : {
id : true ,
2026-04-13 13:38:17 +05:30
foodAvailable : true ,
trainerAvailable : true ,
inActivityAvailable : true ,
equipmentAvailable : true ,
activityFoodTypes : {
where : {
isActive : true ,
deletedAt : null ,
} ,
2026-04-07 19:13:06 +05:30
select : {
id : true ,
2026-04-13 13:38:17 +05:30
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 ,
2026-04-07 19:13:06 +05:30
} ,
} ,
} ,
} ,
} ,
2026-04-13 13:38:17 +05:30
} ) ;
2026-04-07 19:13:06 +05:30
2026-04-13 13:38:17 +05:30
if ( ! itineraryActivity ) {
throw new ApiError (
404 ,
2026-04-13 15:33:27 +05:30
` activities[ ${ index } ] with itineraryActivityXid= ${ item . itineraryActivityXid } was not found. ` ,
2026-04-13 13:38:17 +05:30
) ;
}
2026-04-07 19:13:06 +05:30
2026-04-13 15:33:27 +05:30
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 ) {
2026-04-13 13:38:17 +05:30
throw new ApiError (
400 ,
2026-04-13 15:33:27 +05:30
` activities[ ${ index } ] with itineraryActivityXid= ${ item . itineraryActivityXid } could not load linked activity details. ` ,
2026-04-13 13:38:17 +05:30
) ;
}
2026-04-07 19:13:06 +05:30
2026-04-13 13:38:17 +05:30
const itineraryMember = await tx . itineraryMembers . findFirst ( {
where : {
itineraryHeaderXid : itineraryActivity.itineraryHeaderXid ,
memberXid : userXid ,
isActive : true ,
deletedAt : null ,
} ,
select : {
id : true ,
} ,
} ) ;
2026-04-07 19:13:06 +05:30
2026-04-13 13:38:17 +05:30
if ( ! itineraryMember ) {
throw new ApiError ( 404 , 'Itinerary member record not found.' ) ;
}
2026-04-07 19:13:06 +05:30
2026-04-13 13:38:17 +05:30
if ( selectedEquipmentIds . some ( ( id ) = > ! Number . isInteger ( id ) || id <= 0 ) ) {
throw new ApiError (
400 ,
` activities[ ${ index } ].selectedEquipmentIds must contain valid ids. ` ,
) ;
}
2026-04-07 19:13:06 +05:30
2026-04-13 13:38:17 +05:30
if ( selectedFoodTypeIds . some ( ( id ) = > ! Number . isInteger ( id ) || id <= 0 ) ) {
throw new ApiError (
400 ,
` activities[ ${ index } ].selectedFoodTypeIds must contain valid ids. ` ,
) ;
}
2026-04-07 19:13:06 +05:30
2026-04-13 13:38:17 +05:30
const isFoodOpted = Boolean ( item . isFoodOpted ) ;
const isTrainerOpted = Boolean ( item . isTrainerOpted ) ;
const isInActivityNavigationOpted = Boolean (
item . isInActivityNavigationOpted ,
2026-04-07 19:13:06 +05:30
) ;
2026-04-13 13:38:17 +05:30
const selectedNavigationModeXid =
item . selectedNavigationModeXid === undefined ||
item . selectedNavigationModeXid === null
? null
: Number ( item . selectedNavigationModeXid ) ;
2026-04-07 19:13:06 +05:30
2026-04-13 13:38:17 +05:30
if (
selectedNavigationModeXid !== null &&
( ! Number . isInteger ( selectedNavigationModeXid ) ||
selectedNavigationModeXid <= 0 )
) {
throw new ApiError (
400 ,
` activities[ ${ index } ].selectedNavigationModeXid must be a valid id. ` ,
) ;
}
2026-04-07 19:13:06 +05:30
2026-04-13 15:55:40 +05:30
const availableFoodTypes = itineraryActivity . activity . activityFoodTypes . map ( ( entry ) = > ( {
id : entry.id ,
name : entry.foodType.foodTypeName ,
} ) ) ;
2026-04-13 13:38:17 +05:30
const availableFoodTypeIds = new Set (
2026-04-13 15:55:40 +05:30
availableFoodTypes . map ( ( entry ) = > entry . id ) ,
2026-04-07 19:13:06 +05:30
) ;
2026-04-13 13:38:17 +05:30
const availableNavigationModeIds = new Set (
itineraryActivity . activity . ActivityNavigationModes . map ( ( entry ) = > entry . id ) ,
2026-04-07 19:13:06 +05:30
) ;
2026-04-13 15:55:40 +05:30
const availableEquipments = itineraryActivity . activity . ActivityEquipments . map ( ( entry ) = > ( {
id : entry.id ,
name : entry.equipmentName ,
} ) ) ;
2026-04-13 13:38:17 +05:30
const availableEquipmentIds = new Set (
2026-04-13 15:55:40 +05:30
availableEquipments . map ( ( entry ) = > entry . id ) ,
2026-04-07 19:13:06 +05:30
) ;
2026-04-13 13:38:17 +05:30
if ( isFoodOpted ) {
if ( ! itineraryActivity . activity . foodAvailable ) {
throw new ApiError ( 400 , ` activities[ ${ index } ]: Food is not available for this activity. ` ) ;
}
2026-04-07 19:13:06 +05:30
2026-04-13 13:38:17 +05:30
if (
itineraryActivity . activity . activityFoodTypes . length > 0 &&
! selectedFoodTypeIds . length
) {
throw new ApiError (
400 ,
` activities[ ${ index } ].selectedFoodTypeIds is required when food is opted. ` ,
) ;
}
2026-04-07 19:13:06 +05:30
2026-04-13 15:55:40 +05:30
const invalidFoodTypeIds = selectedFoodTypeIds . filter (
( id ) = > ! availableFoodTypeIds . has ( id ) ,
) ;
if ( invalidFoodTypeIds . length ) {
2026-04-13 13:38:17 +05:30
throw new ApiError (
400 ,
2026-04-13 15:55:40 +05:30
` 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' } . ` ,
2026-04-13 13:38:17 +05:30
) ;
}
} else if ( selectedFoodTypeIds . length ) {
throw new ApiError (
400 ,
` activities[ ${ index } ].selectedFoodTypeIds cannot be sent when food is not opted. ` ,
) ;
}
2026-04-07 19:13:06 +05:30
2026-04-13 13:38:17 +05:30
if ( isTrainerOpted && ! itineraryActivity . activity . trainerAvailable ) {
throw new ApiError (
400 ,
` activities[ ${ index } ]: Trainer is not available for this activity. ` ,
) ;
}
2026-04-07 19:13:06 +05:30
2026-04-13 13:38:17 +05:30
if ( isInActivityNavigationOpted ) {
if ( ! itineraryActivity . activity . inActivityAvailable ) {
throw new ApiError (
400 ,
` activities[ ${ index } ]: In-activity navigation is not available for this activity. ` ,
) ;
}
2026-04-07 19:13:06 +05:30
2026-04-13 13:38:17 +05:30
if (
itineraryActivity . activity . ActivityNavigationModes . length > 0 &&
! selectedNavigationModeXid
) {
throw new ApiError (
400 ,
` activities[ ${ index } ].selectedNavigationModeXid is required when navigation is opted. ` ,
) ;
}
2026-04-07 19:13:06 +05:30
2026-04-13 13:38:17 +05:30
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. ` ,
) ;
}
2026-04-07 19:13:06 +05:30
2026-04-13 13:38:17 +05:30
if ( selectedEquipmentIds . length ) {
if ( ! itineraryActivity . activity . equipmentAvailable ) {
throw new ApiError (
400 ,
` activities[ ${ index } ]: Equipment is not available for this activity. ` ,
) ;
}
2026-04-13 15:55:40 +05:30
const invalidEquipmentIds = selectedEquipmentIds . filter (
( id ) = > ! availableEquipmentIds . has ( id ) ,
) ;
if ( invalidEquipmentIds . length ) {
2026-04-13 13:38:17 +05:30
throw new ApiError (
400 ,
2026-04-13 15:55:40 +05:30
` 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' } . ` ,
2026-04-13 13:38:17 +05:30
) ;
}
}
const selection = await tx . itineraryActivitySelection . upsert ( {
2026-04-07 19:13:06 +05:30
where : {
2026-04-13 13:38:17 +05:30
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 ,
2026-04-07 19:13:06 +05:30
isActive : true ,
deletedAt : null ,
} ,
select : {
id : true ,
2026-04-13 13:38:17 +05:30
} ,
} ) ;
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 ,
} ,
2026-04-07 19:13:06 +05:30
select : {
id : true ,
2026-04-13 13:38:17 +05:30
activityFoodTypeXid : true ,
activityFoodType : {
2026-04-07 19:13:06 +05:30
select : {
id : true ,
2026-04-13 13:38:17 +05:30
foodTypeXid : true ,
foodType : {
select : {
id : true ,
foodTypeName : true ,
} ,
} ,
2026-04-07 19:13:06 +05:30
} ,
} ,
} ,
} ,
2026-04-13 13:38:17 +05:30
activityNavigationMode : {
2026-04-07 19:13:06 +05:30
select : {
id : true ,
2026-04-13 13:38:17 +05:30
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 ,
} ,
} ,
2026-04-07 19:13:06 +05:30
} ,
} ,
} ,
2026-04-13 13:38:17 +05:30
} ) ;
} ) ,
) ;
return result ;
2026-04-07 19:13:06 +05:30
} ) ;
}
2026-03-17 16:22:03 +05:30
async getMatchingBucketInterestedActivities (
userXid : number ,
payload : {
userLat : number ;
userLong : number ;
startDate : string ;
endDate : string ;
startTime : string ;
endTime : string ;
2026-03-18 11:09:57 +05:30
energyLevelXid? : number ;
2026-03-17 16:22:03 +05:30
entryTypeXid : number ;
2026-03-25 13:34:12 +05:30
groupCount? : number ;
2026-03-17 16:22:03 +05:30
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.' ,
) ;
}
2026-03-25 13:34:12 +05:30
if ( payload . groupCount !== undefined ) {
if ( ! Number . isInteger ( payload . groupCount ) || payload . groupCount <= 0 ) {
throw new ApiError ( 400 , 'groupCount must be a positive integer.' ) ;
}
}
2026-03-17 16:22:03 +05:30
const rangeStartDay = startOfDay ( requestedStart ) ;
const rangeEndDay = startOfDay ( requestedEnd ) ;
2026-03-25 13:34:12 +05:30
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.' ,
) ;
}
2026-03-17 16:22:03 +05:30
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 ,
2026-03-18 11:09:57 +05:30
. . . ( payload . energyLevelXid !== undefined
? { energyLevelXid : payload.energyLevelXid }
: { } ) ,
2026-03-17 16:22:03 +05:30
} ,
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 ,
2026-03-27 11:57:05 +05:30
activityDescription : true ,
2026-03-17 16:22:03 +05:30
checkInLat : true ,
checkInLong : true ,
checkInAddress : true ,
2026-03-18 13:21:32 +05:30
checkInCity : {
select : {
id : true ,
cityName : true ,
} ,
} ,
checkInState : {
select : {
id : true ,
stateName : true ,
} ,
} ,
checkInCountry : {
select : {
id : true ,
countryName : true ,
countryCode : true ,
} ,
} ,
2026-03-17 16:22:03 +05:30
activityType : {
select : {
2026-03-18 13:21:32 +05:30
activityTypeName : true ,
interestXid : true ,
interests : {
select : {
id : true ,
interestName : true ,
} ,
} ,
2026-03-17 16:22:03 +05:30
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 ,
2026-04-10 17:55:16 +05:30
ActivityPrices : {
where : {
isActive : true ,
deletedAt : null ,
} ,
orderBy : {
createdAt : 'asc' ,
} ,
select : {
id : true ,
noOfSession : true ,
isPackage : true ,
sessionValidity : true ,
sessionValidityFrequency : true ,
basePrice : true ,
sellPrice : true ,
} ,
} ,
2026-03-27 11:57:05 +05:30
ActivityVenueArtifacts : {
where : {
isActive : true ,
deletedAt : null ,
} ,
select : {
id : true ,
mediaType : true ,
mediaFileName : true ,
} ,
} ,
2026-03-17 16:22:03 +05:30
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 ,
2026-03-25 13:34:12 +05:30
maxCapacity :
isGroupEntryType && payload . groupCount !== undefined
? { gte : payload.groupCount }
: { gt : 0 } ,
2026-03-17 16:22:03 +05:30
} ,
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 ;
}
2026-03-25 13:34:12 +05:30
if (
isGroupEntryType &&
payload . groupCount !== undefined &&
slot . maxCapacity < payload . groupCount
) {
return null ;
}
2026-03-17 16:22:03 +05:30
return {
scheduleHeaderXid : header.id ,
slotId : slot.id ,
venueXid : venue.id ,
venueName : venue.venueName ,
venueLabel : venue.venueLabel ,
2026-04-10 17:55:16 +05:30
activityPrices : venue.ActivityPrices ,
2026-03-27 11:57:05 +05:30
mediaFileName : venue.ActivityVenueArtifacts [ 0 ] ? . mediaFileName ? ? null ,
mediaType : venue.ActivityVenueArtifacts [ 0 ] ? . mediaType ? ? null ,
2026-03-17 16:22:03 +05:30
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 ) ;
} ) ;
} ) ,
) ;
2026-03-27 11:57:05 +05:30
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 ) {
2026-03-17 16:22:03 +05:30
return null ;
}
2026-03-27 11:57:05 +05:30
availableSlotsWithPresignedUrl . sort (
2026-03-17 16:22:03 +05:30
( first , second ) = >
new Date ( first ! . startDateTime ) . getTime ( ) -
new Date ( second ! . startDateTime ) . getTime ( ) ,
) ;
2026-04-10 17:55:16 +05:30
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 ,
) ;
2026-03-17 16:22:03 +05:30
const coverImage =
activity . ActivitiesMedia . find ( ( media ) = > media . isCoverImage ) ? ?
activity . ActivitiesMedia [ 0 ] ? ?
null ;
const energyLevel = activity . activityType ? . energyLevel ? ? null ;
2026-04-10 17:55:16 +05:30
const lowestActivityPrice = lowestPriceVenue ? . activityPrice ? ? null ;
const lowestActivityBasePrice = lowestPriceVenue ? . activityBasePrice ? ? null ;
const lowestActivityPriceDetails = lowestPriceVenue ? . activityPriceDetails ? ? null ;
2026-03-17 16:22:03 +05:30
return {
userBucketInterestedXid : entry.id ,
activityXid : entry.activityXid ,
isBucket : entry.isBucket ,
bucketTypeName : entry.bucketTypeName ,
distance ,
2026-04-10 17:55:16 +05:30
activityPrice : lowestActivityPrice ,
activityBasePrice : lowestActivityBasePrice ,
activityPriceDetails : lowestActivityPriceDetails ,
lowestActivityPrice ,
lowestActivityBasePrice ,
lowestActivityPriceDetails ,
2026-03-17 16:22:03 +05:30
activityTitle : activity.activityTitle ,
2026-03-27 11:57:05 +05:30
activityDescription : activity.activityDescription ,
2026-03-17 16:22:03 +05:30
activityDurationMins ,
activityCoverImage : coverImage?.mediaFileName ? ? null ,
activityCoverImagePresignedUrl : await attachPresignedUrl (
coverImage ? . mediaFileName ,
) ,
2026-04-10 17:55:16 +05:30
venue : lowestPriceVenue
2026-03-17 16:22:03 +05:30
? {
2026-04-10 17:55:16 +05:30
venueXid : lowestPriceVenue.venueXid ,
venueName : lowestPriceVenue.venueName ,
venueLabel : lowestPriceVenue.venueLabel ,
venueCapacity : lowestPriceVenue.venueCapacity ,
availableSeats : lowestPriceVenue.availableSeats ,
activityPrice : lowestPriceVenue.activityPrice ,
activityBasePrice : lowestPriceVenue.activityBasePrice ,
activityPriceDetails : lowestPriceVenue.activityPriceDetails ,
2026-03-27 11:57:05 +05:30
}
2026-03-17 16:22:03 +05:30
: null ,
2026-04-10 17:55:16 +05:30
venuePrices : venuePriceDetails ,
2026-03-27 11:57:05 +05:30
availableSlots : availableSlotsWithPresignedUrl ,
2026-03-17 16:22:03 +05:30
entryType : activity.ActivityAllowedEntry [ 0 ] ? . allowedEntryType ? ? null ,
energyLevel : energyLevel
? {
2026-03-27 11:57:05 +05:30
energyLevelXid : energyLevel.id ,
energyLevelName : energyLevel.energyLevelName ,
energyLevelIcon : energyLevel.energyIcon ,
energyLevelIconPresignedUrl : await attachPresignedUrl (
energyLevel . energyIcon ,
) ,
}
2026-03-17 16:22:03 +05:30
: null ,
checkInAddress : activity.checkInAddress ,
2026-03-18 13:21:32 +05:30
checkInCity : activity.checkInCity ,
checkInState : activity.checkInState ,
checkInCountry : activity.checkInCountry ,
2026-03-25 12:36:47 +05:30
checkInLat : activity.checkInLat ,
checkInLong : activity.checkInLong ,
2026-03-18 13:21:32 +05:30
interest : activity.activityType?.interests
? {
2026-03-27 11:57:05 +05:30
id : activity.activityType.interests.id ,
interestName : activity.activityType.interests.interestName ,
}
2026-03-18 13:21:32 +05:30
: null ,
2026-03-17 16:22:03 +05:30
} ;
} ) ,
) ;
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 ;
} ) ;
2026-03-18 13:21:32 +05:30
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' ) ;
2026-03-17 16:22:03 +05:30
const totalCount = activities . length ;
const sanitizedLimit = Math . min ( Math . max ( payload . limit , 1 ) , 20 ) ;
const sanitizedPage = Math . max ( 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 ,
2026-03-25 13:34:12 +05:30
groupCount : payload.groupCount ,
2026-03-17 16:22:03 +05:30
page : sanitizedPage ,
limit : sanitizedLimit ,
2026-03-18 13:21:32 +05:30
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 ,
2026-03-17 16:22:03 +05:30
} ,
pagination : {
page : sanitizedPage ,
limit : sanitizedLimit ,
totalCount ,
totalPages ,
hasNextPage : sanitizedPage < totalPages ,
hasPreviousPage : sanitizedPage > 1 && totalPages > 0 ,
} ,
count : paginatedActivities.length ,
2026-04-10 17:55:16 +05:30
activityCount : totalCount ,
2026-03-17 16:22:03 +05:30
totalCount ,
activities : paginatedActivities ,
} ;
}
2026-03-15 20:33:45 +05:30
}