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