mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 18:48:11 +00:00
add goverment booking
This commit is contained in:
@@ -1886,6 +1886,11 @@ export class TrainSchedulingService {
|
||||
}
|
||||
|
||||
const booking = await this.bookingsRepository.findById(bookingId);
|
||||
if (booking?.isGovernment) {
|
||||
throw new BadRequestException(
|
||||
'Government bookings cannot be removed from a train. They can only be switched onto another allocation.',
|
||||
);
|
||||
}
|
||||
const bookingReference = booking?.reference ?? null;
|
||||
|
||||
await this.dataSource.transaction(async (manager) => {
|
||||
@@ -5384,6 +5389,7 @@ export class TrainSchedulingService {
|
||||
booking.destinationYard?.label ?? booking.destinationYard?.code ?? 'Unknown destination',
|
||||
preferredDepartureDate: booking.scheduledDate?.toISOString() ?? null,
|
||||
status: booking.status,
|
||||
isGovernment: Boolean(booking.isGovernment),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7189,6 +7195,7 @@ export class TrainSchedulingService {
|
||||
// dispatch. Defaults UNLOADED for links written before the column.
|
||||
loadingStatus: sb.loadingStatus ?? LoadingStatus.Unloaded,
|
||||
wagonAssigned: allocatedBookingIds.has(sb.booking?.id ?? sb.bookingId),
|
||||
isGovernment: Boolean(sb.booking?.isGovernment),
|
||||
})) ?? [],
|
||||
// Ordered corridor stops (route milestones; falls back to the two
|
||||
// endpoints) — lets the UI draw per-segment occupancy and label legs.
|
||||
@@ -7361,6 +7368,142 @@ export class TrainSchedulingService {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Government-priority switch: free wagons by unassigning the selected
|
||||
* commercial bookings, then allocate the government booking in their place.
|
||||
* The gov booking must need no more wagons than the switched-out bookings
|
||||
* free (ops selects more bookings otherwise), and the post-switch
|
||||
* composition is fully validated BEFORE anything is unassigned so a failing
|
||||
* switch never leaves the train half-emptied.
|
||||
*/
|
||||
async switchGovernmentBooking(
|
||||
scheduleId: string,
|
||||
governmentBookingId: string,
|
||||
removeBookingIds: string[],
|
||||
userId?: string,
|
||||
) {
|
||||
if (removeBookingIds.includes(governmentBookingId)) {
|
||||
throw new BadRequestException('Government booking cannot be switched out by itself');
|
||||
}
|
||||
|
||||
const schedule = await this.trainSchedulesRepository.findByIdWithFullGraph(scheduleId);
|
||||
if (!schedule) {
|
||||
throw new NotFoundException(`Train schedule ${scheduleId} not found`);
|
||||
}
|
||||
if (!['DRAFT', 'SCHEDULED'].includes(schedule.status)) {
|
||||
throw new BadRequestException(
|
||||
`Cannot switch bookings on a schedule in status ${schedule.status}`,
|
||||
);
|
||||
}
|
||||
|
||||
const [govBooking] = await this.bookingsRepository.findByIdsForScheduling([
|
||||
governmentBookingId,
|
||||
]);
|
||||
if (!govBooking) {
|
||||
throw new NotFoundException(`Booking ${governmentBookingId} not found`);
|
||||
}
|
||||
if (!govBooking.isGovernment) {
|
||||
throw new BadRequestException('Only government bookings can be switched onto a train');
|
||||
}
|
||||
|
||||
const wagonAssignedIds = await this.getWagonAssignedBookingIds(scheduleId);
|
||||
if (wagonAssignedIds.has(governmentBookingId)) {
|
||||
throw new BadRequestException('Government booking is already allocated on this train');
|
||||
}
|
||||
|
||||
const removed = await this.bookingsRepository.findByIdsForScheduling(removeBookingIds);
|
||||
if (removed.length !== removeBookingIds.length) {
|
||||
throw new NotFoundException('One or more bookings to switch out were not found');
|
||||
}
|
||||
const notOnTrain = removed.filter((b) => !wagonAssignedIds.has(b.id));
|
||||
if (notOnTrain.length) {
|
||||
throw new BadRequestException(
|
||||
`Not allocated on this train: ${notOnTrain.map((b) => b.reference).join(', ')}`,
|
||||
);
|
||||
}
|
||||
const govRemoved = removed.filter((b) => b.isGovernment);
|
||||
if (govRemoved.length) {
|
||||
throw new BadRequestException(
|
||||
`Government bookings cannot be switched out: ${govRemoved.map((b) => b.reference).join(', ')}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Dry-run the post-switch composition: survivors + the gov booking.
|
||||
const survivorIds = [...wagonAssignedIds].filter((id) => !removeBookingIds.includes(id));
|
||||
const previewDto = {
|
||||
bookingIds: [...survivorIds, governmentBookingId],
|
||||
scheduleDate: schedule.scheduledDepartureDate.toISOString(),
|
||||
originStationId: schedule.originStationId,
|
||||
destinationStationId: schedule.destinationStationId,
|
||||
};
|
||||
const limits = await this.resolveTrainLimitConfig(
|
||||
undefined,
|
||||
trainSetLocomotiveLimits(schedule.trainSet),
|
||||
);
|
||||
const validation = await this.validateBookingsForScheduling(
|
||||
previewDto,
|
||||
null,
|
||||
false,
|
||||
[],
|
||||
false,
|
||||
limits,
|
||||
scheduleId,
|
||||
);
|
||||
const freedWagons = removed.reduce((sum, b) => sum + Number(b.wagonsRequired ?? 0), 0);
|
||||
if (!validation.valid) {
|
||||
throw new BadRequestException({
|
||||
message: `Switch validation failed: ${validation.violations.join('; ')}`,
|
||||
violations: validation.violations,
|
||||
warnings: validation.warnings,
|
||||
});
|
||||
}
|
||||
if (!validation.bookings.some((b) => b.id === governmentBookingId)) {
|
||||
throw new BadRequestException(
|
||||
`Switching out ${removed.map((b) => b.reference).join(', ')} frees ${freedWagons} wagon(s) — not enough for this government booking. Select more bookings to switch out.`,
|
||||
);
|
||||
}
|
||||
const govWagons = sumWagonsRequired(govBooking, validation.wagonPlan);
|
||||
if (govWagons > freedWagons) {
|
||||
throw new BadRequestException(
|
||||
`Government booking needs ${govWagons} wagon(s) but the selected bookings free only ${freedWagons}. Select more bookings to switch out.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Same container-number gate as single-booking assignment, applied to the
|
||||
// incoming gov booking only.
|
||||
const containerBookings = validation.bookings.filter((b) => b.freightType === 'CONTAINER');
|
||||
const units: ContainerUnitForPlacement[] = expandBookingContainerUnits(containerBookings);
|
||||
const slots = getContainerSlotSequenceNos(validation.wagonPlan);
|
||||
const placements = autoFillPlacements(units, slots);
|
||||
const missingForGov = findMissingContainerNumberIssues(units, placements).find(
|
||||
(m) => m.bookingId === governmentBookingId,
|
||||
);
|
||||
if (missingForGov) {
|
||||
throw new BadRequestException({
|
||||
message: missingForGov.issue,
|
||||
violations: [missingForGov.issue],
|
||||
});
|
||||
}
|
||||
|
||||
// ponytail: unassign + assign run as sequential own-transaction steps, not
|
||||
// one atomic unit — the dry-run above means the assign step can only fail
|
||||
// on a concurrent edit; staff re-add from the eligible pool if it does.
|
||||
for (const booking of removed) {
|
||||
await this.unassignBooking(scheduleId, booking.id, userId);
|
||||
}
|
||||
|
||||
const assignableSet = new Set(validation.bookings.map((b) => b.id));
|
||||
const assignPlacements = placementsForBookings(placements, assignableSet, units);
|
||||
return this.assignBookingsToSchedule(
|
||||
scheduleId,
|
||||
{
|
||||
bookingIds: validation.bookings.map((b) => b.id),
|
||||
containerPlacements: containerBookings.length > 0 ? assignPlacements : undefined,
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
}
|
||||
|
||||
/** Preview wagon allocation issues per linked booking without mutating the schedule. */
|
||||
async previewAllocationForSchedule(
|
||||
scheduleId: string,
|
||||
|
||||
Reference in New Issue
Block a user