Files
edr-platform/apps/edr-freight-api/src/modules/rule-engine/services/yard-distances.service.ts
2026-08-19 13:51:18 +00:00

124 lines
4.2 KiB
TypeScript

import { PaginatedResponse } from '@edr/types';
import {
BadRequestException,
ConflictException,
Inject,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { CreateYardDistanceDto } from '../dto/create-yard-distance.dto';
import { ListYardDistancesQueryDto } from '../dto/list-rule-engine-query.dto';
import { UpdateYardDistanceDto } from '../dto/update-yard-distance.dto';
import { YardDistance } from '../entities/yard-distance.entity';
import {
IYardDistancesRepository,
YARD_DISTANCES_REPOSITORY,
} from '../interfaces/yard-distances.repository.interface';
import { IYardsRepository, YARDS_REPOSITORY } from '../interfaces/yards.repository.interface';
/**
* Flat row shape for the backoffice config table: the yard relations stay for
* API consumers, plus label fields the generic rule-engine grid can render.
*/
export type YardDistanceRow = YardDistance & {
fromYardLabel: string;
toYardLabel: string;
};
const yardDisplay = (yard?: { label?: string; code?: string } | null): string =>
yard?.label ?? yard?.code ?? '—';
const toRow = (entity: YardDistance): YardDistanceRow =>
Object.assign(entity, {
fromYardLabel: yardDisplay(entity.fromYard),
toYardLabel: yardDisplay(entity.toYard),
});
@Injectable()
export class YardDistancesService {
constructor(
@Inject(YARD_DISTANCES_REPOSITORY)
private readonly repository: IYardDistancesRepository,
@Inject(YARDS_REPOSITORY)
private readonly yardsRepository: IYardsRepository,
) {}
async findAll(query: ListYardDistancesQueryDto): Promise<PaginatedResponse<YardDistanceRow>> {
const page = await this.repository.findPaged(query);
return { ...page, items: page.items.map(toRow) };
}
async findById(id: string): Promise<YardDistanceRow> {
const entity = await this.repository.findById(id);
if (!entity) throw new NotFoundException(`Yard distance ${id} not found`);
return toRow(entity);
}
async create(dto: CreateYardDistanceDto): Promise<YardDistanceRow> {
await this.assertValidPair(dto.fromYardId, dto.toYardId);
const created = await this.repository.create({
fromYardId: dto.fromYardId,
toYardId: dto.toYardId,
distanceKm: dto.distanceKm.toFixed(2),
standardHours: dto.standardHours != null ? dto.standardHours.toFixed(2) : null,
});
return toRow(created);
}
async update(id: string, dto: UpdateYardDistanceDto): Promise<YardDistanceRow> {
const existing = await this.findById(id);
const fromYardId = dto.fromYardId ?? existing.fromYardId;
const toYardId = dto.toYardId ?? existing.toYardId;
if (fromYardId !== existing.fromYardId || toYardId !== existing.toYardId) {
await this.assertValidPair(fromYardId, toYardId, id);
}
const updated = await this.repository.update(id, {
fromYardId,
toYardId,
...(dto.distanceKm != null ? { distanceKm: dto.distanceKm.toFixed(2) } : {}),
...(dto.standardHours !== undefined
? { standardHours: dto.standardHours != null ? dto.standardHours.toFixed(2) : null }
: {}),
});
if (!updated) throw new NotFoundException(`Yard distance ${id} not found`);
return toRow(updated);
}
async remove(id: string): Promise<void> {
await this.findById(id);
await this.repository.softDelete(id);
}
/**
* Both yards must exist and differ, and the pair must not already be
* configured in either direction — distances are symmetric, so an A→B row
* already covers B→A.
*/
private async assertValidPair(
fromYardId: string,
toYardId: string,
ignoreId?: string,
): Promise<void> {
if (fromYardId === toYardId) {
throw new BadRequestException('From and to yards must be different');
}
const [fromYard, toYard] = await Promise.all([
this.yardsRepository.findById(fromYardId),
this.yardsRepository.findById(toYardId),
]);
if (!fromYard) throw new BadRequestException(`Yard ${fromYardId} does not exist`);
if (!toYard) throw new BadRequestException(`Yard ${toYardId} does not exist`);
const existing = await this.repository.findBetween(fromYardId, toYardId);
if (existing && existing.id !== ignoreId) {
throw new ConflictException(
`A distance between ${fromYard.label} and ${toYard.label} is already configured`,
);
}
}
}