fix(train-scheduling): scope the not-loaded dispatch guard to EXPORT only

The guard fired on every dispatch. Loading out of an origin warehouse is an
export concept — import cargo isn't loaded from a warehouse, so its warehouse
inventory says nothing about what's aboard and the check would have blocked
legitimate import dispatches. Derive the route direction (reusing
deriveTradeDirection, as the warehouse loading queue does) and return early for
anything that isn't EXPORT. Import and domestic behave exactly as before.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hagernesh
2026-07-16 13:21:55 +00:00
parent 2798fed6d8
commit d9db4cbc0c

View File

@@ -2031,16 +2031,37 @@ export class TrainSchedulingService {
}
/**
* 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.
* EXPORT ONLY. An export train must not leave carrying nothing while its cargo
* sits in the shed: the goods are received into the origin warehouse, GRN'd and
* loaded onto the wagons allocated to the booking, so anything still in the
* warehouse at dispatch is being left behind. Blocks dispatch when an allocated
* booking has warehouse inventory that never made it onto a wagon (received /
* stored / ready but not LOADED) — either load it from the Load-to-Train queue,
* or drop the booking's wagon allocation so it rides a later train.
*
* Import/domestic are untouched: their cargo isn't loaded out of an origin
* warehouse, so warehouse inventory says nothing about what's aboard.
*
* 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 [route]: Array<{ originCountry: string | null; destinationCountry: string | null }> =
await this.dataSource.query(
`SELECT oy.country AS "originCountry", dy.country AS "destinationCountry"
FROM freight.train_schedules ts
LEFT JOIN freight.yards oy ON oy.id = ts.origin_station_id
LEFT JOIN freight.yards dy ON dy.id = ts.destination_station_id
WHERE ts.id = $1 AND ts.deleted_at IS NULL`,
[scheduleId],
);
if (!route) return;
const direction = deriveTradeDirection(
{ country: route.originCountry },
{ country: route.destinationCountry },
);
if (direction !== 'EXPORT') return;
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"
@@ -2070,7 +2091,7 @@ 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.
// Export only: don't leave received cargo behind in the warehouse.
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.