fix(train-scheduling): block dispatch when allocated cargo is not loaded

dispatchSchedule guarded status, Djibouti departure rules, locomotives and
wagons — but never checked the cargo. A train could be dispatched while the
bookings allocated to it sat received in the warehouse, silently leaving them
behind. Dispatch now refuses when an allocated booking has warehouse inventory
in RECEIVED/STORED/READY_FOR_LOADING, naming the bookings and pointing at the
two ways out: load them, or drop the wagon allocation so they ride a later
train. Bookings with no inventory at all are not blocked — allocating a wagon
before the goods arrive is normal planning.

Also drops RESERVED from the Load-to-Train filters: reserved stock is not
awaiting loading.

The sched_bookings CTE moves to common/schedule-bookings.sql so the warehouse
loading queue and this dispatch guard resolve a train's bookings identically —
if they drift, a train departs leaving cargo the warehouse still expects to load.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hagernesh
2026-07-16 13:09:20 +00:00
parent e115b9b753
commit 2798fed6d8
3 changed files with 70 additions and 26 deletions

View File

@@ -19,6 +19,7 @@ import {
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { InjectDataSource } from '@nestjs/typeorm';
import { SCHEDULE_BOOKINGS_CTE } from '../../common/schedule-bookings.sql';
import {
DataSource,
EntityManager,
@@ -2029,6 +2030,37 @@ export class TrainSchedulingService {
return this.getTrainScheduleById(scheduleId);
}
/**
* A train must not leave carrying nothing while its cargo sits in the shed.
* Blocks dispatch when a booking allocated to this train has warehouse
* inventory that never made it onto a wagon (received / stored / ready but not
* LOADED). Either load it from the warehouse Load-to-Train queue, or drop the
* booking's wagon allocation so it travels on a later train.
*
* Bookings with no warehouse inventory at all are NOT blocked — allocating a
* wagon before the goods arrive is normal planning; they simply aren't aboard.
*/
private async assertAllocatedCargoLoaded(scheduleId: string): Promise<void> {
const rows: Array<{ reference: string | null; status: string }> = await this.dataSource.query(
`WITH ${SCHEDULE_BOOKINGS_CTE}
SELECT DISTINCT b.reference AS "reference", inv.status AS "status"
FROM sched_bookings sb
JOIN freight.bookings b ON b.id = sb.booking_id AND b.deleted_at IS NULL
JOIN freight.warehouse_inventory inv
ON inv.booking_id = b.id AND inv.deleted_at IS NULL
WHERE sb.schedule_id = $1
AND inv.status IN ('RECEIVED', 'STORED', 'READY_FOR_LOADING')`,
[scheduleId],
);
if (rows.length) {
const refs = [...new Set(rows.map((r) => r.reference ?? '?'))].join(', ');
throw new BadRequestException(
`Cannot dispatch: cargo for booking(s) ${refs} is in the warehouse but not loaded onto a wagon. ` +
`Load it from the warehouse Load-to-Train queue, or remove the booking's wagon allocation so it travels on a later train.`,
);
}
}
async dispatchSchedule(scheduleId: string) {
const schedule = await this.trainSchedulesRepository.findByIdWithFullGraph(scheduleId);
if (!schedule) {
@@ -2038,6 +2070,8 @@ export class TrainSchedulingService {
throw new BadRequestException('Only SCHEDULED trains can be dispatched');
}
await this.assertImportDjiboutiMayDepart(schedule);
// Don't leave received cargo behind on the platform.
await this.assertAllocatedCargoLoaded(scheduleId);
// A locomotive may sit on many future schedules, but it can only pull one train
// at a time — block dispatch while any set locomotive is out on a dispatched train.
const setLocomotiveIds = this.locomotivesOfTrainSet(schedule.trainSet).map((l) => l.id);