From a411e0bdd0859eb19a566c12f63ab3d85d6edeba Mon Sep 17 00:00:00 2001 From: Hagernesh Date: Fri, 17 Jul 2026 09:55:06 +0000 Subject: [PATCH] feat(intercity): only load/unload where the yard has a facility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Intercity cargo rides a passing train and is handled at the booking's own yards, so those yards need the equipment to do it — a train stopping somewhere is not the same as somewhere being able to load it. Loading at an origin or unloading at a destination without has_facility is now refused, naming the yard. The check sits inside loadBooking/unloadBooking rather than the intercity controller wrapper, because recording a checkpoint auto-unloads every booking destined at that yard (autoUnloadAtYard) and would otherwise route around it. Import/export are untouched: their cargo is handled at the route's terminal ports, not at an arbitrary mid-corridor yard, so the gate returns early for anything that isn't DOMESTIC. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../booking-journey.service.ts | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/apps/edr-freight-api/src/modules/train-scheduling/booking-journey.service.ts b/apps/edr-freight-api/src/modules/train-scheduling/booking-journey.service.ts index 426bd37be..601ba91ee 100644 --- a/apps/edr-freight-api/src/modules/train-scheduling/booking-journey.service.ts +++ b/apps/edr-freight-api/src/modules/train-scheduling/booking-journey.service.ts @@ -9,6 +9,7 @@ import { InjectDataSource } from '@nestjs/typeorm'; import { DataSource, EntityManager, In } from 'typeorm'; import { Freight } from '@edr/types'; +import { YardFacilitiesService } from '../rule-engine/services/yard-facilities.service'; import { Booking } from '../bookings/entities/booking.entity'; import { ClearanceMilestoneService } from '../contracts/clearance-milestone.service'; import { Yard } from '../rule-engine/entities/yard.entity'; @@ -43,6 +44,7 @@ export class BookingJourneyService { constructor( @InjectDataSource() private readonly dataSource: DataSource, + private readonly yardFacilities: YardFacilitiesService, @Optional() private readonly milestoneService?: ClearanceMilestoneService, ) {} @@ -63,6 +65,7 @@ export class BookingJourneyService { ); } await this.assertTrainAtYard(schedule, booking.originYardId, 'origin'); + await this.assertYardCanHandleCargo(booking, booking.originYardId, 'origin'); const now = new Date(); await this.dataSource.transaction(async (manager) => { @@ -99,6 +102,7 @@ export class BookingJourneyService { ); } await this.assertTrainAtYard(schedule, booking.destinationYardId, 'destination'); + await this.assertYardCanHandleCargo(booking, booking.destinationYardId, 'destination'); // Intercity has no clearance/delivery tail — unloading completes it. Import/ // export continue into clearance, keyed on the booking's own arrival. @@ -306,6 +310,33 @@ export class BookingJourneyService { }); } + /** + * INTERCITY ONLY. Intercity cargo rides a passing train and is handled at the + * booking's own yards, so those yards need the equipment to do it — a train + * stopping somewhere is not the same as somewhere being able to load it. + * + * Import/export are untouched: their cargo is handled at the route's terminal + * ports, not at an arbitrary mid-corridor yard, and gating them here would + * block existing traffic. + * + * Lives here rather than in the controller so the checkpoint-driven + * autoUnloadAtYard path cannot route around it. + */ + private async assertYardCanHandleCargo( + booking: Booking, + yardId: string, + side: 'origin' | 'destination', + ): Promise { + if (booking.tradeDirection !== 'DOMESTIC') return; + const facility = await this.yardFacilities.facilityForYard(yardId); + if (!facility?.hasFacility) { + throw new BadRequestException( + `${facility?.yardLabel ?? 'This yard'} has no load/unload facility — an intercity booking cannot be ` + + `${side === 'origin' ? 'loaded at its origin' : 'unloaded at its destination'} here.`, + ); + } + } + /** * The train is "at" a yard when the latest recorded checkpoint is that yard, * or — for a booking boarding at the train's own origin — when the train has