Intercity cargo is loaded at its origin yard and unloaded at its destination, but
only some yards have the equipment. EDR's facilities are Indode, Sebeta, Modjo,
Adama and Dire Dawa — and the set grows, so it has to be data.
- yards.has_facility marks a yard as a load/unload point; the new yard_facilities
record says what it can do. Only Indode stores cargo (has_warehouse), so only it
accrues storage/demurrage — the rest just move cargo on and off the train.
- facility_handling_events records each load/unload and carries its GRN.
warehouse_inventory cannot: its warehouse/yard/zone are NOT NULL, so a facility
without a warehouse could never have a row. inventory_id links to the storage
record when there is one.
- YardFacilitiesService.facilityForYard is the single resolver the handling flows
share, so they cannot drift on what a facility is.
- The seeder flags EXISTING yards and creates none. The codes are historical and
do not read like the facility names — Indode is KALITY ("Gelan Multi Purpose
Port (Indode)") and Sebeta is LEGACY_DEST — so it maps by code. Creating fresh
INDODE/SEBETA yards would have split data that routes and bookings already
reference.
Negad is deliberately absent: NAGAD ("DCT/SGDT") is in Djibouti while
NEGAD_FY_BCC is in Ethiopia and inactive, and which one is the intercity facility
is unsettled.
No behaviour change yet — nothing reads has_facility until the gate lands.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
EDR last-mile is multi-truck but was modelled as one: setVehicles accepted any
number of trucks with no validation, arrival/delivery were stamped once per
last_mile record (N trucks shared one timestamp), EDR trucks got no exit paper,
and the per-truck EDR handover never happened because deliver() resolved the
plate via last_mile_container_allocations — a table nothing writes.
- Migration 2260000000000: per-truck arrived_at/departed_at/gross_weight_tons/
net_weight_tons on last_mile_vehicle_assignments, plus a
last_mile_vehicle_containers child table (a truck holds 1x40ft OR 2x20ft, so
the single container_number scalar could not express a load). Weights are
TONNES and named accordingly — the older gross_weight_kg lies about its unit.
- setVehicles: enforce the size rule (one 40ft, or two 20ft), container
membership, one-container-one-truck, and never more trucks than containers.
Bulk carries no containers and is instead gated on tonnage remaining.
- Bulk drawdown: remainingTonsForBooking = booking VGM minus the net weighed off
every departed truck (both tonnes, no conversion), exposed as
GET /last-mile/booking/:bookingId/remaining-tons.
- release() now stamps the EDR truck's own arrival and exit (matched by plate, so
it works for bulk too) alongside the existing customer-truck stamp. The exit
weighing itself is untouched.
- New GET /warehouse-inventory/edr-truck-exit-paper/:assignmentId — per-truck
exit paper for EDR trucks. Deliberately not signature-gated: EDR handovers are
generated at delivery, after the truck has left.
- deliver(): resolve the handover plate from the truck's own containers instead
of the dead allocations table, so EDR handovers are genuinely per-truck.
- Notify the customer (portal inbox + SMS/email) when a truck leaves — one hook
in release() covers both self-haul and EDR, since it is the single exit path.
Self-haul is intentionally unchanged (one booking-level handover signed once).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Removed the field from and related components.
- Updated to reflect the removal of the reopen delay input field.
- Modified to include new train number fields: and .
- Added interface to manage active schedules with trade direction.
- Introduced interface to track wagon shortages in bookings.
- Updated logic to ensure consistent UI state representation.
- Created migrations to drop the column and add and columns to the table.
- Added tests for the new booking window display logic and wagon planning functionality.
- Added parameter to and for server-side free-text search on contract reference, company name, and booking details.
- Introduced new validation errors in for container clashes and space issues when creating bookings.
- Implemented paginated dropdown settings retrieval in .
- Updated to fetch active yards using a new method that handles pagination.
- Enhanced with a method to fetch all records by walking through pages.
- Refactored to support filtering and pagination in schedule listings.
- Improved to return a paginated list of facilities.
- Updated UI components in and to utilize debounced search inputs for better performance.
- Added alerts in to inform users about booking constraints related to splits and capacity.
- Enhanced to display notifications for split bookings and capacity usage.
"Auto Load Ready Items" now opens a train picker (pre-dispatch trains with
these bookings assigned, via the existing loadable-trains flow). No train
available -> no auto-loading, with a clear notice. Loading goes through the
existing per-wagon load path, so items without an allocated wagon are skipped
with a reason.
The train association is stored on the existing warehouse_loadings table
(no new table needed): new train_schedule_id column + a note recording train
number, origin -> destination, and departure time; wagon_id becomes nullable.
The trainless load-passed-export endpoint, its frontend wiring, and the unused
useLoadPassedExport hook are removed.
Migration 2100000000000 (idempotent) also applied to the dev database.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
AddGrnNumberToWarehouseInventory1828000000000 is recorded in the migrations
table but the column is absent - it was added, then dropped out-of-band. Because
TypeORM has the original recorded it will never re-run, so every GRN read/write
fails with "column grn_number does not exist":
- bulkReceive() INSERT names grn_number (receive to warehouse)
- importQueueByStatuses() Unloaded + Dispatch queues
- exportInventoryByStatus() Received / Ready-To-Load / Loaded tabs
- grnDocument() GRN PDF
Re-adds the column, backfills from the "GRN Number:" receive note, recreates the
partial index. Idempotent, and down() is a deliberate no-op so reverting the
repair cannot re-introduce the outage.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Removed maxWagonsPerTrain from WagonType entity and related DTOs.
- Updated containerWagonsForLines function to calculate required wagons based on container lines more accurately.
- Added unit tests for containerWagonsForLines to ensure correct calculations.
- Adjusted related services and scripts to reflect the removal of maxWagonsPerTrain.
- Enhanced booking and contract components to use new status labels for better user experience.
- Implemented validation for unique container numbers in shipment forms.