saving multiple activities selections at once in the api

This commit is contained in:
2026-04-13 13:38:17 +05:30
parent 958a3e5cec
commit b47e6271a3
2 changed files with 404 additions and 355 deletions

View File

@@ -35,24 +35,14 @@ export const handler = safeHandler(async (
}
}
const itineraryActivityXid = Number(body.itineraryActivityXid);
if (!Number.isInteger(itineraryActivityXid) || itineraryActivityXid <= 0) {
throw new ApiError(400, 'itineraryActivityXid is required.');
}
const activities = Array.isArray(body.activities)
? body.activities
: body.itineraryActivityXid !== undefined
? [body]
: [];
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.');
if (!activities.length) {
throw new ApiError(400, 'activities is required and must be a non-empty array.');
}
const toOptionalId = (value: unknown) => {
@@ -68,21 +58,60 @@ export const handler = safeHandler(async (
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,
const normalizedActivities = activities.map((activity: any, index: number) => {
const itineraryActivityXid = Number(activity.itineraryActivityXid);
if (!Number.isInteger(itineraryActivityXid) || itineraryActivityXid <= 0) {
throw new ApiError(400, `activities[${index}].itineraryActivityXid is required.`);
}
const selectedFoodTypeIds: number[] = Array.isArray(activity.selectedFoodTypeIds)
? Array.from(
new Set(
activity.selectedFoodTypeIds.map((id: unknown): number => Number(id)),
),
)
: [];
const selectedEquipmentIds: number[] = Array.isArray(activity.selectedEquipmentIds)
? Array.from(
new Set(
activity.selectedEquipmentIds.map((id: unknown): number => Number(id)),
),
)
: [];
if (selectedEquipmentIds.some((id) => !Number.isInteger(id) || id <= 0)) {
throw new ApiError(
400,
`activities[${index}].selectedEquipmentIds must contain valid ids.`,
);
}
if (selectedFoodTypeIds.some((id) => !Number.isInteger(id) || id <= 0)) {
throw new ApiError(
400,
`activities[${index}].selectedFoodTypeIds must contain valid ids.`,
);
}
return {
itineraryActivityXid,
isFoodOpted: activity.isFoodOpted === undefined ? false : Boolean(activity.isFoodOpted),
selectedFoodTypeIds,
isTrainerOpted: activity.isTrainerOpted === undefined ? false : Boolean(activity.isTrainerOpted),
isInActivityNavigationOpted:
activity.isInActivityNavigationOpted === undefined
? false
: Boolean(activity.isInActivityNavigationOpted),
selectedNavigationModeXid: toOptionalId(activity.selectedNavigationModeXid),
selectedEquipmentIds,
};
});
const result = await itineraryService.saveItineraryActivitySelections(
userId,
normalizedActivities,
);
return {
statusCode: 200,
headers: {

View File

@@ -2393,7 +2393,7 @@ export class ItineraryService {
async saveItineraryActivitySelections(
userXid: number,
payload: {
payload: Array<{
itineraryActivityXid: number;
isFoodOpted?: boolean;
selectedFoodTypeIds?: number[];
@@ -2401,64 +2401,356 @@ export class ItineraryService {
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,
const result = await Promise.all(
payload.map(async (item, index) => {
const selectedFoodTypeIds = Array.from(
new Set((item.selectedFoodTypeIds ?? []).map(Number)),
);
const selectedEquipmentIds = Array.from(
new Set((item.selectedEquipmentIds ?? []).map(Number)),
);
const itineraryActivity = await tx.itineraryActivities.findFirst({
where: {
id: item.itineraryActivityXid,
isActive: true,
deletedAt: null,
itineraryHeader: {
ItineraryMembers: {
some: {
memberXid: userXid,
isActive: true,
deletedAt: null,
},
},
},
},
},
},
select: {
id: true,
itineraryHeaderXid: true,
itineraryType: true,
activityXid: true,
activity: {
select: {
id: true,
foodAvailable: true,
trainerAvailable: true,
inActivityAvailable: true,
equipmentAvailable: true,
activityFoodTypes: {
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 item ${index}.`,
);
}
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,
`activities[${index}].selectedEquipmentIds must contain valid ids.`,
);
}
if (selectedFoodTypeIds.some((id) => !Number.isInteger(id) || id <= 0)) {
throw new ApiError(
400,
`activities[${index}].selectedFoodTypeIds must contain valid ids.`,
);
}
const isFoodOpted = Boolean(item.isFoodOpted);
const isTrainerOpted = Boolean(item.isTrainerOpted);
const isInActivityNavigationOpted = Boolean(
item.isInActivityNavigationOpted,
);
const selectedNavigationModeXid =
item.selectedNavigationModeXid === undefined ||
item.selectedNavigationModeXid === null
? null
: Number(item.selectedNavigationModeXid);
if (
selectedNavigationModeXid !== null &&
(!Number.isInteger(selectedNavigationModeXid) ||
selectedNavigationModeXid <= 0)
) {
throw new ApiError(
400,
`activities[${index}].selectedNavigationModeXid must be a valid id.`,
);
}
const availableFoodTypeIds = new Set(
itineraryActivity.activity.activityFoodTypes.map((entry) => entry.id),
);
const availableNavigationModeIds = new Set(
itineraryActivity.activity.ActivityNavigationModes.map((entry) => entry.id),
);
const availableEquipmentIds = new Set(
itineraryActivity.activity.ActivityEquipments.map((entry) => entry.id),
);
if (isFoodOpted) {
if (!itineraryActivity.activity.foodAvailable) {
throw new ApiError(400, `activities[${index}]: Food is not available for this activity.`);
}
if (
itineraryActivity.activity.activityFoodTypes.length > 0 &&
!selectedFoodTypeIds.length
) {
throw new ApiError(
400,
`activities[${index}].selectedFoodTypeIds is required when food is opted.`,
);
}
if (selectedFoodTypeIds.some((id) => !availableFoodTypeIds.has(id))) {
throw new ApiError(
400,
`activities[${index}]: One or more selected food types do not belong to this activity.`,
);
}
} else if (selectedFoodTypeIds.length) {
throw new ApiError(
400,
`activities[${index}].selectedFoodTypeIds cannot be sent when food is not opted.`,
);
}
if (isTrainerOpted && !itineraryActivity.activity.trainerAvailable) {
throw new ApiError(
400,
`activities[${index}]: Trainer is not available for this activity.`,
);
}
if (isInActivityNavigationOpted) {
if (!itineraryActivity.activity.inActivityAvailable) {
throw new ApiError(
400,
`activities[${index}]: In-activity navigation is not available for this activity.`,
);
}
if (
itineraryActivity.activity.ActivityNavigationModes.length > 0 &&
!selectedNavigationModeXid
) {
throw new ApiError(
400,
`activities[${index}].selectedNavigationModeXid is required when navigation is opted.`,
);
}
if (
selectedNavigationModeXid &&
!availableNavigationModeIds.has(selectedNavigationModeXid)
) {
throw new ApiError(
400,
`activities[${index}]: Selected navigation mode does not belong to this activity.`,
);
}
} else if (selectedNavigationModeXid) {
throw new ApiError(
400,
`activities[${index}].selectedNavigationModeXid cannot be sent when navigation is not opted.`,
);
}
if (selectedEquipmentIds.length) {
if (!itineraryActivity.activity.equipmentAvailable) {
throw new ApiError(
400,
`activities[${index}]: Equipment is not available for this activity.`,
);
}
if (selectedEquipmentIds.some((id) => !availableEquipmentIds.has(id))) {
throw new ApiError(
400,
`activities[${index}]: 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,
foodTypeXid: true,
foodType: {
activityFoodTypeXid: true,
activityFoodType: {
select: {
id: true,
foodTypeName: true,
foodTypeXid: true,
foodType: {
select: {
id: true,
foodTypeName: true,
},
},
},
},
},
},
ActivityNavigationModes: {
where: {
isActive: true,
deletedAt: null,
},
activityNavigationMode: {
select: {
id: true,
navigationModeName: true,
@@ -2467,303 +2759,31 @@ export class ItineraryService {
navigationModesTotalPrice: true,
},
},
ActivityEquipments: {
selectedEquipments: {
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: {
activityEquipmentXid: true,
activityEquipment: {
select: {
id: true,
foodTypeName: true,
equipmentName: true,
isEquipmentChargeable: true,
equipmentBasePrice: true,
equipmentTotalPrice: true,
},
},
},
},
},
},
activityNavigationMode: {
select: {
id: true,
navigationModeName: true,
isInActivityChargeable: true,
navigationModesBasePrice: true,
navigationModesTotalPrice: true,
},
},
selectedEquipments: {
where: {
isActive: true,
deletedAt: null,
},
select: {
id: true,
activityEquipmentXid: true,
activityEquipment: {
select: {
id: true,
equipmentName: true,
isEquipmentChargeable: true,
equipmentBasePrice: true,
equipmentTotalPrice: true,
},
},
},
},
},
});
});
}),
);
return result;
});
}