|
|
|
|
@@ -18,6 +18,8 @@ import { TrainSet } from "../train-sets/entities/train-set.entity";
|
|
|
|
|
import { Route } from "../routes/entities/route.entity";
|
|
|
|
|
import { TrainScheduleBooking } from "../train-schedules/entities/train-schedule-booking.entity";
|
|
|
|
|
import { TrainSchedule } from "../train-schedules/entities/train-schedule.entity";
|
|
|
|
|
import { WagonBookingAllocation } from "../train-schedules/entities/wagon-booking-allocation.entity";
|
|
|
|
|
import { Wagon } from "../wagons/entities/wagon.entity";
|
|
|
|
|
import { WagonType } from "../wagon-types/entities/wagon-type.entity";
|
|
|
|
|
import { WagonTypesRepository } from "../wagon-types/wagon-types.repository";
|
|
|
|
|
import { CreateContainerTrainScheduleDto } from "./dto/create-container-train-schedule.dto";
|
|
|
|
|
@@ -27,7 +29,10 @@ import { PreviewContainerTrainScheduleDto } from "./dto/preview-container-train-
|
|
|
|
|
const DEFAULT_WAGON_TYPE_CODE = "NW5";
|
|
|
|
|
const MAX_TRAIN_WEIGHT_TONS = 3500;
|
|
|
|
|
const MAX_TRAIN_LENGTH_METERS = 760;
|
|
|
|
|
const SCHEDULABLE_BOOKING_STATUSES = ["PAID"] as const;
|
|
|
|
|
const ASSIGNABLE_BOOKING_STATUSES = ["APPROVED", "READY_FOR_ASSIGNMENT"] as const;
|
|
|
|
|
const EXCLUDED_BOOKING_STATUSES = ["CANCELLED", "COMPLETED", "IN_TRANSIT", "ARRIVED"] as const;
|
|
|
|
|
const MAX_CONTAINER_WAGONS = 53;
|
|
|
|
|
const MAX_BULK_WAGONS = 37;
|
|
|
|
|
|
|
|
|
|
type EligibleBookingItem = {
|
|
|
|
|
id: string;
|
|
|
|
|
@@ -94,11 +99,13 @@ export class TrainSchedulingService {
|
|
|
|
|
"scheduleBooking",
|
|
|
|
|
"scheduleBooking.booking_id = booking.id",
|
|
|
|
|
)
|
|
|
|
|
.where("booking.freightType = :freightType", { freightType: "CONTAINER" })
|
|
|
|
|
.where("booking.freightType = :freightType", {
|
|
|
|
|
freightType: query.assignmentType ?? "CONTAINER",
|
|
|
|
|
})
|
|
|
|
|
.andWhere("scheduleBooking.id IS NULL");
|
|
|
|
|
|
|
|
|
|
queryBuilder.andWhere("booking.status IN (:...schedulableStatuses)", {
|
|
|
|
|
schedulableStatuses: SCHEDULABLE_BOOKING_STATUSES,
|
|
|
|
|
queryBuilder.andWhere("booking.status IN (:...assignableStatuses)", {
|
|
|
|
|
assignableStatuses: ASSIGNABLE_BOOKING_STATUSES,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (query.originStationId) {
|
|
|
|
|
@@ -116,6 +123,26 @@ export class TrainSchedulingService {
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (query.tradeDirection === "IMPORT") {
|
|
|
|
|
queryBuilder.andWhere(
|
|
|
|
|
`(
|
|
|
|
|
lower(originYard.country) IN ('djibouti', 'djoubti', 'dj')
|
|
|
|
|
OR lower(originYard.code) LIKE '%djib%'
|
|
|
|
|
OR lower(originYard.label) LIKE '%djib%'
|
|
|
|
|
)`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (query.tradeDirection === "EXPORT") {
|
|
|
|
|
queryBuilder.andWhere(
|
|
|
|
|
`(
|
|
|
|
|
lower(destinationYard.country) IN ('djibouti', 'djoubti', 'dj')
|
|
|
|
|
OR lower(destinationYard.code) LIKE '%djib%'
|
|
|
|
|
OR lower(destinationYard.label) LIKE '%djib%'
|
|
|
|
|
)`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (query.scheduleDate) {
|
|
|
|
|
queryBuilder.andWhere(
|
|
|
|
|
`DATE(booking.scheduled_date AT TIME ZONE 'UTC') = :scheduleDate`,
|
|
|
|
|
@@ -141,7 +168,7 @@ export class TrainSchedulingService {
|
|
|
|
|
container.containerType?.code ??
|
|
|
|
|
"Container",
|
|
|
|
|
)
|
|
|
|
|
.join(", ") ?? "Container",
|
|
|
|
|
.join(", ") ?? (booking.freightType === "BULK" ? "Bulk cargo" : "Container"),
|
|
|
|
|
quantity:
|
|
|
|
|
booking.bookingContainers?.reduce(
|
|
|
|
|
(sum, container) => sum + Number(container.quantity ?? 0),
|
|
|
|
|
@@ -180,11 +207,27 @@ export class TrainSchedulingService {
|
|
|
|
|
|
|
|
|
|
async createContainerTrainSchedule(dto: CreateContainerTrainScheduleDto) {
|
|
|
|
|
const route = await this.getActiveRoute(dto.routeId);
|
|
|
|
|
const validation = dto.bookingIds?.length
|
|
|
|
|
? await this.validateContainerBookingsForScheduling({
|
|
|
|
|
bookingIds: dto.bookingIds,
|
|
|
|
|
scheduleDate: dto.scheduleDate,
|
|
|
|
|
originStationId: route.originYardId,
|
|
|
|
|
destinationStationId: route.destinationYardId,
|
|
|
|
|
assignmentType: dto.assignmentType ?? "CONTAINER",
|
|
|
|
|
})
|
|
|
|
|
: null;
|
|
|
|
|
|
|
|
|
|
if (validation && !validation.valid) {
|
|
|
|
|
throw new BadRequestException({
|
|
|
|
|
message: "Train schedule assignment is invalid",
|
|
|
|
|
violations: validation.violations,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const locomotive = await this.selectOrValidateLocomotive(
|
|
|
|
|
dto.locomotiveId,
|
|
|
|
|
0,
|
|
|
|
|
0,
|
|
|
|
|
validation?.summary.totalWeightTons ?? 0,
|
|
|
|
|
validation?.summary.totalLengthMeters ?? 0,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const createdSchedule = await this.dataSource.transaction(
|
|
|
|
|
@@ -205,10 +248,28 @@ export class TrainSchedulingService {
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const trainSet = await this.buildEmptyTrainSet(
|
|
|
|
|
manager,
|
|
|
|
|
lockedLocomotive,
|
|
|
|
|
);
|
|
|
|
|
const selectedPhysicalWagons = validation
|
|
|
|
|
? await this.lockSelectedWagonsForSchedule(
|
|
|
|
|
manager,
|
|
|
|
|
dto.wagonIds ?? [],
|
|
|
|
|
validation.wagonPlan.length,
|
|
|
|
|
route,
|
|
|
|
|
dto.assignmentType ?? "CONTAINER",
|
|
|
|
|
)
|
|
|
|
|
: [];
|
|
|
|
|
|
|
|
|
|
const trainSetResult = validation
|
|
|
|
|
? await this.buildTrainSet(
|
|
|
|
|
manager,
|
|
|
|
|
lockedLocomotive,
|
|
|
|
|
validation.wagonType,
|
|
|
|
|
validation.summary.totalWeightTons,
|
|
|
|
|
validation.summary.totalLengthMeters,
|
|
|
|
|
validation.wagonPlan,
|
|
|
|
|
selectedPhysicalWagons,
|
|
|
|
|
)
|
|
|
|
|
: { trainSet: await this.buildEmptyTrainSet(manager, lockedLocomotive), wagons: [] };
|
|
|
|
|
const { trainSet, wagons } = trainSetResult;
|
|
|
|
|
|
|
|
|
|
const schedule = manager.getRepository(TrainSchedule).create({
|
|
|
|
|
trainSetId: trainSet.id,
|
|
|
|
|
@@ -216,13 +277,51 @@ export class TrainSchedulingService {
|
|
|
|
|
originStationId: route.originYardId,
|
|
|
|
|
destinationStationId: route.destinationYardId,
|
|
|
|
|
scheduledDepartureDate: new Date(dto.scheduleDate),
|
|
|
|
|
status: "DRAFT",
|
|
|
|
|
scheduledArrivalDate: dto.arrivalDate ? new Date(dto.arrivalDate) : null,
|
|
|
|
|
status: validation ? "READY" : "DRAFT",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const savedSchedule = await manager
|
|
|
|
|
.getRepository(TrainSchedule)
|
|
|
|
|
.save(schedule);
|
|
|
|
|
|
|
|
|
|
if (validation) {
|
|
|
|
|
await manager.getRepository(TrainScheduleBooking).save(
|
|
|
|
|
validation.bookings.map((booking) =>
|
|
|
|
|
manager.getRepository(TrainScheduleBooking).create({
|
|
|
|
|
trainScheduleId: savedSchedule.id,
|
|
|
|
|
bookingId: booking.id,
|
|
|
|
|
}),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const wagonBySequence = new Map(wagons.map((wagon) => [wagon.sequenceNo, wagon]));
|
|
|
|
|
const allocations = validation.wagonPlan.flatMap((wagonPlan) => {
|
|
|
|
|
const savedWagon = wagonBySequence.get(wagonPlan.sequenceNo);
|
|
|
|
|
if (!savedWagon) return [];
|
|
|
|
|
|
|
|
|
|
return wagonPlan.allocations.map((allocation) =>
|
|
|
|
|
manager.getRepository(WagonBookingAllocation).create({
|
|
|
|
|
trainSetWagonId: savedWagon.id,
|
|
|
|
|
bookingId: allocation.bookingId,
|
|
|
|
|
allocatedWeightTons: allocation.allocatedWeightTons,
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (allocations.length > 0) {
|
|
|
|
|
await manager.getRepository(WagonBookingAllocation).save(allocations);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await manager.getRepository(Booking).update(
|
|
|
|
|
{ id: In(validation.bookings.map((booking) => booking.id)) },
|
|
|
|
|
{
|
|
|
|
|
status: "INVOICED",
|
|
|
|
|
paymentStatus: "PENDING",
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await locomotiveRepository.update(lockedLocomotive.id, {
|
|
|
|
|
status: "ASSIGNED",
|
|
|
|
|
});
|
|
|
|
|
@@ -276,24 +375,32 @@ export class TrainSchedulingService {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const nonContainerBookings = bookings.filter(
|
|
|
|
|
(booking) => booking.freightType !== "CONTAINER",
|
|
|
|
|
(booking) => booking.freightType !== (dto.assignmentType ?? "CONTAINER"),
|
|
|
|
|
);
|
|
|
|
|
if (nonContainerBookings.length > 0) {
|
|
|
|
|
violations.push(
|
|
|
|
|
"Only CONTAINER bookings are supported for train scheduling",
|
|
|
|
|
`Only ${dto.assignmentType ?? "CONTAINER"} bookings are supported for this assignment`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const invalidStatusBookings = bookings.filter(
|
|
|
|
|
(booking) => !SCHEDULABLE_BOOKING_STATUSES.includes(booking.status as "PAID"),
|
|
|
|
|
(booking) =>
|
|
|
|
|
!ASSIGNABLE_BOOKING_STATUSES.includes(booking.status as (typeof ASSIGNABLE_BOOKING_STATUSES)[number]),
|
|
|
|
|
);
|
|
|
|
|
if (invalidStatusBookings.length > 0) {
|
|
|
|
|
const invalidStatuses = [...new Set(invalidStatusBookings.map((booking) => booking.status))];
|
|
|
|
|
violations.push(
|
|
|
|
|
`Only ${SCHEDULABLE_BOOKING_STATUSES.join(", ")} bookings can be scheduled; received: ${invalidStatuses.join(", ")}`,
|
|
|
|
|
`Only ${ASSIGNABLE_BOOKING_STATUSES.join(", ")} bookings can be assigned; received: ${invalidStatuses.join(", ")}`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const excludedStatusBookings = bookings.filter((booking) =>
|
|
|
|
|
EXCLUDED_BOOKING_STATUSES.includes(booking.status as (typeof EXCLUDED_BOOKING_STATUSES)[number]),
|
|
|
|
|
);
|
|
|
|
|
if (excludedStatusBookings.length > 0) {
|
|
|
|
|
violations.push(`Cancelled, completed, in-transit, or arrived bookings cannot be assigned`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const scheduleDateKey = this.toUtcDateKey(dto.scheduleDate);
|
|
|
|
|
const routeMismatch = bookings.some(
|
|
|
|
|
(booking) =>
|
|
|
|
|
@@ -366,11 +473,11 @@ export class TrainSchedulingService {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
wagonType.maxWagonsPerTrain != null &&
|
|
|
|
|
wagonPlan.length > Number(wagonType.maxWagonsPerTrain)
|
|
|
|
|
wagonPlan.length >
|
|
|
|
|
(dto.assignmentType === "BULK" ? MAX_BULK_WAGONS : MAX_CONTAINER_WAGONS)
|
|
|
|
|
) {
|
|
|
|
|
violations.push(
|
|
|
|
|
`Wagon count ${wagonPlan.length} exceeds wagon marshalling limit ${wagonType.maxWagonsPerTrain}`,
|
|
|
|
|
`Wagon count ${wagonPlan.length} exceeds ${dto.assignmentType === "BULK" ? "bulk" : "container"} limit ${dto.assignmentType === "BULK" ? MAX_BULK_WAGONS : MAX_CONTAINER_WAGONS}`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -474,6 +581,82 @@ export class TrainSchedulingService {
|
|
|
|
|
return locomotive;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async lockSelectedWagonsForSchedule(
|
|
|
|
|
manager: EntityManager,
|
|
|
|
|
wagonIds: string[],
|
|
|
|
|
requiredCount: number,
|
|
|
|
|
route: Route,
|
|
|
|
|
assignmentType: "CONTAINER" | "BULK",
|
|
|
|
|
) {
|
|
|
|
|
const uniqueWagonIds = [...new Set(wagonIds)];
|
|
|
|
|
|
|
|
|
|
if (uniqueWagonIds.length < requiredCount) {
|
|
|
|
|
throw new BadRequestException(
|
|
|
|
|
`Select at least ${requiredCount} available wagons for this schedule`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const wagons = await manager
|
|
|
|
|
.getRepository(Wagon)
|
|
|
|
|
.createQueryBuilder("wagon")
|
|
|
|
|
.leftJoinAndSelect("wagon.wagonType", "wagonType")
|
|
|
|
|
.where("wagon.id IN (:...wagonIds)", { wagonIds: uniqueWagonIds })
|
|
|
|
|
.setLock("pessimistic_write")
|
|
|
|
|
.getMany();
|
|
|
|
|
|
|
|
|
|
if (wagons.length !== uniqueWagonIds.length) {
|
|
|
|
|
throw new BadRequestException("One or more selected wagons were not found");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const expectedStatus = this.expectedWagonStatusForRoute(route);
|
|
|
|
|
const allowedStatuses = new Set([
|
|
|
|
|
expectedStatus,
|
|
|
|
|
"AVAILABLE",
|
|
|
|
|
...(expectedStatus === "EXPORT_READY" ? ["IMPORT_READY"] : []),
|
|
|
|
|
]);
|
|
|
|
|
const invalidWagon = wagons.find(
|
|
|
|
|
(wagon) =>
|
|
|
|
|
wagon.trainId ||
|
|
|
|
|
wagon.status === "ASSIGNED" ||
|
|
|
|
|
wagon.currentLocationYardId !== route.originYardId ||
|
|
|
|
|
!allowedStatuses.has(wagon.status) ||
|
|
|
|
|
!this.wagonTypeSupportsAssignment(wagon, assignmentType),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (invalidWagon) {
|
|
|
|
|
throw new BadRequestException(
|
|
|
|
|
`Wagon ${invalidWagon.wagonNumber} is not at the route origin or is not ready for this ${this.routeDirection(route).toLowerCase()} route`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const wagonById = new Map(wagons.map((wagon) => [wagon.id, wagon]));
|
|
|
|
|
return uniqueWagonIds.slice(0, requiredCount).map((wagonId) => wagonById.get(wagonId)!);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private wagonTypeSupportsAssignment(wagon: Wagon, assignmentType: "CONTAINER" | "BULK") {
|
|
|
|
|
const supportedLoadTypes = wagon.wagonType?.supportedLoadTypes ?? [];
|
|
|
|
|
const normalized = supportedLoadTypes.map((loadType) => loadType.trim().toUpperCase());
|
|
|
|
|
return normalized.includes(assignmentType);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private routeDirection(route: Route) {
|
|
|
|
|
const originCountry = route.originYard?.country?.trim().toLowerCase();
|
|
|
|
|
const destinationCountry = route.destinationYard?.country?.trim().toLowerCase();
|
|
|
|
|
const isOriginEthiopia = originCountry === "ethiopia" || originCountry === "et";
|
|
|
|
|
const isDestinationEthiopia = destinationCountry === "ethiopia" || destinationCountry === "et";
|
|
|
|
|
|
|
|
|
|
if (!isOriginEthiopia && isDestinationEthiopia) return "IMPORT";
|
|
|
|
|
if (isOriginEthiopia && !isDestinationEthiopia) return "EXPORT";
|
|
|
|
|
return "DOMESTIC";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private expectedWagonStatusForRoute(route: Route) {
|
|
|
|
|
const direction = this.routeDirection(route);
|
|
|
|
|
if (direction === "IMPORT") return "IMPORT_READY";
|
|
|
|
|
if (direction === "EXPORT" || direction === "DOMESTIC") return "EXPORT_READY";
|
|
|
|
|
return "AVAILABLE";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async buildTrainSet(
|
|
|
|
|
manager: EntityManager,
|
|
|
|
|
locomotive: Locomotive,
|
|
|
|
|
@@ -481,6 +664,7 @@ export class TrainSchedulingService {
|
|
|
|
|
totalWeightTons: number,
|
|
|
|
|
totalLengthMeters: number,
|
|
|
|
|
wagonPlan: WagonPlanRecord[],
|
|
|
|
|
physicalWagons: Wagon[] = [],
|
|
|
|
|
) {
|
|
|
|
|
const trainSet = manager.getRepository(TrainSet).create({
|
|
|
|
|
locomotiveId: locomotive.id,
|
|
|
|
|
@@ -491,20 +675,35 @@ export class TrainSchedulingService {
|
|
|
|
|
});
|
|
|
|
|
const savedTrainSet = await manager.getRepository(TrainSet).save(trainSet);
|
|
|
|
|
|
|
|
|
|
const wagons = wagonPlan.map((wagon) =>
|
|
|
|
|
manager.getRepository(TrainSetWagon).create({
|
|
|
|
|
const wagons = wagonPlan.map((wagon, index) => {
|
|
|
|
|
const physicalWagon = physicalWagons[index];
|
|
|
|
|
const selectedWagonType = physicalWagon?.wagonType ?? wagonType;
|
|
|
|
|
|
|
|
|
|
return manager.getRepository(TrainSetWagon).create({
|
|
|
|
|
trainSetId: savedTrainSet.id,
|
|
|
|
|
wagonTypeId: wagonType.id,
|
|
|
|
|
wagonTypeId: selectedWagonType.id,
|
|
|
|
|
physicalWagonId: physicalWagon?.id ?? null,
|
|
|
|
|
sequenceNo: wagon.sequenceNo,
|
|
|
|
|
capacityTons: wagon.capacityTons,
|
|
|
|
|
lengthMeters: wagon.lengthMeters,
|
|
|
|
|
capacityTons: Number(selectedWagonType.capacityTons),
|
|
|
|
|
lengthMeters: Number(selectedWagonType.lengthMeters),
|
|
|
|
|
assignedWeightTons: wagon.assignedWeightTons,
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await manager.getRepository(TrainSetWagon).save(wagons);
|
|
|
|
|
const savedWagons = await manager.getRepository(TrainSetWagon).save(wagons);
|
|
|
|
|
|
|
|
|
|
return savedTrainSet;
|
|
|
|
|
if (physicalWagons.length > 0) {
|
|
|
|
|
await Promise.all(
|
|
|
|
|
physicalWagons.map((wagon, index) =>
|
|
|
|
|
manager.getRepository(Wagon).update(wagon.id, {
|
|
|
|
|
status: "ASSIGNED",
|
|
|
|
|
sequenceNumber: index + 1,
|
|
|
|
|
}),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return { trainSet: savedTrainSet, wagons: savedWagons };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async buildEmptyTrainSet(
|
|
|
|
|
@@ -627,7 +826,7 @@ export class TrainSchedulingService {
|
|
|
|
|
route: true,
|
|
|
|
|
trainSet: {
|
|
|
|
|
locomotive: true,
|
|
|
|
|
wagons: { wagonType: true, allocations: { booking: true } },
|
|
|
|
|
wagons: { wagonType: true, physicalWagon: true, allocations: { booking: true } },
|
|
|
|
|
},
|
|
|
|
|
originStation: true,
|
|
|
|
|
destinationStation: true,
|
|
|
|
|
@@ -696,6 +895,13 @@ export class TrainSchedulingService {
|
|
|
|
|
name: wagon.wagonType.name,
|
|
|
|
|
}
|
|
|
|
|
: null,
|
|
|
|
|
physicalWagon: wagon.physicalWagon
|
|
|
|
|
? {
|
|
|
|
|
id: wagon.physicalWagon.id,
|
|
|
|
|
wagonNumber: wagon.physicalWagon.wagonNumber,
|
|
|
|
|
status: wagon.physicalWagon.status,
|
|
|
|
|
}
|
|
|
|
|
: null,
|
|
|
|
|
allocations:
|
|
|
|
|
wagon.allocations?.map((allocation) => ({
|
|
|
|
|
id: allocation.id,
|
|
|
|
|
@@ -729,7 +935,7 @@ export class TrainSchedulingService {
|
|
|
|
|
.getRepository(TrainSchedule)
|
|
|
|
|
.findOne({
|
|
|
|
|
where: { id },
|
|
|
|
|
relations: { trainSet: { locomotive: true } },
|
|
|
|
|
relations: { trainSet: { locomotive: true, wagons: true } },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!schedule) {
|
|
|
|
|
@@ -754,11 +960,58 @@ export class TrainSchedulingService {
|
|
|
|
|
status: "AVAILABLE",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const physicalWagonIds =
|
|
|
|
|
schedule.trainSet?.wagons
|
|
|
|
|
?.map((wagon) => wagon.physicalWagonId)
|
|
|
|
|
.filter((wagonId): wagonId is string => Boolean(wagonId)) ?? [];
|
|
|
|
|
|
|
|
|
|
if (physicalWagonIds.length > 0) {
|
|
|
|
|
const physicalWagons = await manager.getRepository(Wagon).find({
|
|
|
|
|
where: { id: In(physicalWagonIds) },
|
|
|
|
|
relations: { currentLocationYard: true },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await Promise.all(
|
|
|
|
|
physicalWagons.map((wagon) =>
|
|
|
|
|
manager.getRepository(Wagon).update(wagon.id, {
|
|
|
|
|
status: this.expectedWagonStatusForYard(wagon.currentLocationYard),
|
|
|
|
|
sequenceNumber: null,
|
|
|
|
|
}),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return this.getContainerTrainScheduleById(id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async publishTrainSchedule(id: string) {
|
|
|
|
|
const schedule = await this.dataSource.getRepository(TrainSchedule).findOne({
|
|
|
|
|
where: { id },
|
|
|
|
|
relations: { trainSet: true, scheduleBookings: true },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!schedule) {
|
|
|
|
|
throw new NotFoundException(`Train schedule ${id} not found`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (schedule.status === "CANCELLED") {
|
|
|
|
|
throw new BadRequestException("Cancelled schedules cannot be published");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!schedule.trainSet || schedule.trainSet.wagonCount <= 0) {
|
|
|
|
|
throw new BadRequestException("Allocate wagons before publishing the schedule");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ((schedule.scheduleBookings?.length ?? 0) === 0) {
|
|
|
|
|
throw new BadRequestException("Assign bookings before publishing the schedule");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await this.dataSource.getRepository(TrainSchedule).update(id, { status: "PUBLISHED" });
|
|
|
|
|
return this.getContainerTrainScheduleById(id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async loadBookingsForScheduling(bookingIds: string[]) {
|
|
|
|
|
return this.dataSource.getRepository(Booking).find({
|
|
|
|
|
where: { id: In(bookingIds) },
|
|
|
|
|
@@ -775,6 +1028,7 @@ export class TrainSchedulingService {
|
|
|
|
|
private async getActiveRoute(routeId: string) {
|
|
|
|
|
const route = await this.dataSource.getRepository(Route).findOne({
|
|
|
|
|
where: { id: routeId },
|
|
|
|
|
relations: { originYard: true, destinationYard: true },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!route) {
|
|
|
|
|
@@ -788,6 +1042,13 @@ export class TrainSchedulingService {
|
|
|
|
|
return route;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private expectedWagonStatusForYard(yard?: { country?: string } | null) {
|
|
|
|
|
const country = yard?.country?.trim().toLowerCase();
|
|
|
|
|
if (country === "ethiopia" || country === "et") return "EXPORT_READY";
|
|
|
|
|
if (country === "djibouti" || country === "djoubti" || country === "dj") return "IMPORT_READY";
|
|
|
|
|
return "AVAILABLE";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private toUtcDateKey(value: Date | string) {
|
|
|
|
|
const date = value instanceof Date ? value : new Date(value);
|
|
|
|
|
return date.toISOString().slice(0, 10);
|
|
|
|
|
|