mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 19:58:11 +00:00
209 lines
6.4 KiB
TypeScript
209 lines
6.4 KiB
TypeScript
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
|
import { DataSource } from 'typeorm';
|
|
|
|
import { deriveTradeDirection } from '../../common/derive-trade-direction.util';
|
|
import { Yard } from '../rule-engine/entities/yard.entity';
|
|
import { CreateRouteDto } from './dto/create-route.dto';
|
|
import { FilterRoutesDto } from './dto/filter-routes.dto';
|
|
import { UpdateRouteDto } from './dto/update-route.dto';
|
|
import { RouteMilestone } from './entities/route-milestone.entity';
|
|
import { formatRouteLabel, Route } from './entities/route.entity';
|
|
import { RoutesRepository } from './routes.repository';
|
|
|
|
@Injectable()
|
|
export class RoutesService {
|
|
constructor(
|
|
private readonly dataSource: DataSource,
|
|
private readonly routesRepository: RoutesRepository,
|
|
) {}
|
|
|
|
async findAll(filter: FilterRoutesDto): Promise<Route[]> {
|
|
const routes = await this.routesRepository.findAll({
|
|
where: {
|
|
...(filter.status ? { status: filter.status } : {}),
|
|
},
|
|
relations: {
|
|
originYard: true,
|
|
destinationYard: true,
|
|
milestones: { yard: true },
|
|
},
|
|
order: {
|
|
milestones: { sequenceNo: 'ASC' },
|
|
},
|
|
});
|
|
|
|
const sorted = [...routes].sort((a, b) =>
|
|
formatRouteLabel(a).localeCompare(formatRouteLabel(b)),
|
|
);
|
|
|
|
const query = filter.search?.trim().toLowerCase();
|
|
if (!query) return sorted;
|
|
|
|
return sorted.filter((route) => {
|
|
const haystack = [
|
|
formatRouteLabel(route),
|
|
route.originYard?.label,
|
|
route.originYard?.code,
|
|
route.destinationYard?.label,
|
|
route.destinationYard?.code,
|
|
...(route.milestones ?? []).map(
|
|
(m) => m.yard?.label ?? m.yard?.code ?? m.yardId,
|
|
),
|
|
]
|
|
.filter(Boolean)
|
|
.join(' ')
|
|
.toLowerCase();
|
|
return haystack.includes(query);
|
|
});
|
|
}
|
|
|
|
async findById(id: string): Promise<Route> {
|
|
const route = await this.dataSource.getRepository(Route).findOne({
|
|
where: { id },
|
|
relations: {
|
|
originYard: true,
|
|
destinationYard: true,
|
|
milestones: { yard: true },
|
|
},
|
|
order: { milestones: { sequenceNo: 'ASC' } },
|
|
});
|
|
|
|
if (!route) {
|
|
throw new NotFoundException(`Route ${id} not found`);
|
|
}
|
|
|
|
return route;
|
|
}
|
|
|
|
async create(dto: CreateRouteDto): Promise<Route> {
|
|
const validated = await this.validateMilestones(dto.milestones);
|
|
|
|
const route = await this.dataSource.transaction(async (manager) => {
|
|
const savedRoute = await manager.getRepository(Route).save(
|
|
manager.getRepository(Route).create({
|
|
originYardId: validated.originYardId,
|
|
destinationYardId: validated.destinationYardId,
|
|
status: dto.status ?? 'AVAILABLE',
|
|
direction: validated.direction,
|
|
}),
|
|
);
|
|
|
|
await manager.getRepository(RouteMilestone).save(
|
|
validated.milestones.map((milestone) =>
|
|
manager.getRepository(RouteMilestone).create({
|
|
routeId: savedRoute.id,
|
|
yardId: milestone.yardId,
|
|
sequenceNo: milestone.sequenceNo,
|
|
distanceKm: milestone.distanceKm,
|
|
}),
|
|
),
|
|
);
|
|
|
|
return savedRoute;
|
|
});
|
|
|
|
return this.findById(route.id);
|
|
}
|
|
|
|
async update(id: string, dto: UpdateRouteDto): Promise<Route> {
|
|
const existing = await this.findById(id);
|
|
|
|
const milestoneInput = dto.milestones
|
|
? await this.validateMilestones(dto.milestones)
|
|
: null;
|
|
|
|
await this.dataSource.transaction(async (manager) => {
|
|
await manager.getRepository(Route).update(id, {
|
|
originYardId: milestoneInput?.originYardId ?? existing.originYardId,
|
|
destinationYardId:
|
|
milestoneInput?.destinationYardId ?? existing.destinationYardId,
|
|
...(milestoneInput ? { direction: milestoneInput.direction } : {}),
|
|
...(dto.status !== undefined ? { status: dto.status } : {}),
|
|
});
|
|
|
|
if (milestoneInput) {
|
|
await manager.getRepository(RouteMilestone).delete({ routeId: id });
|
|
await manager.getRepository(RouteMilestone).save(
|
|
milestoneInput.milestones.map((milestone) =>
|
|
manager.getRepository(RouteMilestone).create({
|
|
routeId: id,
|
|
yardId: milestone.yardId,
|
|
sequenceNo: milestone.sequenceNo,
|
|
distanceKm: milestone.distanceKm,
|
|
}),
|
|
),
|
|
);
|
|
}
|
|
});
|
|
|
|
return this.findById(id);
|
|
}
|
|
|
|
async deactivate(id: string): Promise<Route> {
|
|
await this.findById(id);
|
|
const updated = await this.routesRepository.update(id, {
|
|
status: 'STOP_WORKING',
|
|
} as never);
|
|
|
|
if (!updated) {
|
|
throw new NotFoundException(`Route ${id} not found`);
|
|
}
|
|
|
|
return this.findById(id);
|
|
}
|
|
|
|
private async validateMilestones(
|
|
milestones: Array<{ yardId: string; distanceKm?: number }>,
|
|
) {
|
|
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 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) {
|
|
if (!yardIds.has(milestone.yardId)) {
|
|
throw new BadRequestException(`Yard ${milestone.yardId} does not exist`);
|
|
}
|
|
}
|
|
|
|
if (normalized[0].yardId === normalized[normalized.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 direction = deriveTradeDirection(
|
|
yardById.get(originYardId) ?? { country: null },
|
|
yardById.get(destinationYardId) ?? { country: null },
|
|
);
|
|
|
|
return {
|
|
originYardId,
|
|
destinationYardId,
|
|
direction,
|
|
milestones: normalized,
|
|
};
|
|
}
|
|
}
|