|
|
|
|
@@ -46,73 +46,6 @@ import { PreviewTrainScheduleDto } from './dto/preview-train-schedule.dto';
|
|
|
|
|
import { TrainSchedulingGlobalRules } from './entities/train-scheduling-global-rules.entity';
|
|
|
|
|
import { UpdateTrainSchedulingGlobalRulesDto } from './dto/update-train-scheduling-global-rules.dto';
|
|
|
|
|
import {
|
|
|
|
|
<<<<<<< HEAD
|
|
|
|
|
Locomotive,
|
|
|
|
|
type LocomotiveStatus,
|
|
|
|
|
} from "../locomotives/entities/locomotive.entity";
|
|
|
|
|
import { LocomotivesRepository } from "../locomotives/locomotives.repository";
|
|
|
|
|
import { TrainSetWagon } from "../train-sets/entities/train-set-wagon.entity";
|
|
|
|
|
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";
|
|
|
|
|
import { GetEligibleContainerBookingsDto } from "./dto/get-eligible-container-bookings.dto";
|
|
|
|
|
import { PreviewContainerTrainScheduleDto } from "./dto/preview-container-train-schedule.dto";
|
|
|
|
|
|
|
|
|
|
const DEFAULT_WAGON_TYPE_CODE = "NW5";
|
|
|
|
|
const MAX_TRAIN_WEIGHT_TONS = 3500;
|
|
|
|
|
const MAX_TRAIN_LENGTH_METERS = 760;
|
|
|
|
|
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;
|
|
|
|
|
reference: string;
|
|
|
|
|
customer: string;
|
|
|
|
|
containerType: string;
|
|
|
|
|
quantity: number;
|
|
|
|
|
weightTons: number;
|
|
|
|
|
origin: string;
|
|
|
|
|
destination: string;
|
|
|
|
|
preferredDepartureDate: string;
|
|
|
|
|
status: string;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type WagonAllocationRecord = {
|
|
|
|
|
bookingId: string;
|
|
|
|
|
bookingReference: string;
|
|
|
|
|
allocatedWeightTons: number;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type WagonPlanRecord = {
|
|
|
|
|
sequenceNo: number;
|
|
|
|
|
capacityTons: number;
|
|
|
|
|
lengthMeters: number;
|
|
|
|
|
assignedWeightTons: number;
|
|
|
|
|
allocations: WagonAllocationRecord[];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type ValidationResult = {
|
|
|
|
|
valid: boolean;
|
|
|
|
|
violations: string[];
|
|
|
|
|
bookings: Booking[];
|
|
|
|
|
wagonType: WagonType;
|
|
|
|
|
summary: {
|
|
|
|
|
totalBookings: number;
|
|
|
|
|
totalWeightTons: number;
|
|
|
|
|
wagonType: string;
|
|
|
|
|
wagonsNeeded: number;
|
|
|
|
|
totalLengthMeters: number;
|
|
|
|
|
};
|
|
|
|
|
wagonPlan: WagonPlanRecord[];
|
|
|
|
|
=======
|
|
|
|
|
buildCappedWagonPlan,
|
|
|
|
|
computeFleetAvailability,
|
|
|
|
|
selectBookingsWithinFleetCap,
|
|
|
|
|
@@ -150,7 +83,6 @@ const DEFAULT_TRAIN_LIMITS: Required<TrainLimitConfig> = {
|
|
|
|
|
maxWagonsPerTrain: 53,
|
|
|
|
|
max20ftContainerWeightTons: 30,
|
|
|
|
|
max20ftPairWeightDiffTons: 10,
|
|
|
|
|
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
@Injectable()
|
|
|
|
|
@@ -180,36 +112,12 @@ export class TrainSchedulingService {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async getEligibleContainerBookings(query: GetEligibleContainerBookingsDto) {
|
|
|
|
|
<<<<<<< HEAD
|
|
|
|
|
const bookingRepository = this.dataSource.getRepository(Booking);
|
|
|
|
|
const queryBuilder = bookingRepository
|
|
|
|
|
.createQueryBuilder("booking")
|
|
|
|
|
.leftJoinAndSelect("booking.company", "company")
|
|
|
|
|
.leftJoinAndSelect("booking.originYard", "originYard")
|
|
|
|
|
.leftJoinAndSelect("booking.destinationYard", "destinationYard")
|
|
|
|
|
.leftJoinAndSelect("booking.bookingContainers", "bookingContainer")
|
|
|
|
|
.leftJoinAndSelect("bookingContainer.containerType", "containerType")
|
|
|
|
|
.leftJoin(
|
|
|
|
|
TrainScheduleBooking,
|
|
|
|
|
"scheduleBooking",
|
|
|
|
|
"scheduleBooking.booking_id = booking.id",
|
|
|
|
|
)
|
|
|
|
|
.where("booking.freightType = :freightType", {
|
|
|
|
|
freightType: query.assignmentType ?? "CONTAINER",
|
|
|
|
|
})
|
|
|
|
|
.andWhere("scheduleBooking.id IS NULL");
|
|
|
|
|
|
|
|
|
|
queryBuilder.andWhere("booking.status IN (:...assignableStatuses)", {
|
|
|
|
|
assignableStatuses: ASSIGNABLE_BOOKING_STATUSES,
|
|
|
|
|
});
|
|
|
|
|
=======
|
|
|
|
|
return this.getEligibleBookings({ ...query, freightType: 'CONTAINER' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async getEligibleBulkBookings(query: GetEligibleBulkBookingsDto) {
|
|
|
|
|
return this.getEligibleBookings({ ...query, freightType: 'BULK' });
|
|
|
|
|
}
|
|
|
|
|
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
|
|
|
|
|
|
|
|
async getTrainSchedulingGlobalRules() {
|
|
|
|
|
return this.loadGlobalRulesRow();
|
|
|
|
|
@@ -226,84 +134,12 @@ export class TrainSchedulingService {
|
|
|
|
|
if (dto.max20ftContainerWeightTons != null) {
|
|
|
|
|
row.max20ftContainerWeightTons = dto.max20ftContainerWeightTons;
|
|
|
|
|
}
|
|
|
|
|
<<<<<<< HEAD
|
|
|
|
|
|
|
|
|
|
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`,
|
|
|
|
|
{ scheduleDate: this.toUtcDateKey(query.scheduleDate) },
|
|
|
|
|
);
|
|
|
|
|
=======
|
|
|
|
|
if (dto.max20ftPairWeightDiffTons != null) {
|
|
|
|
|
row.max20ftPairWeightDiffTons = dto.max20ftPairWeightDiffTons;
|
|
|
|
|
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
|
|
|
}
|
|
|
|
|
return this.dataSource.getRepository(TrainSchedulingGlobalRules).save(row);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
<<<<<<< HEAD
|
|
|
|
|
const bookings = await queryBuilder
|
|
|
|
|
.orderBy("booking.scheduled_date", "ASC")
|
|
|
|
|
.addOrderBy("booking.created_at", "ASC")
|
|
|
|
|
.getMany();
|
|
|
|
|
|
|
|
|
|
const items: EligibleBookingItem[] = bookings.map((booking) => ({
|
|
|
|
|
id: booking.id,
|
|
|
|
|
reference: booking.reference,
|
|
|
|
|
customer:
|
|
|
|
|
booking.company?.name ?? booking.company?.email ?? "Unknown customer",
|
|
|
|
|
containerType:
|
|
|
|
|
booking.bookingContainers
|
|
|
|
|
?.map(
|
|
|
|
|
(container) =>
|
|
|
|
|
container.containerType?.label ??
|
|
|
|
|
container.containerType?.code ??
|
|
|
|
|
"Container",
|
|
|
|
|
)
|
|
|
|
|
.join(", ") ?? (booking.freightType === "BULK" ? "Bulk cargo" : "Container"),
|
|
|
|
|
quantity:
|
|
|
|
|
booking.bookingContainers?.reduce(
|
|
|
|
|
(sum, container) => sum + Number(container.quantity ?? 0),
|
|
|
|
|
0,
|
|
|
|
|
) ?? 0,
|
|
|
|
|
weightTons: this.roundTons(booking.cargoTotalWeightVgm),
|
|
|
|
|
origin:
|
|
|
|
|
booking.originYard?.label ??
|
|
|
|
|
booking.originYard?.code ??
|
|
|
|
|
"Unknown origin",
|
|
|
|
|
destination:
|
|
|
|
|
booking.destinationYard?.label ??
|
|
|
|
|
booking.destinationYard?.code ??
|
|
|
|
|
"Unknown destination",
|
|
|
|
|
preferredDepartureDate: booking.scheduledDate.toISOString(),
|
|
|
|
|
status: booking.status,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
count: items.length,
|
|
|
|
|
items,
|
|
|
|
|
};
|
|
|
|
|
=======
|
|
|
|
|
async previewTrainSchedule(dto: PreviewTrainScheduleDto) {
|
|
|
|
|
const limits = await this.resolveTrainLimitConfig(dto);
|
|
|
|
|
return this.buildPreviewResponse(
|
|
|
|
|
@@ -317,7 +153,6 @@ export class TrainSchedulingService {
|
|
|
|
|
dto.targetScheduleId,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async previewContainerTrainSchedule(dto: PreviewContainerTrainScheduleDto) {
|
|
|
|
|
@@ -370,29 +205,6 @@ export class TrainSchedulingService {
|
|
|
|
|
|
|
|
|
|
async createContainerTrainSchedule(dto: CreateContainerTrainScheduleDto) {
|
|
|
|
|
const route = await this.getActiveRoute(dto.routeId);
|
|
|
|
|
<<<<<<< HEAD
|
|
|
|
|
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,
|
|
|
|
|
validation?.summary.totalWeightTons ?? 0,
|
|
|
|
|
validation?.summary.totalLengthMeters ?? 0,
|
|
|
|
|
=======
|
|
|
|
|
const locomotive = await this.selectOrValidateLocomotive(dto.locomotiveId, 0, 0);
|
|
|
|
|
|
|
|
|
|
const createdScheduleId = await this.dataSource.transaction(async (manager) => {
|
|
|
|
|
@@ -467,7 +279,6 @@ export class TrainSchedulingService {
|
|
|
|
|
true,
|
|
|
|
|
limits,
|
|
|
|
|
scheduleId,
|
|
|
|
|
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (!validation.valid) {
|
|
|
|
|
@@ -671,42 +482,8 @@ export class TrainSchedulingService {
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
<<<<<<< HEAD
|
|
|
|
|
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,
|
|
|
|
|
routeId: route.id,
|
|
|
|
|
originStationId: route.originYardId,
|
|
|
|
|
destinationStationId: route.destinationYardId,
|
|
|
|
|
scheduledDepartureDate: new Date(dto.scheduleDate),
|
|
|
|
|
scheduledArrivalDate: dto.arrivalDate ? new Date(dto.arrivalDate) : null,
|
|
|
|
|
status: validation ? "READY" : "DRAFT",
|
|
|
|
|
=======
|
|
|
|
|
const physicalWagon = await manager.getRepository(Wagon).findOne({
|
|
|
|
|
where: { id: assignment.physicalWagonId },
|
|
|
|
|
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
|
|
|
});
|
|
|
|
|
if (!physicalWagon) {
|
|
|
|
|
throw new NotFoundException(`Wagon ${assignment.physicalWagonId} not found`);
|
|
|
|
|
@@ -725,55 +502,9 @@ export class TrainSchedulingService {
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
<<<<<<< HEAD
|
|
|
|
|
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",
|
|
|
|
|
=======
|
|
|
|
|
await manager.getRepository(TrainSetWagon).update(assignment.trainSetWagonId, {
|
|
|
|
|
physicalWagonId: assignment.physicalWagonId,
|
|
|
|
|
status: 'RESERVED',
|
|
|
|
|
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
|
|
|
});
|
|
|
|
|
await manager.getRepository(Wagon).update(assignment.physicalWagonId, {
|
|
|
|
|
trainSetWagonId: assignment.trainSetWagonId,
|
|
|
|
|
@@ -950,10 +681,6 @@ export class TrainSchedulingService {
|
|
|
|
|
violations.push('One or more selected bookings are already assigned to a train schedule');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
<<<<<<< HEAD
|
|
|
|
|
const nonContainerBookings = bookings.filter(
|
|
|
|
|
(booking) => booking.freightType !== (dto.assignmentType ?? "CONTAINER"),
|
|
|
|
|
=======
|
|
|
|
|
const bookingTypes = new Set(bookings.map((b) => b.freightType));
|
|
|
|
|
const isMixed = bookingTypes.size > 1;
|
|
|
|
|
const resolvedMode: 'CONTAINER' | 'BULK' | 'MIXED' =
|
|
|
|
|
@@ -968,117 +695,15 @@ export class TrainSchedulingService {
|
|
|
|
|
|
|
|
|
|
const invalidStatus = bookings.filter(
|
|
|
|
|
(b) => !SCHEDULABLE_BOOKING_STATUSES.includes(b.status as 'PAID'),
|
|
|
|
|
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
|
|
|
);
|
|
|
|
|
if (invalidStatus.length) {
|
|
|
|
|
const statuses = [...new Set(invalidStatus.map((b) => b.status))];
|
|
|
|
|
violations.push(
|
|
|
|
|
<<<<<<< HEAD
|
|
|
|
|
`Only ${dto.assignmentType ?? "CONTAINER"} bookings are supported for this assignment`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const invalidStatusBookings = bookings.filter(
|
|
|
|
|
(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 ${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) =>
|
|
|
|
|
booking.originYardId !== dto.originStationId ||
|
|
|
|
|
booking.destinationYardId !== dto.destinationStationId,
|
|
|
|
|
);
|
|
|
|
|
if (routeMismatch) {
|
|
|
|
|
violations.push(
|
|
|
|
|
"Selected bookings must share the same origin and destination as the schedule",
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const dateMismatch = bookings.some(
|
|
|
|
|
(booking) => this.toUtcDateKey(booking.scheduledDate) !== scheduleDateKey,
|
|
|
|
|
);
|
|
|
|
|
if (dateMismatch) {
|
|
|
|
|
violations.push("Selected bookings must share the same schedule date");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const uniqueOriginCount = new Set(
|
|
|
|
|
bookings.map((booking) => booking.originYardId),
|
|
|
|
|
).size;
|
|
|
|
|
if (uniqueOriginCount > 1) {
|
|
|
|
|
violations.push("Selected bookings must share the same origin station");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const uniqueDestinationCount = new Set(
|
|
|
|
|
bookings.map((booking) => booking.destinationYardId),
|
|
|
|
|
).size;
|
|
|
|
|
if (uniqueDestinationCount > 1) {
|
|
|
|
|
violations.push(
|
|
|
|
|
"Selected bookings must share the same destination station",
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const uniqueDateCount = new Set(
|
|
|
|
|
bookings.map((booking) => this.toUtcDateKey(booking.scheduledDate)),
|
|
|
|
|
).size;
|
|
|
|
|
if (uniqueDateCount > 1) {
|
|
|
|
|
violations.push(
|
|
|
|
|
"Selected bookings must share the same preferred departure date",
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const totalWeightTons = this.roundTons(
|
|
|
|
|
bookings.reduce(
|
|
|
|
|
(sum, booking) => sum + Number(booking.cargoTotalWeightVgm ?? 0),
|
|
|
|
|
0,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const wagonPlan = this.allocateBookingsToWagons(
|
|
|
|
|
bookings,
|
|
|
|
|
this.calculateNW5WagonPlan(totalWeightTons, wagonType),
|
|
|
|
|
);
|
|
|
|
|
const totalLengthMeters = this.roundTons(
|
|
|
|
|
wagonPlan.reduce((sum, wagon) => sum + wagon.lengthMeters, 0),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (totalWeightTons > MAX_TRAIN_WEIGHT_TONS) {
|
|
|
|
|
violations.push(
|
|
|
|
|
`Total booking weight ${totalWeightTons}T exceeds max train weight ${MAX_TRAIN_WEIGHT_TONS}T`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (totalLengthMeters > MAX_TRAIN_LENGTH_METERS) {
|
|
|
|
|
violations.push(
|
|
|
|
|
`Total wagon length ${totalLengthMeters}m exceeds max train length ${MAX_TRAIN_LENGTH_METERS}m`,
|
|
|
|
|
=======
|
|
|
|
|
`Only ${SCHEDULABLE_BOOKING_STATUSES.join(', ')} bookings can be scheduled; received: ${statuses.join(', ')}`,
|
|
|
|
|
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
<<<<<<< HEAD
|
|
|
|
|
wagonPlan.length >
|
|
|
|
|
(dto.assignmentType === "BULK" ? MAX_BULK_WAGONS : MAX_CONTAINER_WAGONS)
|
|
|
|
|
) {
|
|
|
|
|
violations.push(
|
|
|
|
|
`Wagon count ${wagonPlan.length} exceeds ${dto.assignmentType === "BULK" ? "bulk" : "container"} limit ${dto.assignmentType === "BULK" ? MAX_BULK_WAGONS : MAX_CONTAINER_WAGONS}`,
|
|
|
|
|
);
|
|
|
|
|
=======
|
|
|
|
|
bookings.some(
|
|
|
|
|
(b) =>
|
|
|
|
|
b.originYardId !== dto.originStationId ||
|
|
|
|
|
@@ -1086,7 +711,6 @@ export class TrainSchedulingService {
|
|
|
|
|
)
|
|
|
|
|
) {
|
|
|
|
|
violations.push('Selected bookings must share the same origin and destination as the schedule');
|
|
|
|
|
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!forceAssign) {
|
|
|
|
|
@@ -1644,139 +1268,7 @@ export class TrainSchedulingService {
|
|
|
|
|
return locomotive;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
<<<<<<< HEAD
|
|
|
|
|
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,
|
|
|
|
|
wagonType: WagonType,
|
|
|
|
|
totalWeightTons: number,
|
|
|
|
|
totalLengthMeters: number,
|
|
|
|
|
wagonPlan: WagonPlanRecord[],
|
|
|
|
|
physicalWagons: Wagon[] = [],
|
|
|
|
|
) {
|
|
|
|
|
const trainSet = manager.getRepository(TrainSet).create({
|
|
|
|
|
locomotiveId: locomotive.id,
|
|
|
|
|
totalWeightTons,
|
|
|
|
|
totalLengthMeters,
|
|
|
|
|
wagonCount: wagonPlan.length,
|
|
|
|
|
status: "ASSIGNED",
|
|
|
|
|
});
|
|
|
|
|
const savedTrainSet = await manager.getRepository(TrainSet).save(trainSet);
|
|
|
|
|
|
|
|
|
|
const wagons = wagonPlan.map((wagon, index) => {
|
|
|
|
|
const physicalWagon = physicalWagons[index];
|
|
|
|
|
const selectedWagonType = physicalWagon?.wagonType ?? wagonType;
|
|
|
|
|
|
|
|
|
|
return manager.getRepository(TrainSetWagon).create({
|
|
|
|
|
trainSetId: savedTrainSet.id,
|
|
|
|
|
wagonTypeId: selectedWagonType.id,
|
|
|
|
|
physicalWagonId: physicalWagon?.id ?? null,
|
|
|
|
|
sequenceNo: wagon.sequenceNo,
|
|
|
|
|
capacityTons: Number(selectedWagonType.capacityTons),
|
|
|
|
|
lengthMeters: Number(selectedWagonType.lengthMeters),
|
|
|
|
|
assignedWeightTons: wagon.assignedWeightTons,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const savedWagons = await manager.getRepository(TrainSetWagon).save(wagons);
|
|
|
|
|
|
|
|
|
|
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(
|
|
|
|
|
manager: EntityManager,
|
|
|
|
|
locomotive: Locomotive,
|
|
|
|
|
) {
|
|
|
|
|
=======
|
|
|
|
|
private async buildEmptyTrainSet(manager: EntityManager, locomotive: Locomotive) {
|
|
|
|
|
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
|
|
|
const trainSet = manager.getRepository(TrainSet).create({
|
|
|
|
|
locomotiveId: locomotive.id,
|
|
|
|
|
totalWeightTons: 0,
|
|
|
|
|
@@ -1787,313 +1279,6 @@ export class TrainSchedulingService {
|
|
|
|
|
return manager.getRepository(TrainSet).save(trainSet);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
<<<<<<< HEAD
|
|
|
|
|
allocateBookingsToWagons(
|
|
|
|
|
bookings: Booking[],
|
|
|
|
|
baseWagonPlan: WagonPlanRecord[],
|
|
|
|
|
): WagonPlanRecord[] {
|
|
|
|
|
const remaining = bookings.map((booking) => ({
|
|
|
|
|
bookingId: booking.id,
|
|
|
|
|
bookingReference: booking.reference,
|
|
|
|
|
remainingWeightTons: this.roundTons(
|
|
|
|
|
Number(booking.cargoTotalWeightVgm ?? 0),
|
|
|
|
|
),
|
|
|
|
|
}));
|
|
|
|
|
let bookingIndex = 0;
|
|
|
|
|
|
|
|
|
|
return baseWagonPlan.map((wagon) => {
|
|
|
|
|
let wagonRemaining = this.roundTons(wagon.capacityTons);
|
|
|
|
|
const allocations: WagonAllocationRecord[] = [];
|
|
|
|
|
let assignedWeightTons = 0;
|
|
|
|
|
|
|
|
|
|
while (wagonRemaining > 0 && bookingIndex < remaining.length) {
|
|
|
|
|
const booking = remaining[bookingIndex];
|
|
|
|
|
const allocatedWeightTons = this.roundTons(
|
|
|
|
|
Math.min(wagonRemaining, booking.remainingWeightTons),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (allocatedWeightTons <= 0) {
|
|
|
|
|
bookingIndex += 1;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
allocations.push({
|
|
|
|
|
bookingId: booking.bookingId,
|
|
|
|
|
bookingReference: booking.bookingReference,
|
|
|
|
|
allocatedWeightTons,
|
|
|
|
|
});
|
|
|
|
|
booking.remainingWeightTons = this.roundTons(
|
|
|
|
|
booking.remainingWeightTons - allocatedWeightTons,
|
|
|
|
|
);
|
|
|
|
|
wagonRemaining = this.roundTons(wagonRemaining - allocatedWeightTons);
|
|
|
|
|
assignedWeightTons = this.roundTons(
|
|
|
|
|
assignedWeightTons + allocatedWeightTons,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (booking.remainingWeightTons <= 0) {
|
|
|
|
|
bookingIndex += 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...wagon,
|
|
|
|
|
assignedWeightTons,
|
|
|
|
|
allocations,
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async getContainerTrainSchedules() {
|
|
|
|
|
const schedules = await this.dataSource.getRepository(TrainSchedule).find({
|
|
|
|
|
relations: {
|
|
|
|
|
trainSet: { locomotive: true },
|
|
|
|
|
route: true,
|
|
|
|
|
originStation: true,
|
|
|
|
|
destinationStation: true,
|
|
|
|
|
scheduleBookings: true,
|
|
|
|
|
},
|
|
|
|
|
order: { scheduledDepartureDate: "DESC", createdAt: "DESC" },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return schedules.map((schedule) => ({
|
|
|
|
|
id: schedule.id,
|
|
|
|
|
scheduleDate: schedule.scheduledDepartureDate,
|
|
|
|
|
routeName: schedule.route?.name ?? null,
|
|
|
|
|
origin:
|
|
|
|
|
schedule.originStation?.label ?? schedule.originStation?.code ?? null,
|
|
|
|
|
destination:
|
|
|
|
|
schedule.destinationStation?.label ??
|
|
|
|
|
schedule.destinationStation?.code ??
|
|
|
|
|
null,
|
|
|
|
|
locomotive: schedule.trainSet?.locomotive
|
|
|
|
|
? {
|
|
|
|
|
id: schedule.trainSet.locomotive.id,
|
|
|
|
|
code: schedule.trainSet.locomotive.code,
|
|
|
|
|
name: schedule.trainSet.locomotive.name ?? null,
|
|
|
|
|
}
|
|
|
|
|
: null,
|
|
|
|
|
wagonCount: schedule.trainSet?.wagonCount ?? 0,
|
|
|
|
|
totalWeightTons: this.roundTons(
|
|
|
|
|
Number(schedule.trainSet?.totalWeightTons ?? 0),
|
|
|
|
|
),
|
|
|
|
|
totalLengthMeters: this.roundTons(
|
|
|
|
|
Number(schedule.trainSet?.totalLengthMeters ?? 0),
|
|
|
|
|
),
|
|
|
|
|
bookingsCount: schedule.scheduleBookings?.length ?? 0,
|
|
|
|
|
status: schedule.status,
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async getContainerTrainScheduleById(id: string) {
|
|
|
|
|
const schedule = await this.dataSource
|
|
|
|
|
.getRepository(TrainSchedule)
|
|
|
|
|
.findOne({
|
|
|
|
|
where: { id },
|
|
|
|
|
relations: {
|
|
|
|
|
route: true,
|
|
|
|
|
trainSet: {
|
|
|
|
|
locomotive: true,
|
|
|
|
|
wagons: { wagonType: true, physicalWagon: true, allocations: { booking: true } },
|
|
|
|
|
},
|
|
|
|
|
originStation: true,
|
|
|
|
|
destinationStation: true,
|
|
|
|
|
scheduleBookings: {
|
|
|
|
|
booking: { company: true, originYard: true, destinationYard: true },
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!schedule) {
|
|
|
|
|
throw new NotFoundException(`Train schedule ${id} not found`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
id: schedule.id,
|
|
|
|
|
status: schedule.status,
|
|
|
|
|
route: schedule.route
|
|
|
|
|
? {
|
|
|
|
|
id: schedule.route.id,
|
|
|
|
|
name: schedule.route.name,
|
|
|
|
|
}
|
|
|
|
|
: null,
|
|
|
|
|
scheduledDepartureDate: schedule.scheduledDepartureDate,
|
|
|
|
|
scheduledArrivalDate: schedule.scheduledArrivalDate,
|
|
|
|
|
originStation: schedule.originStation,
|
|
|
|
|
destinationStation: schedule.destinationStation,
|
|
|
|
|
trainSet: schedule.trainSet
|
|
|
|
|
? {
|
|
|
|
|
id: schedule.trainSet.id,
|
|
|
|
|
status: schedule.trainSet.status,
|
|
|
|
|
wagonCount: schedule.trainSet.wagonCount,
|
|
|
|
|
totalWeightTons: this.roundTons(
|
|
|
|
|
Number(schedule.trainSet.totalWeightTons),
|
|
|
|
|
),
|
|
|
|
|
totalLengthMeters: this.roundTons(
|
|
|
|
|
Number(schedule.trainSet.totalLengthMeters),
|
|
|
|
|
),
|
|
|
|
|
locomotive: schedule.trainSet.locomotive
|
|
|
|
|
? {
|
|
|
|
|
id: schedule.trainSet.locomotive.id,
|
|
|
|
|
code: schedule.trainSet.locomotive.code,
|
|
|
|
|
name: schedule.trainSet.locomotive.name,
|
|
|
|
|
status: schedule.trainSet.locomotive.status,
|
|
|
|
|
maxPullWeightTons: this.roundTons(
|
|
|
|
|
Number(schedule.trainSet.locomotive.maxPullWeightTons),
|
|
|
|
|
),
|
|
|
|
|
maxTrainLengthMeters: this.roundTons(
|
|
|
|
|
Number(schedule.trainSet.locomotive.maxTrainLengthMeters),
|
|
|
|
|
),
|
|
|
|
|
}
|
|
|
|
|
: null,
|
|
|
|
|
wagons: [...(schedule.trainSet.wagons ?? [])]
|
|
|
|
|
.sort((left, right) => left.sequenceNo - right.sequenceNo)
|
|
|
|
|
.map((wagon) => ({
|
|
|
|
|
id: wagon.id,
|
|
|
|
|
sequenceNo: wagon.sequenceNo,
|
|
|
|
|
capacityTons: this.roundTons(Number(wagon.capacityTons)),
|
|
|
|
|
lengthMeters: this.roundTons(Number(wagon.lengthMeters)),
|
|
|
|
|
assignedWeightTons: this.roundTons(
|
|
|
|
|
Number(wagon.assignedWeightTons),
|
|
|
|
|
),
|
|
|
|
|
wagonType: wagon.wagonType
|
|
|
|
|
? {
|
|
|
|
|
id: wagon.wagonType.id,
|
|
|
|
|
code: wagon.wagonType.code,
|
|
|
|
|
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,
|
|
|
|
|
bookingId: allocation.bookingId,
|
|
|
|
|
bookingReference: allocation.booking?.reference ?? null,
|
|
|
|
|
allocatedWeightTons: this.roundTons(
|
|
|
|
|
Number(allocation.allocatedWeightTons),
|
|
|
|
|
),
|
|
|
|
|
})) ?? [],
|
|
|
|
|
})),
|
|
|
|
|
}
|
|
|
|
|
: null,
|
|
|
|
|
bookings:
|
|
|
|
|
schedule.scheduleBookings?.map((scheduleBooking) => ({
|
|
|
|
|
id: scheduleBooking.booking?.id ?? scheduleBooking.bookingId,
|
|
|
|
|
reference: scheduleBooking.booking?.reference ?? null,
|
|
|
|
|
customer:
|
|
|
|
|
scheduleBooking.booking?.company?.name ??
|
|
|
|
|
scheduleBooking.booking?.company?.email ??
|
|
|
|
|
null,
|
|
|
|
|
weightTons: this.roundTons(
|
|
|
|
|
Number(scheduleBooking.booking?.cargoTotalWeightVgm ?? 0),
|
|
|
|
|
),
|
|
|
|
|
status: scheduleBooking.booking?.status ?? null,
|
|
|
|
|
})) ?? [],
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async cancelTrainSchedule(id: string) {
|
|
|
|
|
const schedule = await this.dataSource
|
|
|
|
|
.getRepository(TrainSchedule)
|
|
|
|
|
.findOne({
|
|
|
|
|
where: { id },
|
|
|
|
|
relations: { trainSet: { locomotive: true, wagons: true } },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!schedule) {
|
|
|
|
|
throw new NotFoundException(`Train schedule ${id} not found`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await this.dataSource.transaction(async (manager) => {
|
|
|
|
|
await manager.getRepository(TrainSchedule).update(schedule.id, {
|
|
|
|
|
status: "CANCELLED",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (schedule.trainSetId) {
|
|
|
|
|
await manager.getRepository(TrainSet).update(schedule.trainSetId, {
|
|
|
|
|
status: "CANCELLED",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (schedule.trainSet?.locomotiveId) {
|
|
|
|
|
await manager
|
|
|
|
|
.getRepository(Locomotive)
|
|
|
|
|
.update(schedule.trainSet.locomotiveId, {
|
|
|
|
|
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) },
|
|
|
|
|
relations: {
|
|
|
|
|
company: true,
|
|
|
|
|
originYard: true,
|
|
|
|
|
destinationYard: true,
|
|
|
|
|
bookingContainers: { containerType: true },
|
|
|
|
|
},
|
|
|
|
|
order: { createdAt: "ASC" },
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
=======
|
|
|
|
|
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
|
|
|
private async getActiveRoute(routeId: string) {
|
|
|
|
|
const route = await this.dataSource.getRepository(Route).findOne({
|
|
|
|
|
where: { id: routeId },
|
|
|
|
|
@@ -2104,18 +1289,6 @@ export class TrainSchedulingService {
|
|
|
|
|
return route;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
<<<<<<< HEAD
|
|
|
|
|
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);
|
|
|
|
|
=======
|
|
|
|
|
private mapEligibleBooking(booking: Booking) {
|
|
|
|
|
return {
|
|
|
|
|
id: booking.id,
|
|
|
|
|
@@ -2137,7 +1310,6 @@ export class TrainSchedulingService {
|
|
|
|
|
preferredDepartureDate: booking.scheduledDate.toISOString(),
|
|
|
|
|
status: booking.status,
|
|
|
|
|
};
|
|
|
|
|
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private resolveScheduleFreightType(
|
|
|
|
|
|