mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 12:18:11 +00:00
fix issues
This commit is contained in:
@@ -2949,7 +2949,9 @@ export class TrainSchedulingService {
|
||||
// Per-wagon loading: a booking mid-load is neither ridable nor removable —
|
||||
// every wagon must be LOADED, or the never-loaded remainder cancelled
|
||||
// (at-loading cancellation), before the train departs.
|
||||
await this.assertNoPartiallyLoadedBookings(schedule);
|
||||
await this.assertNoPartiallyLoadedBookings(schedule, schedule.originStationId, {
|
||||
action: 'dispatch',
|
||||
});
|
||||
|
||||
await this.dataSource.transaction(async (manager) => {
|
||||
const trainNumber = await this.assignTrainNumber(manager, schedule);
|
||||
@@ -3149,13 +3151,21 @@ export class TrainSchedulingService {
|
||||
* (their charge sits on the credit ledger) yet ride from accept.
|
||||
*/
|
||||
/**
|
||||
* Per-wagon loading dispatch gate: a booking with SOME wagons LOADED and
|
||||
* SOME still PLANNED/RESERVED must resolve before departure — load the rest
|
||||
* or cancel it (which shrinks the booking to its loaded wagons). Blocking
|
||||
* here beats silently unassigning: unassign would delete LOADED allocations
|
||||
* and strand cargo that is physically on the train.
|
||||
* Per-wagon loading gate: a booking with SOME wagons LOADED and SOME still
|
||||
* PLANNED/RESERVED must resolve before the train leaves the yard it boards
|
||||
* at — load the rest, or cancel the remainder (which shrinks the booking to
|
||||
* its loaded wagons). Blocking beats silently unassigning: unassign would
|
||||
* delete LOADED allocations and strand cargo physically on the train.
|
||||
*
|
||||
* Scoped to bookings BOARDING AT `boardingYardId`, so each yard answers only
|
||||
* for its own cargo: a mid-corridor booking (A→B→C→D carrying a B→C load) is
|
||||
* not due at A and must never hold the train there.
|
||||
*/
|
||||
private async assertNoPartiallyLoadedBookings(schedule: TrainSchedule): Promise<void> {
|
||||
private async assertNoPartiallyLoadedBookings(
|
||||
schedule: TrainSchedule,
|
||||
boardingYardId: string,
|
||||
context: { action: string; yardLabel?: string },
|
||||
): Promise<void> {
|
||||
if (!schedule.trainSetId) return;
|
||||
const rows: Array<{ reference: string; loaded: string; total: string }> =
|
||||
await this.dataSource.query(
|
||||
@@ -3166,24 +3176,51 @@ export class TrainSchedulingService {
|
||||
JOIN freight.train_set_wagons tsw ON tsw.id = a.train_set_wagon_id
|
||||
JOIN freight.bookings b ON b.id = a.booking_id
|
||||
WHERE tsw.train_set_id = $1
|
||||
AND b.origin_yard_id = $2
|
||||
AND a.deleted_at IS NULL
|
||||
AND tsw.deleted_at IS NULL
|
||||
AND b.deleted_at IS NULL
|
||||
GROUP BY b.id, b.reference
|
||||
HAVING COUNT(*) FILTER (WHERE a.status IN ('LOADED', 'DEPARTED')) > 0
|
||||
AND COUNT(*) FILTER (WHERE a.status NOT IN ('LOADED', 'DEPARTED')) > 0`,
|
||||
[schedule.trainSetId],
|
||||
[schedule.trainSetId, boardingYardId],
|
||||
);
|
||||
if (rows.length) {
|
||||
const detail = rows
|
||||
.map((r) => `${r.reference} (${r.loaded}/${r.total} wagons loaded)`)
|
||||
.join(', ');
|
||||
const where = context.yardLabel ? ` at ${context.yardLabel}` : '';
|
||||
throw new BadRequestException(
|
||||
`Cannot dispatch: booking(s) partially loaded — load every wagon or cancel the remainder first: ${detail}`,
|
||||
`Cannot ${context.action}: booking(s) partially loaded${where} — load every wagon ` +
|
||||
`or cancel the remainder (customer fault: cancellation fee; EDR fault: no fee, ` +
|
||||
`rebookable) first: ${detail}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mid-corridor twin of the dispatch gate. Logging a checkpoint at station N
|
||||
* asserts the train has left every earlier stop, so each of those yards must
|
||||
* have no half-loaded booking of its own left behind. The origin (seq 0) is
|
||||
* skipped — dispatch already gated it — and the final station is included:
|
||||
* arriving there still means the train left the stop before it.
|
||||
*/
|
||||
private async assertPassedYardsFullyLoaded(
|
||||
schedule: TrainSchedule,
|
||||
stations: Array<{ sequenceNo: number; yardId: string; label: string }>,
|
||||
sequenceNo: number,
|
||||
): Promise<void> {
|
||||
const departed = stations.filter(
|
||||
(st) => st.sequenceNo > 0 && st.sequenceNo < sequenceNo,
|
||||
);
|
||||
for (const st of departed) {
|
||||
await this.assertNoPartiallyLoadedBookings(schedule, st.yardId, {
|
||||
action: 'record this checkpoint',
|
||||
yardLabel: st.label,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async unloadedOriginBoarderIds(
|
||||
scheduleId: string,
|
||||
originYardId: string,
|
||||
@@ -4652,6 +4689,12 @@ export class TrainSchedulingService {
|
||||
: TrainCheckpointKind.Passed);
|
||||
const occurredAt = dto.occurredAt ? new Date(dto.occurredAt) : new Date();
|
||||
await this.assertCheckpointTime(schedule, stations, dto.sequenceNo, occurredAt);
|
||||
// Per-wagon loading, mid-corridor: recording THIS station means the train
|
||||
// left the previous one, so every booking that boarded back there must be
|
||||
// fully loaded or its remainder cancelled. The origin is covered by
|
||||
// dispatch; here we answer for the stops between it and this one, so a
|
||||
// skipped checkpoint log cannot smuggle an unresolved yard past the gate.
|
||||
await this.assertPassedYardsFullyLoaded(schedule, stations, dto.sequenceNo);
|
||||
|
||||
// Upsert by (scheduleId, sequenceNo) so re-logging a station updates rather than duplicates.
|
||||
const [existing] = await this.trainCheckpointEventsRepository.findAll({
|
||||
@@ -5404,7 +5447,11 @@ export class TrainSchedulingService {
|
||||
return this.getTrainScheduleById(id);
|
||||
}
|
||||
|
||||
async cancelTrainSchedule(id: string) {
|
||||
async cancelTrainSchedule(
|
||||
id: string,
|
||||
dto?: { reason?: string },
|
||||
userId?: string,
|
||||
) {
|
||||
const schedule = await this.trainSchedulesRepository.findByIdWithFullGraph(id);
|
||||
if (!schedule) {
|
||||
throw new NotFoundException(`Train schedule ${id} not found`);
|
||||
@@ -5435,6 +5482,11 @@ export class TrainSchedulingService {
|
||||
TrainScheduleStatusEnum.Cancelled,
|
||||
now,
|
||||
),
|
||||
// Why the train died — read back by every view of the cancelled
|
||||
// schedule, and by the staff who have to re-place its bookings.
|
||||
cancellationReason: dto?.reason?.trim() || null,
|
||||
cancelledAt: now,
|
||||
cancelledByUserId: userId ?? null,
|
||||
},
|
||||
manager,
|
||||
);
|
||||
@@ -7928,6 +7980,8 @@ export class TrainSchedulingService {
|
||||
freightType: this.resolveScheduleFreightType(schedule),
|
||||
status: schedule.status,
|
||||
bookingWindowStatus: schedule.bookingWindowStatus ?? 'OPEN',
|
||||
cancellationReason: schedule.cancellationReason ?? null,
|
||||
cancelledAt: schedule.cancelledAt ?? null,
|
||||
maxWagons: schedule.maxWagons ?? 0,
|
||||
remainingWagons: Math.max(
|
||||
0,
|
||||
@@ -9886,6 +9940,8 @@ export class TrainSchedulingService {
|
||||
id: schedule.id,
|
||||
reference: schedule.reference ?? null,
|
||||
status: schedule.status,
|
||||
cancellationReason: schedule.cancellationReason ?? null,
|
||||
cancelledAt: schedule.cancelledAt ?? null,
|
||||
freightType: this.resolveScheduleFreightType(schedule),
|
||||
trainNumber: schedule.trainNumber ?? null,
|
||||
voyageNumber: schedule.voyageNumber ?? null,
|
||||
|
||||
Reference in New Issue
Block a user