mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 19:58:11 +00:00
add yard distances management to rule engine
- Introduced new yard distances resource with CRUD operations. - Created migration for yard distances table with necessary constraints. - Implemented service and repository for yard distances handling. - Added controller for API endpoints to manage yard distances. - Updated rule engine configuration to include yard distances. - Enhanced rule engine resource page to support yard distance selection. - Updated contracts and train builder pages to handle new yard distance logic. - Added error handling utility for better error message extraction.
This commit is contained in:
@@ -4,25 +4,22 @@ import {
|
||||
ArrayMinSize,
|
||||
IsArray,
|
||||
IsEnum,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsUUID,
|
||||
Min,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
|
||||
import { RouteStatus } from '../entities/route.entity';
|
||||
|
||||
/**
|
||||
* Segment distances are no longer part of the payload — they are resolved
|
||||
* from the configured yard_distances table (Configuration → Yard Distances)
|
||||
* and snapshotted onto route_milestones at create/update.
|
||||
*/
|
||||
export class CreateRouteMilestoneDto {
|
||||
@ApiProperty({ format: 'uuid' })
|
||||
@IsUUID()
|
||||
yardId!: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Km from the previous stop (0 for origin)' })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
distanceKm?: number;
|
||||
}
|
||||
|
||||
export class CreateRouteDto {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { DataSource, In } from 'typeorm';
|
||||
|
||||
import { deriveTradeDirection } from '../../common/derive-trade-direction.util';
|
||||
import { Yard } from '../rule-engine/entities/yard.entity';
|
||||
import { YardDistance } from '../rule-engine/entities/yard-distance.entity';
|
||||
import { TrainSchedule } from '../train-schedules/entities/train-schedule.entity';
|
||||
import { CreateRouteDto } from './dto/create-route.dto';
|
||||
import { FilterRoutesDto } from './dto/filter-routes.dto';
|
||||
@@ -17,6 +18,9 @@ import { RouteMilestone } from './entities/route-milestone.entity';
|
||||
import { formatRouteLabel, Route } from './entities/route.entity';
|
||||
import { RoutesRepository } from './routes.repository';
|
||||
|
||||
/** Order-insensitive key: distances are symmetric. */
|
||||
const pairKey = (a: string, b: string): string => (a < b ? `${a}|${b}` : `${b}|${a}`);
|
||||
|
||||
@Injectable()
|
||||
export class RoutesService {
|
||||
constructor(
|
||||
@@ -183,47 +187,63 @@ export class RoutesService {
|
||||
return this.findById(id);
|
||||
}
|
||||
|
||||
private async validateMilestones(
|
||||
milestones: Array<{ yardId: string; distanceKm?: number }>,
|
||||
) {
|
||||
private async validateMilestones(milestones: Array<{ yardId: string }>) {
|
||||
if (milestones.length < 2) {
|
||||
throw new BadRequestException('A route requires at least two yards');
|
||||
}
|
||||
|
||||
const normalized = milestones.map((milestone, index) => {
|
||||
const distanceKm =
|
||||
index === 0 ? 0 : milestone.distanceKm != null ? milestone.distanceKm : null;
|
||||
if (index > 0 && (distanceKm == null || distanceKm < 0)) {
|
||||
throw new BadRequestException(
|
||||
`Enter segment KM for stop ${index + 1} (from previous yard).`,
|
||||
);
|
||||
}
|
||||
return {
|
||||
yardId: milestone.yardId,
|
||||
sequenceNo: index + 1,
|
||||
distanceKm,
|
||||
};
|
||||
});
|
||||
|
||||
const uniqueYardIds = [...new Set(normalized.map((milestone) => milestone.yardId))];
|
||||
const uniqueYardIds = [...new Set(milestones.map((milestone) => milestone.yardId))];
|
||||
const yards = await this.dataSource
|
||||
.getRepository(Yard)
|
||||
.find({ where: uniqueYardIds.map((id) => ({ id })) });
|
||||
const yardIds = new Set(yards.map((yard) => yard.id));
|
||||
|
||||
for (const milestone of normalized) {
|
||||
for (const milestone of milestones) {
|
||||
if (!yardIds.has(milestone.yardId)) {
|
||||
throw new BadRequestException(`Yard ${milestone.yardId} does not exist`);
|
||||
}
|
||||
}
|
||||
|
||||
if (normalized[0].yardId === normalized[normalized.length - 1].yardId) {
|
||||
if (milestones[0].yardId === milestones[milestones.length - 1].yardId) {
|
||||
throw new BadRequestException('Origin and destination yards must be different');
|
||||
}
|
||||
|
||||
const originYardId = normalized[0].yardId;
|
||||
const destinationYardId = normalized[normalized.length - 1].yardId;
|
||||
const yardById = new Map(yards.map((yard) => [yard.id, yard]));
|
||||
const distanceByPair = await this.loadDistanceLookup(uniqueYardIds);
|
||||
|
||||
// Segment km come from the configured yard-distance table, not the payload
|
||||
// — a route can only be built over pairs an admin has entered. Distances
|
||||
// are symmetric, so an A→B row also serves B→A.
|
||||
const missingPairs: string[] = [];
|
||||
const normalized = milestones.map((milestone, index) => {
|
||||
if (index === 0) {
|
||||
return { yardId: milestone.yardId, sequenceNo: 1, distanceKm: 0 };
|
||||
}
|
||||
const previousYardId = milestones[index - 1].yardId;
|
||||
const distanceKm = distanceByPair.get(pairKey(previousYardId, milestone.yardId));
|
||||
if (distanceKm == null) {
|
||||
const from = yardById.get(previousYardId);
|
||||
const to = yardById.get(milestone.yardId);
|
||||
missingPairs.push(
|
||||
`${from?.label ?? previousYardId} ↔ ${to?.label ?? milestone.yardId}`,
|
||||
);
|
||||
}
|
||||
return {
|
||||
yardId: milestone.yardId,
|
||||
sequenceNo: index + 1,
|
||||
distanceKm: distanceKm ?? null,
|
||||
};
|
||||
});
|
||||
|
||||
if (missingPairs.length > 0) {
|
||||
throw new BadRequestException(
|
||||
`No distance configured for: ${missingPairs.join(', ')}. ` +
|
||||
'Add the missing yard distances in Configuration → Yard Distances first.',
|
||||
);
|
||||
}
|
||||
|
||||
const originYardId = milestones[0].yardId;
|
||||
const destinationYardId = milestones[milestones.length - 1].yardId;
|
||||
const direction = deriveTradeDirection(
|
||||
yardById.get(originYardId) ?? { country: null },
|
||||
yardById.get(destinationYardId) ?? { country: null },
|
||||
@@ -236,4 +256,17 @@ export class RoutesService {
|
||||
milestones: normalized,
|
||||
};
|
||||
}
|
||||
|
||||
/** Order-insensitive pair → km map over every configured distance touching the yards. */
|
||||
private async loadDistanceLookup(yardIds: string[]): Promise<Map<string, number>> {
|
||||
const rows = await this.dataSource
|
||||
.getRepository(YardDistance)
|
||||
.find({ where: [{ fromYardId: In(yardIds) }, { toYardId: In(yardIds) }] });
|
||||
|
||||
const lookup = new Map<string, number>();
|
||||
for (const row of rows) {
|
||||
lookup.set(pairKey(row.fromYardId, row.toYardId), Number(row.distanceKm));
|
||||
}
|
||||
return lookup;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user