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 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))
const activities = Array.isArray(body.activities)
? body.activities
: body.itineraryActivityXid !== undefined
? [body]
: [];
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, {
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:
body.isFoodOpted === undefined ? false : Boolean(body.isFoodOpted),
isFoodOpted: activity.isFoodOpted === undefined ? false : Boolean(activity.isFoodOpted),
selectedFoodTypeIds,
isTrainerOpted:
body.isTrainerOpted === undefined ? false : Boolean(body.isTrainerOpted),
isTrainerOpted: activity.isTrainerOpted === undefined ? false : Boolean(activity.isTrainerOpted),
isInActivityNavigationOpted:
body.isInActivityNavigationOpted === undefined
activity.isInActivityNavigationOpted === undefined
? false
: Boolean(body.isInActivityNavigationOpted),
selectedNavigationModeXid: toOptionalId(body.selectedNavigationModeXid),
: 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,19 +2401,21 @@ export class ItineraryService {
isInActivityNavigationOpted?: boolean;
selectedNavigationModeXid?: number | null;
selectedEquipmentIds?: number[];
},
}>,
) {
return this.prisma.$transaction(async (tx) => {
const result = await Promise.all(
payload.map(async (item, index) => {
const selectedFoodTypeIds = Array.from(
new Set((payload.selectedFoodTypeIds ?? []).map(Number)),
new Set((item.selectedFoodTypeIds ?? []).map(Number)),
);
const selectedEquipmentIds = Array.from(
new Set((payload.selectedEquipmentIds ?? []).map(Number)),
new Set((item.selectedEquipmentIds ?? []).map(Number)),
);
return this.prisma.$transaction(async (tx) => {
const itineraryActivity = await tx.itineraryActivities.findFirst({
where: {
id: payload.itineraryActivityXid,
id: item.itineraryActivityXid,
isActive: true,
deletedAt: null,
itineraryHeader: {
@@ -2486,7 +2488,10 @@ export class ItineraryService {
});
if (!itineraryActivity) {
throw new ApiError(404, 'Itinerary activity not found for this user.');
throw new ApiError(
404,
`Itinerary activity not found for item ${index}.`,
);
}
if (
@@ -2517,46 +2522,55 @@ export class ItineraryService {
}
if (selectedEquipmentIds.some((id) => !Number.isInteger(id) || id <= 0)) {
throw new ApiError(400, 'selectedEquipmentIds must contain valid ids.');
throw new ApiError(
400,
`activities[${index}].selectedEquipmentIds must contain valid ids.`,
);
}
if (selectedFoodTypeIds.some((id) => !Number.isInteger(id) || id <= 0)) {
throw new ApiError(400, 'selectedFoodTypeIds must contain valid ids.');
throw new ApiError(
400,
`activities[${index}].selectedFoodTypeIds must contain valid ids.`,
);
}
const isFoodOpted = Boolean(payload.isFoodOpted);
const isTrainerOpted = Boolean(payload.isTrainerOpted);
const isFoodOpted = Boolean(item.isFoodOpted);
const isTrainerOpted = Boolean(item.isTrainerOpted);
const isInActivityNavigationOpted = Boolean(
payload.isInActivityNavigationOpted,
item.isInActivityNavigationOpted,
);
const selectedNavigationModeXid =
payload.selectedNavigationModeXid === undefined ||
payload.selectedNavigationModeXid === null
item.selectedNavigationModeXid === undefined ||
item.selectedNavigationModeXid === null
? null
: Number(payload.selectedNavigationModeXid);
: Number(item.selectedNavigationModeXid);
if (
selectedNavigationModeXid !== null &&
(!Number.isInteger(selectedNavigationModeXid) ||
selectedNavigationModeXid <= 0)
) {
throw new ApiError(400, 'selectedNavigationModeXid must be a valid id.');
throw new ApiError(
400,
`activities[${index}].selectedNavigationModeXid must be a valid id.`,
);
}
const availableFoodTypeIds = new Set(
itineraryActivity.activity.activityFoodTypes.map((item) => item.id),
itineraryActivity.activity.activityFoodTypes.map((entry) => entry.id),
);
const availableNavigationModeIds = new Set(
itineraryActivity.activity.ActivityNavigationModes.map((item) => item.id),
itineraryActivity.activity.ActivityNavigationModes.map((entry) => entry.id),
);
const availableEquipmentIds = new Set(
itineraryActivity.activity.ActivityEquipments.map((item) => item.id),
itineraryActivity.activity.ActivityEquipments.map((entry) => entry.id),
);
if (isFoodOpted) {
if (!itineraryActivity.activity.foodAvailable) {
throw new ApiError(400, 'Food is not available for this activity.');
throw new ApiError(400, `activities[${index}]: Food is not available for this activity.`);
}
if (
@@ -2565,34 +2579,35 @@ export class ItineraryService {
) {
throw new ApiError(
400,
'selectedFoodTypeIds is required when food is opted.',
`activities[${index}].selectedFoodTypeIds is required when food is opted.`,
);
}
if (
selectedFoodTypeIds.some((id) => !availableFoodTypeIds.has(id))
) {
if (selectedFoodTypeIds.some((id) => !availableFoodTypeIds.has(id))) {
throw new ApiError(
400,
'One or more selected food types do not belong to this activity.',
`activities[${index}]: 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.',
`activities[${index}].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.');
throw new ApiError(
400,
`activities[${index}]: 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.',
`activities[${index}]: In-activity navigation is not available for this activity.`,
);
}
@@ -2602,7 +2617,7 @@ export class ItineraryService {
) {
throw new ApiError(
400,
'selectedNavigationModeXid is required when navigation is opted.',
`activities[${index}].selectedNavigationModeXid is required when navigation is opted.`,
);
}
@@ -2612,27 +2627,28 @@ export class ItineraryService {
) {
throw new ApiError(
400,
'Selected navigation mode does not belong to this activity.',
`activities[${index}]: 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.',
`activities[${index}].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.',
`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.`,
);
}
}
@@ -2764,6 +2780,10 @@ export class ItineraryService {
},
},
});
}),
);
return result;
});
}