From 84c3f7a58403d8a074a7db9f03a2ec4b384cf5d1 Mon Sep 17 00:00:00 2001 From: Marshal Date: Fri, 7 Aug 2026 12:22:43 +0000 Subject: [PATCH] feat(bookings): pending export requests hold wagons --- .../train-scheduling/booking-batch.service.ts | 40 ++++++++++++++++--- .../src/modules/wagons/wagons.module.ts | 2 +- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/apps/edr-freight-api/src/modules/train-scheduling/booking-batch.service.ts b/apps/edr-freight-api/src/modules/train-scheduling/booking-batch.service.ts index 5185dcd99..e89c56b2a 100644 --- a/apps/edr-freight-api/src/modules/train-scheduling/booking-batch.service.ts +++ b/apps/edr-freight-api/src/modules/train-scheduling/booking-batch.service.ts @@ -859,7 +859,9 @@ export class BookingBatchService implements OnModuleInit { const locomotive = trainSetLocomotiveLimits(schedule?.trainSet); if (!schedule || !locomotive) continue; const limits = await this.capacityLimits(locomotive); - const budget = await this.remainingBudget(schedule, limits, wagonDims); + const budget = await this.remainingBudget(schedule, limits, wagonDims, [ + booking.id, + ]); const leg = budget.legOf(booking.originYardId, booking.destinationYardId); if (!leg) continue; // this train's route doesn't carry the booking's leg report.corridorMatched = true; @@ -1012,7 +1014,9 @@ export class BookingBatchService implements OnModuleInit { const locomotive = trainSetLocomotiveLimits(schedule?.trainSet); if (!schedule || !locomotive) continue; const limits = await this.capacityLimits(locomotive); - const budget = await this.remainingBudget(schedule, limits, wagonDims); + const budget = await this.remainingBudget(schedule, limits, wagonDims, [ + booking.id, + ]); const leg = budget.legOf(booking.originYardId, booking.destinationYardId); if (!leg) continue; // this train's route doesn't carry the booking's leg const room = budget.remainingFor(leg); @@ -1128,7 +1132,9 @@ export class BookingBatchService implements OnModuleInit { const locomotive = trainSetLocomotiveLimits(schedule?.trainSet); if (!schedule || !locomotive) continue; const limits = await this.capacityLimits(locomotive); - const budget = await this.remainingBudget(schedule, limits, wagonDims); + const budget = await this.remainingBudget(schedule, limits, wagonDims, [ + booking.id, + ]); const leg = budget.legOf(booking.originYardId, booking.destinationYardId); if (!leg) continue; // this train's route doesn't carry the booking's leg const room = budget.remainingFor(leg); @@ -1281,7 +1287,9 @@ export class BookingBatchService implements OnModuleInit { if (!schedule || !locomotive) return false; const wagonDims = await this.loadWagonDims(); const limits = await this.capacityLimits(locomotive); - const budget = await this.remainingBudget(schedule, limits, wagonDims); + const budget = await this.remainingBudget(schedule, limits, wagonDims, [ + booking.id, + ]); const leg = budget.legOf(booking.originYardId, booking.destinationYardId); if (!leg) return false; @@ -1382,7 +1390,12 @@ export class BookingBatchService implements OnModuleInit { } const wagonDims = await this.loadWagonDims(); const limits = await this.capacityLimits(locomotive); - const budget = await this.remainingBudget(schedule, limits, wagonDims); + const budget = await this.remainingBudget( + schedule, + limits, + wagonDims, + bookings.map((b) => b.id), + ); const leg = budget.legOf( bookings[0].originYardId, bookings[0].destinationYardId, @@ -4745,6 +4758,7 @@ export class BookingBatchService implements OnModuleInit { schedule: TrainSchedule, limits: TrainLimits, wagonDims: WagonDims, + excludeBookingIds?: string[], ): Promise { const physicalWagons = await this.builtTrainWagonCount(schedule); if (physicalWagons != null) { @@ -4780,7 +4794,21 @@ export class BookingBatchService implements OnModuleInit { b.status === "PAID" || !payWindowLapsed(b.paymentDeadline, deadlineCutoff), ); - for (const b of [...allocated, ...reserved]) { + // Export FCFS: a customer's pending operation request HOLDS its wagons from + // the moment it is submitted — the request named this exact train + // (requestedTrainScheduleId), so its capacity must not be shown to or + // booked by anyone else while staff review it. The booking(s) being + // evaluated are excluded so a request never blocks its own accept. + const pendingHolds = ( + await this.dataSource.getRepository(Booking).find({ + where: { + requestedTrainScheduleId: schedule.id, + status: 'OPERATION_REQUEST_PENDING', + } as never, + relations: ['bookingContainers'], + }) + ).filter((b) => !excludeBookingIds?.includes(b.id)); + for (const b of [...allocated, ...reserved, ...pendingHolds]) { budget.subtract( this.needFor(b, wagonDims), budget.legForYards(b.originYardId, b.destinationYardId), diff --git a/apps/edr-freight-api/src/modules/wagons/wagons.module.ts b/apps/edr-freight-api/src/modules/wagons/wagons.module.ts index ec915a553..4204d7618 100644 --- a/apps/edr-freight-api/src/modules/wagons/wagons.module.ts +++ b/apps/edr-freight-api/src/modules/wagons/wagons.module.ts @@ -17,7 +17,7 @@ import { WagonTransferRequestsService } from './wagon-transfer-requests.service' TypeOrmModule.forFeature([ Wagon, WagonMovement, - WagonStatusLog, + WagonStatusLog, WagonTransferRequest, Train, Yard,