fix(warehouse): resolve Load-to-Train bookings from wagon allocation

The Load-to-Train queue was permanently empty for real traffic. loadableTrains()
and trainLoadableItems() gated on freight.train_schedule_bookings, but nothing in
the application writes that table — only the demo seeders do. Real wagon
allocation writes wagon_booking_allocations, reached via
train_schedules -> train_sets -> train_set_wagons, so an allocated export booking
never satisfied the EXISTS gate and no train ever appeared.

Both queries now resolve a schedule's bookings through a shared sched_bookings
CTE that unions the wagon-allocation chain with train_schedule_bookings, so real
allocations show up and the seeded demo scenarios keep working. The panel already
groups the returned rows by booking with their containers, so the queue now lists
the train, its bookings and their containers for selection.

Export flow this serves: booked -> paid -> received at the warehouse (first-mile
or self-haul) -> GRN -> loaded onto the wagons allocated to the booking.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hagernesh
2026-07-16 12:43:54 +00:00
parent 6c5ab98e4e
commit e115b9b753

View File

@@ -1542,11 +1542,38 @@ export class WarehouseInventoryService {
// their already-allocated wagons. Reuses the single-item load() machinery.
/** Pre-dispatch EXPORT trains that have inventory waiting to be (or already) loaded. */
/**
* The bookings riding a train schedule.
*
* Export flow: booked -> paid -> received at the warehouse (first-mile or
* self-haul) -> GRN -> loaded onto the wagons allocated to it. A booking
* actually reaches a train through WAGON ALLOCATION
* (train_schedules -> train_sets -> train_set_wagons -> wagon_booking_allocations),
* which is what the allocation UI writes. `train_schedule_bookings` is only
* ever written by the demo seeders — keying off it alone left this queue
* permanently empty for real traffic — so both sources are unioned.
*/
private readonly SCHEDULE_BOOKINGS_CTE = `
sched_bookings AS (
SELECT ts.id AS schedule_id, wba.booking_id
FROM freight.train_schedules ts
JOIN freight.train_set_wagons tsw
ON tsw.train_set_id = ts.train_set_id AND tsw.deleted_at IS NULL
JOIN freight.wagon_booking_allocations wba
ON wba.train_set_wagon_id = tsw.id AND wba.deleted_at IS NULL
WHERE ts.deleted_at IS NULL
UNION
SELECT tsb.train_schedule_id, tsb.booking_id
FROM freight.train_schedule_bookings tsb
WHERE tsb.deleted_at IS NULL
)`;
async loadableTrains(): Promise<LoadableTrainRow[]> {
const rows: Array<
LoadableTrainRow & { originCountry: string | null; destinationCountry: string | null }
> = await this.dataSource.query(
`SELECT ts.id AS "scheduleId",
`WITH ${this.SCHEDULE_BOOKINGS_CTE}
SELECT ts.id AS "scheduleId",
ts.train_number AS "trainNumber",
oy.code AS "origin",
dy.code AS "destination",
@@ -1554,15 +1581,15 @@ export class WarehouseInventoryService {
dy.country AS "destinationCountry",
ts.status AS "status",
ts.scheduled_departure_date AS "departureTime",
(SELECT count(*) FROM freight.train_schedule_bookings tsb
(SELECT count(*) FROM sched_bookings sb
JOIN freight.warehouse_inventory inv
ON inv.booking_id = tsb.booking_id AND inv.deleted_at IS NULL
WHERE tsb.train_schedule_id = ts.id AND tsb.deleted_at IS NULL
ON inv.booking_id = sb.booking_id AND inv.deleted_at IS NULL
WHERE sb.schedule_id = ts.id
AND inv.status IN ('RECEIVED','STORED','RESERVED','READY_FOR_LOADING')) AS "readyCount",
(SELECT count(*) FROM freight.train_schedule_bookings tsb
(SELECT count(*) FROM sched_bookings sb
JOIN freight.warehouse_inventory inv
ON inv.booking_id = tsb.booking_id AND inv.deleted_at IS NULL
WHERE tsb.train_schedule_id = ts.id AND tsb.deleted_at IS NULL
ON inv.booking_id = sb.booking_id AND inv.deleted_at IS NULL
WHERE sb.schedule_id = ts.id
AND inv.status = 'LOADED') AS "loadedCount"
FROM freight.train_schedules ts
LEFT JOIN freight.yards oy ON oy.id = ts.origin_station_id
@@ -1570,10 +1597,10 @@ export class WarehouseInventoryService {
WHERE ts.deleted_at IS NULL
AND ts.status = ANY($1)
AND EXISTS (
SELECT 1 FROM freight.train_schedule_bookings tsb2
SELECT 1 FROM sched_bookings sb2
JOIN freight.warehouse_inventory inv2
ON inv2.booking_id = tsb2.booking_id AND inv2.deleted_at IS NULL
WHERE tsb2.train_schedule_id = ts.id AND tsb2.deleted_at IS NULL
ON inv2.booking_id = sb2.booking_id AND inv2.deleted_at IS NULL
WHERE sb2.schedule_id = ts.id
AND inv2.status IN ('RECEIVED','STORED','RESERVED','READY_FOR_LOADING','LOADED')
)
ORDER BY ts.scheduled_departure_date ASC NULLS LAST`,
@@ -1599,7 +1626,8 @@ export class WarehouseInventoryService {
*/
async trainLoadableItems(scheduleId: string): Promise<TrainLoadableItemRow[]> {
const rows: Array<Omit<TrainLoadableItemRow, 'loadable'>> = await this.dataSource.query(
`SELECT inv.id AS "id",
`WITH ${this.SCHEDULE_BOOKINGS_CTE}
SELECT inv.id AS "id",
inv.booking_id AS "bookingId",
b.reference AS "bookingReference",
company.name AS "customerName",
@@ -1612,9 +1640,9 @@ export class WarehouseInventoryService {
wl.wagon_id AS "wagonId",
wl.wagon_number AS "wagonNumber",
wl.sequence_no AS "sequenceNo"
FROM freight.train_schedule_bookings tsb
JOIN freight.train_schedules ts ON ts.id = tsb.train_schedule_id
JOIN freight.bookings b ON b.id = tsb.booking_id AND b.deleted_at IS NULL
FROM sched_bookings sb
JOIN freight.train_schedules ts ON ts.id = sb.schedule_id
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
LEFT JOIN freight.companies company ON company.id = b.company_id
LEFT JOIN freight.cargo_types cgt ON cgt.id = b.cargo_type_id
@@ -1631,7 +1659,7 @@ export class WarehouseInventoryService {
ORDER BY tsw.sequence_no ASC NULLS LAST
LIMIT 1
) wl ON true
WHERE tsb.train_schedule_id = $1 AND tsb.deleted_at IS NULL
WHERE sb.schedule_id = $1
AND inv.status IN ('RECEIVED','STORED','RESERVED','READY_FOR_LOADING','LOADED')
ORDER BY wl.sequence_no ASC NULLS LAST, b.reference ASC NULLS LAST, ct.container_number ASC NULLS LAST`,
[scheduleId],