mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 01:48:12 +00:00
changes
This commit is contained in:
@@ -10,8 +10,8 @@ import { DataSource } from 'typeorm';
|
||||
import { Booking } from '../bookings/entities/booking.entity';
|
||||
import { RouteMilestone } from '../routes/entities/route-milestone.entity';
|
||||
import { TrainSchedule } from '../train-schedules/entities/train-schedule.entity';
|
||||
import { BookingBatchService, type Capacity } from './booking-batch.service';
|
||||
import { TrainCheckpointEvent } from './entities/train-checkpoint-event.entity';
|
||||
import { BookingBatchService } from './booking-batch.service';
|
||||
import { BookingJourneyService } from './booking-journey.service';
|
||||
|
||||
/**
|
||||
* Intercity (DOMESTIC) ride-along: intercity bookings never get their own
|
||||
@@ -32,6 +32,7 @@ export class IntercityService {
|
||||
constructor(
|
||||
@InjectDataSource() private readonly dataSource: DataSource,
|
||||
private readonly bookingBatchService: BookingBatchService,
|
||||
private readonly bookingJourneyService: BookingJourneyService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -52,13 +53,20 @@ export class IntercityService {
|
||||
return {
|
||||
scheduleId,
|
||||
routeId: schedule.routeId ?? null,
|
||||
remaining: capacity?.budget ?? null,
|
||||
// Segment-based: "remaining" is the most-open edge; each candidate's
|
||||
// `fits` is judged against ITS OWN leg, so a booking on a free leg fits
|
||||
// even when the train is full elsewhere.
|
||||
remaining: capacity?.budget.maxRemaining() ?? null,
|
||||
candidates: waiting.map((booking) => {
|
||||
const need = capacity?.needFor(booking) ?? null;
|
||||
const leg = capacity?.budget.legOf(
|
||||
booking.originYardId,
|
||||
booking.destinationYardId,
|
||||
);
|
||||
return {
|
||||
...this.mapBooking(booking),
|
||||
need,
|
||||
fits: need && capacity ? fits(need, capacity.budget) : false,
|
||||
fits: Boolean(need && capacity && leg && capacity.budget.fits(need, leg)),
|
||||
};
|
||||
}),
|
||||
accepted: accepted.map((booking) => ({
|
||||
@@ -94,7 +102,7 @@ export class IntercityService {
|
||||
|
||||
const accepted: string[] = [];
|
||||
const rejected: Array<{ bookingId: string; reason: string }> = [];
|
||||
let budget = capacity.budget;
|
||||
const budget = capacity.budget;
|
||||
|
||||
for (const bookingId of bookingIds) {
|
||||
const booking = await this.dataSource
|
||||
@@ -110,45 +118,35 @@ export class IntercityService {
|
||||
continue;
|
||||
}
|
||||
const need = capacity.needFor(booking);
|
||||
if (!fits(need, budget)) {
|
||||
const leg = budget.legOf(booking.originYardId, booking.destinationYardId);
|
||||
// Segment-based: only the booking's own leg must have room, so an
|
||||
// intercity booking still boards a train that is full on other legs.
|
||||
if (!leg || !budget.fits(need, leg)) {
|
||||
rejected.push({
|
||||
bookingId,
|
||||
reason: 'Does not fit the remaining wagon/weight/length capacity',
|
||||
reason:
|
||||
'Does not fit the remaining wagon/weight/length capacity on its leg',
|
||||
});
|
||||
continue;
|
||||
}
|
||||
await this.bookingBatchService.acceptIntercity(booking, scheduleId);
|
||||
budget = subtract(budget, need);
|
||||
budget.subtract(need, leg);
|
||||
accepted.push(bookingId);
|
||||
this.logger.log(
|
||||
`Intercity booking ${booking.reference ?? bookingId} accepted onto schedule ${scheduleId}`,
|
||||
);
|
||||
}
|
||||
|
||||
return { accepted, rejected, remaining: budget };
|
||||
return { accepted, rejected, remaining: budget.maxRemaining() };
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark an accepted intercity booking's cargo as loaded. Only allowed while
|
||||
* the train is physically at the booking's origin yard: either it has not
|
||||
* departed yet and the booking boards at the train's own origin, or the
|
||||
* latest recorded checkpoint is at the booking's origin yard.
|
||||
* Mark an accepted intercity booking's cargo as loaded. Delegates to the
|
||||
* shared per-booking journey flow (same checkpoint gating as import/export).
|
||||
*/
|
||||
async loadBooking(scheduleId: string, bookingId: string) {
|
||||
const { schedule, booking } = await this.getAcceptedBooking(
|
||||
scheduleId,
|
||||
bookingId,
|
||||
);
|
||||
if (booking.status !== 'PAID') {
|
||||
throw new BadRequestException(
|
||||
`Booking must be paid before loading (currently ${booking.status})`,
|
||||
);
|
||||
}
|
||||
await this.assertTrainAtYard(schedule, booking.originYardId, 'origin');
|
||||
await this.dataSource
|
||||
.getRepository(Booking)
|
||||
.update(bookingId, { status: 'IN_TRANSIT' });
|
||||
return { bookingId, status: 'IN_TRANSIT' as const };
|
||||
await this.getAcceptedBooking(scheduleId, bookingId); // intercity-only guard
|
||||
return this.bookingJourneyService.loadBooking(scheduleId, bookingId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -156,20 +154,8 @@ export class IntercityService {
|
||||
* requires the latest checkpoint to be at that yard. Completes the booking.
|
||||
*/
|
||||
async unloadBooking(scheduleId: string, bookingId: string) {
|
||||
const { schedule, booking } = await this.getAcceptedBooking(
|
||||
scheduleId,
|
||||
bookingId,
|
||||
);
|
||||
if (booking.status !== 'IN_TRANSIT') {
|
||||
throw new BadRequestException(
|
||||
`Booking must be loaded/in transit before unloading (currently ${booking.status})`,
|
||||
);
|
||||
}
|
||||
await this.assertTrainAtYard(schedule, booking.destinationYardId, 'destination');
|
||||
await this.dataSource
|
||||
.getRepository(Booking)
|
||||
.update(bookingId, { status: 'COMPLETED' });
|
||||
return { bookingId, status: 'COMPLETED' as const };
|
||||
await this.getAcceptedBooking(scheduleId, bookingId); // intercity-only guard
|
||||
return this.bookingJourneyService.unloadBooking(scheduleId, bookingId);
|
||||
}
|
||||
|
||||
// ---- helpers ---------------------------------------------------------------
|
||||
@@ -298,36 +284,6 @@ export class IntercityService {
|
||||
return { schedule, booking };
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* not recorded any checkpoint yet (still sitting at its origin).
|
||||
*/
|
||||
private async assertTrainAtYard(
|
||||
schedule: TrainSchedule,
|
||||
yardId: string,
|
||||
side: 'origin' | 'destination',
|
||||
): Promise<void> {
|
||||
const latest = await this.dataSource
|
||||
.getRepository(TrainCheckpointEvent)
|
||||
.findOne({
|
||||
where: { trainScheduleId: schedule.id },
|
||||
order: { occurredAt: 'DESC', createdAt: 'DESC' },
|
||||
});
|
||||
|
||||
if (!latest) {
|
||||
if (side === 'origin' && schedule.originStationId === yardId) return;
|
||||
throw new BadRequestException(
|
||||
'Train has not reached this yard yet — record its checkpoint first',
|
||||
);
|
||||
}
|
||||
if (latest.yardId !== yardId) {
|
||||
throw new BadRequestException(
|
||||
`Train's last recorded position is not at the booking's ${side} yard`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private mapBooking(booking: Booking) {
|
||||
return {
|
||||
id: booking.id,
|
||||
@@ -350,18 +306,3 @@ export class IntercityService {
|
||||
}
|
||||
}
|
||||
|
||||
function fits(need: Capacity, budget: Capacity): boolean {
|
||||
return (
|
||||
need.wagons <= budget.wagons &&
|
||||
need.weightTons <= budget.weightTons &&
|
||||
need.lengthMeters <= budget.lengthMeters
|
||||
);
|
||||
}
|
||||
|
||||
function subtract(budget: Capacity, need: Capacity): Capacity {
|
||||
return {
|
||||
wagons: budget.wagons - need.wagons,
|
||||
weightTons: budget.weightTons - need.weightTons,
|
||||
lengthMeters: budget.lengthMeters - need.lengthMeters,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user