add goverment booking

This commit is contained in:
Marshal
2026-07-31 00:41:51 +00:00
parent 34e4a7d13f
commit 65f18b4796
35 changed files with 913 additions and 88 deletions

View File

@@ -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,