made the api for storing the selections for the itinerary of the user

This commit is contained in:
2026-04-07 19:13:06 +05:30
parent fcac64e0a9
commit a44321044f
4 changed files with 680 additions and 0 deletions

View File

@@ -1369,6 +1369,7 @@ model ActivityFoodTypes {
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
itineraryActivitySelectionFoodTypes ItineraryActivitySelectionFoodType[]
@@map("activity_food_types")
@@schema("act")
@@ -1419,6 +1420,7 @@ model ActivityEquipments {
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
ActivityEquipmentTaxes ActivityEquipmentTaxes[]
itineraryActivitySelectionEquipments ItineraryActivitySelectionEquipment[]
@@map("activity_equipments")
@@schema("act")
@@ -1454,6 +1456,7 @@ model ActivityNavigationModes {
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
ActivityNavigationModesTaxes ActivityNavigationModesTaxes[]
ItineraryActivitySelections ItineraryActivitySelection[]
@@map("activity_navigation_modes")
@@schema("act")
@@ -1680,6 +1683,7 @@ model ItineraryMembers {
User User? @relation(fields: [userId], references: [id])
userId Int?
ItineraryDetails ItineraryDetails[]
itineraryActivitySelections ItineraryActivitySelection[]
@@map("itinerary_members")
@@schema("itn")
@@ -1740,11 +1744,67 @@ model ItineraryActivities {
ActivitySOSDetails ActivitySOSDetails[]
ActivityFeedbacks ActivityFeedbacks[]
ItineraryDetails ItineraryDetails[]
itineraryActivitySelections ItineraryActivitySelection[]
@@map("itinerary_activities")
@@schema("itn")
}
model ItineraryActivitySelection {
id Int @id @default(autoincrement())
itineraryActivityXid Int @map("itinerary_activity_xid")
itineraryActivity ItineraryActivities @relation(fields: [itineraryActivityXid], references: [id], onDelete: Cascade)
itineraryMemberXid Int @map("itinerary_member_xid")
itineraryMember ItineraryMembers @relation(fields: [itineraryMemberXid], references: [id], onDelete: Cascade)
isFoodOpted Boolean @default(false) @map("is_food_opted")
isTrainerOpted Boolean @default(false) @map("is_trainer_opted")
isInActivityNavigationOpted Boolean @default(false) @map("is_in_activity_navigation_opted")
activityNavigationModeXid Int? @map("activity_navigation_mode_xid")
activityNavigationMode ActivityNavigationModes? @relation(fields: [activityNavigationModeXid], references: [id], onDelete: Restrict)
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
selectedFoodTypes ItineraryActivitySelectionFoodType[]
selectedEquipments ItineraryActivitySelectionEquipment[]
@@unique([itineraryActivityXid, itineraryMemberXid])
@@map("itinerary_activity_selection")
@@schema("itn")
}
model ItineraryActivitySelectionFoodType {
id Int @id @default(autoincrement())
itineraryActivitySelectionXid Int @map("itinerary_activity_selection_xid")
itineraryActivitySelection ItineraryActivitySelection @relation(fields: [itineraryActivitySelectionXid], references: [id], onDelete: Cascade)
activityFoodTypeXid Int @map("activity_food_type_xid")
activityFoodType ActivityFoodTypes @relation(fields: [activityFoodTypeXid], references: [id], onDelete: Cascade)
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
@@unique([itineraryActivitySelectionXid, activityFoodTypeXid])
@@map("itinerary_activity_selection_food_type")
@@schema("itn")
}
model ItineraryActivitySelectionEquipment {
id Int @id @default(autoincrement())
itineraryActivitySelectionXid Int @map("itinerary_activity_selection_xid")
itineraryActivitySelection ItineraryActivitySelection @relation(fields: [itineraryActivitySelectionXid], references: [id], onDelete: Cascade)
activityEquipmentXid Int @map("activity_equipment_xid")
activityEquipment ActivityEquipments @relation(fields: [activityEquipmentXid], references: [id], onDelete: Cascade)
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
@@unique([itineraryActivitySelectionXid, activityEquipmentXid])
@@map("itinerary_activity_selection_equipment")
@@schema("itn")
}
model ActivitySOSDetails {
id Int @id @default(autoincrement())
itineraryActivityXid Int @map("itinerary_activity_xid")

View File

@@ -453,6 +453,21 @@ saveUserItinerary:
path: /itinerary/save-user-itinerary
method: post
saveItineraryActivitySelections:
handler: src/modules/user/handlers/itinerary/saveItineraryActivitySelections.handler
memorySize: 512
package:
patterns:
- 'src/modules/user/**'
- ${file(./serverless/patterns/base.yml):pattern1}
- ${file(./serverless/patterns/base.yml):pattern2}
- ${file(./serverless/patterns/base.yml):pattern3}
- ${file(./serverless/patterns/base.yml):pattern4}
events:
- httpApi:
path: /itinerary/save-itinerary-activity-selections
method: post
getAllUserSavedItineraries:
handler: src/modules/user/handlers/itinerary/getAllUserSavedItineraries.handler
memorySize: 512

View File

@@ -0,0 +1,98 @@
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
import { verifyUserToken } from '../../../../common/middlewares/jwt/authForUser';
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
import ApiError from '../../../../common/utils/helper/ApiError';
import { ItineraryService } from '../../services/itinerary.service';
const itineraryService = new ItineraryService(prismaClient);
export const handler = safeHandler(async (
event: APIGatewayProxyEvent,
context?: Context,
): Promise<APIGatewayProxyResult> => {
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
if (!token) {
throw new ApiError(
400,
'This is a protected route. Please provide a valid token.',
);
}
const userInfo = await verifyUserToken(token);
const userId = Number(userInfo.id);
if (!userId || Number.isNaN(userId)) {
throw new ApiError(400, 'Invalid user ID');
}
let body: Record<string, any> = {};
if (event.body) {
try {
body = JSON.parse(event.body);
} catch {
throw new ApiError(400, 'Invalid JSON body');
}
}
const itineraryActivityXid = Number(body.itineraryActivityXid);
if (!Number.isInteger(itineraryActivityXid) || itineraryActivityXid <= 0) {
throw new ApiError(400, 'itineraryActivityXid is required.');
}
const selectedEquipmentIds = Array.isArray(body.selectedEquipmentIds)
? body.selectedEquipmentIds.map((id: unknown) => Number(id))
: [];
const selectedFoodTypeIds = Array.isArray(body.selectedFoodTypeIds)
? body.selectedFoodTypeIds.map((id: unknown) => Number(id))
: [];
if (selectedEquipmentIds.some((id) => !Number.isInteger(id) || id <= 0)) {
throw new ApiError(400, 'selectedEquipmentIds must contain valid ids.');
}
if (selectedFoodTypeIds.some((id) => !Number.isInteger(id) || id <= 0)) {
throw new ApiError(400, 'selectedFoodTypeIds must contain valid ids.');
}
const toOptionalId = (value: unknown) => {
if (value === undefined || value === null || value === '') {
return null;
}
const parsed = Number(value);
if (!Number.isInteger(parsed) || parsed <= 0) {
throw new ApiError(400, 'One or more selected option ids are invalid.');
}
return parsed;
};
const result = await itineraryService.saveItineraryActivitySelections(userId, {
itineraryActivityXid,
isFoodOpted:
body.isFoodOpted === undefined ? false : Boolean(body.isFoodOpted),
selectedFoodTypeIds,
isTrainerOpted:
body.isTrainerOpted === undefined ? false : Boolean(body.isTrainerOpted),
isInActivityNavigationOpted:
body.isInActivityNavigationOpted === undefined
? false
: Boolean(body.isInActivityNavigationOpted),
selectedNavigationModeXid: toOptionalId(body.selectedNavigationModeXid),
selectedEquipmentIds,
});
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
success: true,
message: 'Itinerary activity selections saved successfully.',
data: result,
}),
};
});

View File

@@ -1058,6 +1058,76 @@ export class ItineraryService {
paxCount: true,
totalAmount: true,
bookingStatus: true,
itineraryActivitySelections: {
where: {
isActive: true,
deletedAt: null,
itineraryMember: {
memberXid: userXid,
isActive: true,
deletedAt: null,
},
},
take: 1,
select: {
id: true,
itineraryMemberXid: true,
isFoodOpted: true,
isTrainerOpted: true,
isInActivityNavigationOpted: true,
activityNavigationModeXid: true,
selectedFoodTypes: {
where: {
isActive: true,
deletedAt: null,
},
select: {
id: true,
activityFoodTypeXid: true,
activityFoodType: {
select: {
id: true,
foodTypeXid: true,
foodType: {
select: {
id: true,
foodTypeName: true,
},
},
},
},
},
},
activityNavigationMode: {
select: {
id: true,
navigationModeName: true,
isInActivityChargeable: true,
navigationModesBasePrice: true,
navigationModesTotalPrice: true,
},
},
selectedEquipments: {
where: {
isActive: true,
deletedAt: null,
},
select: {
id: true,
activityEquipmentXid: true,
activityEquipment: {
select: {
id: true,
equipmentName: true,
isEquipmentChargeable: true,
equipmentBasePrice: true,
equipmentTotalPrice: true,
},
},
},
},
},
},
activity: {
select: {
id: true,
@@ -1075,6 +1145,8 @@ export class ItineraryService {
inActivityIsChargeable: true,
pickUpDropAvailable: true,
pickUpDropIsChargeable: true,
equipmentAvailable: true,
equipmentIsChargeable: true,
activityFoodTypes: {
where: {
isActive: true,
@@ -1126,6 +1198,19 @@ export class ItineraryService {
navigationModesTotalPrice: true,
},
},
ActivityEquipments: {
where: {
isActive: true,
deletedAt: null,
},
select: {
id: true,
equipmentName: true,
isEquipmentChargeable: true,
equipmentBasePrice: true,
equipmentTotalPrice: true,
},
},
ActivityPickUpDetails: {
where: {
isActive: true,
@@ -1228,6 +1313,7 @@ export class ItineraryService {
item.activity.ActivitiesMedia[0] ??
null
: null;
const userSelection = item.itineraryActivitySelections[0] ?? null;
return {
id: item.id,
@@ -1303,6 +1389,17 @@ export class ItineraryService {
}),
),
},
equipmentDetails: {
equipmentAvailable: item.activity.equipmentAvailable,
equipmentIsChargeable: item.activity.equipmentIsChargeable,
equipments: item.activity.ActivityEquipments.map((equipment) => ({
id: equipment.id,
equipmentName: equipment.equipmentName,
isEquipmentChargeable: equipment.isEquipmentChargeable,
equipmentBasePrice: equipment.equipmentBasePrice,
equipmentTotalPrice: equipment.equipmentTotalPrice,
})),
},
pickUpDetails: {
pickUpDropAvailable: item.activity.pickUpDropAvailable,
pickUpDropIsChargeable:
@@ -1340,6 +1437,40 @@ export class ItineraryService {
},
}
: null,
userSelections: userSelection
? {
id: userSelection.id,
itineraryMemberXid: userSelection.itineraryMemberXid,
isFoodOpted: userSelection.isFoodOpted,
selectedFoodTypeIds: userSelection.selectedFoodTypes.map(
(foodType) => foodType.activityFoodTypeXid,
),
selectedFoodTypes: userSelection.selectedFoodTypes.map(
(foodType) => ({
id: foodType.id,
activityFoodTypeXid: foodType.activityFoodTypeXid,
activityFoodType: foodType.activityFoodType,
}),
),
isTrainerOpted: userSelection.isTrainerOpted,
isInActivityNavigationOpted:
userSelection.isInActivityNavigationOpted,
selectedNavigationModeXid:
userSelection.activityNavigationModeXid,
selectedNavigationMode:
userSelection.activityNavigationMode ?? null,
selectedEquipmentIds: userSelection.selectedEquipments.map(
(equipment) => equipment.activityEquipmentXid,
),
selectedEquipments: userSelection.selectedEquipments.map(
(equipment) => ({
id: equipment.id,
activityEquipmentXid: equipment.activityEquipmentXid,
activityEquipment: equipment.activityEquipment,
}),
),
}
: null,
venue: item.venue,
scheduleHeader: item.scheduledHeader,
};
@@ -1381,6 +1512,382 @@ export class ItineraryService {
};
}
async saveItineraryActivitySelections(
userXid: number,
payload: {
itineraryActivityXid: number;
isFoodOpted?: boolean;
selectedFoodTypeIds?: number[];
isTrainerOpted?: boolean;
isInActivityNavigationOpted?: boolean;
selectedNavigationModeXid?: number | null;
selectedEquipmentIds?: number[];
},
) {
const selectedFoodTypeIds = Array.from(
new Set((payload.selectedFoodTypeIds ?? []).map(Number)),
);
const selectedEquipmentIds = Array.from(
new Set((payload.selectedEquipmentIds ?? []).map(Number)),
);
return this.prisma.$transaction(async (tx) => {
const itineraryActivity = await tx.itineraryActivities.findFirst({
where: {
id: payload.itineraryActivityXid,
isActive: true,
deletedAt: null,
itineraryHeader: {
ItineraryMembers: {
some: {
memberXid: userXid,
isActive: true,
deletedAt: null,
},
},
},
},
select: {
id: true,
itineraryHeaderXid: true,
itineraryType: true,
activityXid: true,
activity: {
select: {
id: true,
foodAvailable: true,
trainerAvailable: true,
inActivityAvailable: true,
equipmentAvailable: true,
activityFoodTypes: {
where: {
isActive: true,
deletedAt: null,
},
select: {
id: true,
foodTypeXid: true,
foodType: {
select: {
id: true,
foodTypeName: true,
},
},
},
},
ActivityNavigationModes: {
where: {
isActive: true,
deletedAt: null,
},
select: {
id: true,
navigationModeName: true,
isInActivityChargeable: true,
navigationModesBasePrice: true,
navigationModesTotalPrice: true,
},
},
ActivityEquipments: {
where: {
isActive: true,
deletedAt: null,
},
select: {
id: true,
equipmentName: true,
isEquipmentChargeable: true,
equipmentBasePrice: true,
equipmentTotalPrice: true,
},
},
},
},
},
});
if (!itineraryActivity) {
throw new ApiError(404, 'Itinerary activity not found for this user.');
}
if (
itineraryActivity.itineraryType !== 'ACTIVITY' ||
!itineraryActivity.activityXid ||
!itineraryActivity.activity
) {
throw new ApiError(
400,
'Selections can only be stored for itinerary items linked to an activity.',
);
}
const itineraryMember = await tx.itineraryMembers.findFirst({
where: {
itineraryHeaderXid: itineraryActivity.itineraryHeaderXid,
memberXid: userXid,
isActive: true,
deletedAt: null,
},
select: {
id: true,
},
});
if (!itineraryMember) {
throw new ApiError(404, 'Itinerary member record not found.');
}
if (selectedEquipmentIds.some((id) => !Number.isInteger(id) || id <= 0)) {
throw new ApiError(400, 'selectedEquipmentIds must contain valid ids.');
}
if (selectedFoodTypeIds.some((id) => !Number.isInteger(id) || id <= 0)) {
throw new ApiError(400, 'selectedFoodTypeIds must contain valid ids.');
}
const isFoodOpted = Boolean(payload.isFoodOpted);
const isTrainerOpted = Boolean(payload.isTrainerOpted);
const isInActivityNavigationOpted = Boolean(
payload.isInActivityNavigationOpted,
);
const selectedNavigationModeXid =
payload.selectedNavigationModeXid === undefined ||
payload.selectedNavigationModeXid === null
? null
: Number(payload.selectedNavigationModeXid);
if (
selectedNavigationModeXid !== null &&
(!Number.isInteger(selectedNavigationModeXid) ||
selectedNavigationModeXid <= 0)
) {
throw new ApiError(400, 'selectedNavigationModeXid must be a valid id.');
}
const availableFoodTypeIds = new Set(
itineraryActivity.activity.activityFoodTypes.map((item) => item.id),
);
const availableNavigationModeIds = new Set(
itineraryActivity.activity.ActivityNavigationModes.map((item) => item.id),
);
const availableEquipmentIds = new Set(
itineraryActivity.activity.ActivityEquipments.map((item) => item.id),
);
if (isFoodOpted) {
if (!itineraryActivity.activity.foodAvailable) {
throw new ApiError(400, 'Food is not available for this activity.');
}
if (
itineraryActivity.activity.activityFoodTypes.length > 0 &&
!selectedFoodTypeIds.length
) {
throw new ApiError(
400,
'selectedFoodTypeIds is required when food is opted.',
);
}
if (
selectedFoodTypeIds.some((id) => !availableFoodTypeIds.has(id))
) {
throw new ApiError(
400,
'One or more selected food types do not belong to this activity.',
);
}
} else if (selectedFoodTypeIds.length) {
throw new ApiError(
400,
'selectedFoodTypeIds cannot be sent when food is not opted.',
);
}
if (isTrainerOpted && !itineraryActivity.activity.trainerAvailable) {
throw new ApiError(400, 'Trainer is not available for this activity.');
}
if (isInActivityNavigationOpted) {
if (!itineraryActivity.activity.inActivityAvailable) {
throw new ApiError(
400,
'In-activity navigation is not available for this activity.',
);
}
if (
itineraryActivity.activity.ActivityNavigationModes.length > 0 &&
!selectedNavigationModeXid
) {
throw new ApiError(
400,
'selectedNavigationModeXid is required when navigation is opted.',
);
}
if (
selectedNavigationModeXid &&
!availableNavigationModeIds.has(selectedNavigationModeXid)
) {
throw new ApiError(
400,
'Selected navigation mode does not belong to this activity.',
);
}
} else if (selectedNavigationModeXid) {
throw new ApiError(
400,
'selectedNavigationModeXid cannot be sent when navigation is not opted.',
);
}
if (selectedEquipmentIds.length) {
if (!itineraryActivity.activity.equipmentAvailable) {
throw new ApiError(400, 'Equipment is not available for this activity.');
}
if (
selectedEquipmentIds.some((id) => !availableEquipmentIds.has(id))
) {
throw new ApiError(
400,
'One or more selected equipments do not belong to this activity.',
);
}
}
const selection = await tx.itineraryActivitySelection.upsert({
where: {
itineraryActivityXid_itineraryMemberXid: {
itineraryActivityXid: itineraryActivity.id,
itineraryMemberXid: itineraryMember.id,
},
},
create: {
itineraryActivityXid: itineraryActivity.id,
itineraryMemberXid: itineraryMember.id,
isFoodOpted,
isTrainerOpted,
isInActivityNavigationOpted,
activityNavigationModeXid: isInActivityNavigationOpted
? selectedNavigationModeXid
: null,
isActive: true,
},
update: {
isFoodOpted,
isTrainerOpted,
isInActivityNavigationOpted,
activityNavigationModeXid: isInActivityNavigationOpted
? selectedNavigationModeXid
: null,
isActive: true,
deletedAt: null,
},
select: {
id: true,
},
});
await tx.itineraryActivitySelectionFoodType.deleteMany({
where: {
itineraryActivitySelectionXid: selection.id,
},
});
await tx.itineraryActivitySelectionEquipment.deleteMany({
where: {
itineraryActivitySelectionXid: selection.id,
},
});
if (selectedFoodTypeIds.length) {
await tx.itineraryActivitySelectionFoodType.createMany({
data: selectedFoodTypeIds.map((activityFoodTypeXid) => ({
itineraryActivitySelectionXid: selection.id,
activityFoodTypeXid,
})),
});
}
if (selectedEquipmentIds.length) {
await tx.itineraryActivitySelectionEquipment.createMany({
data: selectedEquipmentIds.map((activityEquipmentXid) => ({
itineraryActivitySelectionXid: selection.id,
activityEquipmentXid,
})),
});
}
return tx.itineraryActivitySelection.findUnique({
where: {
id: selection.id,
},
select: {
id: true,
itineraryActivityXid: true,
itineraryMemberXid: true,
isFoodOpted: true,
isTrainerOpted: true,
isInActivityNavigationOpted: true,
activityNavigationModeXid: true,
selectedFoodTypes: {
where: {
isActive: true,
deletedAt: null,
},
select: {
id: true,
activityFoodTypeXid: true,
activityFoodType: {
select: {
id: true,
foodTypeXid: true,
foodType: {
select: {
id: true,
foodTypeName: true,
},
},
},
},
},
},
activityNavigationMode: {
select: {
id: true,
navigationModeName: true,
isInActivityChargeable: true,
navigationModesBasePrice: true,
navigationModesTotalPrice: true,
},
},
selectedEquipments: {
where: {
isActive: true,
deletedAt: null,
},
select: {
id: true,
activityEquipmentXid: true,
activityEquipment: {
select: {
id: true,
equipmentName: true,
isEquipmentChargeable: true,
equipmentBasePrice: true,
equipmentTotalPrice: true,
},
},
},
},
},
});
});
}
async getMatchingBucketInterestedActivities(
userXid: number,
payload: {