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> { const page = await this.repository.findPaged(query); return { ...page, items: page.items.map(toRow) }; } async findById(id: string): Promise { 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 { 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 { 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 { 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 { 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`, ); } } }