import { ConflictException, Injectable, NotFoundException } from '@nestjs/common'; import { CreateLocomotiveDto } from './dto/create-locomotive.dto'; import { FilterLocomotivesDto } from './dto/filter-locomotives.dto'; import { UpdateLocomotiveDto } from './dto/update-locomotive.dto'; import { Locomotive, type LocomotiveStatus, type LocomotiveType, } from './entities/locomotive.entity'; import { LocomotivesRepository } from './locomotives.repository'; @Injectable() export class LocomotivesService { constructor(private readonly locomotivesRepository: LocomotivesRepository) {} findAll(filter: FilterLocomotivesDto): Promise { return this.locomotivesRepository.findAll({ where: { ...(filter.status ? { status: filter.status as LocomotiveStatus } : {}), ...(filter.locomotiveType ? { locomotiveType: filter.locomotiveType as LocomotiveType } : {}), ...(filter.currentYardId ? { currentYardId: filter.currentYardId } : {}), }, relations: { currentYard: true }, order: { code: 'ASC' }, }); } /** Default max pull weight (tons) applied when the caller omits it. */ private static readonly DEFAULT_MAX_PULL_WEIGHT_TONS = 2500; /** * Generate the next sequential locomotive code (LOCO-001, LOCO-002, …) by * scanning the highest existing LOCO-NNN number. Used when the caller does not * supply a code. */ private async generateCode(): Promise { const all = await this.locomotivesRepository.findAll({}); let max = 0; for (const loco of all) { const match = /^LOCO-(\d+)$/.exec(loco.code ?? ''); if (match) max = Math.max(max, Number(match[1])); } return `LOCO-${String(max + 1).padStart(3, '0')}`; } async create(dto: CreateLocomotiveDto): Promise { const code = dto.code?.trim() || (await this.generateCode()); const [existing] = await this.locomotivesRepository.findAll({ where: { code } }); if (existing) { throw new ConflictException(`Locomotive code ${code} already exists`); } return this.locomotivesRepository.create({ code, name: dto.name?.trim() || null, locomotiveType: dto.locomotiveType as LocomotiveType, status: dto.status as LocomotiveStatus, currentYardId: dto.currentYardId ?? null, maxPullWeightTons: dto.maxPullWeightTons ?? LocomotivesService.DEFAULT_MAX_PULL_WEIGHT_TONS, maxTrainLengthMeters: dto.maxTrainLengthMeters, powerKw: dto.powerKw ?? null, tractionForceKn: dto.tractionForceKn ?? null, maxSpeedKmh: dto.maxSpeedKmh ?? null, }); } async findById(id: string): Promise { const locomotive = await this.locomotivesRepository.findById(id, { relations: { currentYard: true }, }); if (!locomotive) { throw new NotFoundException(`Locomotive ${id} not found`); } return locomotive; } async update(id: string, dto: UpdateLocomotiveDto): Promise { const locomotive = await this.findById(id); if (dto.code && dto.code !== locomotive.code) { const [existing] = await this.locomotivesRepository.findAll({ where: { code: dto.code } }); if (existing && existing.id !== id) { throw new ConflictException(`Locomotive code ${dto.code} already exists`); } } const updated = await this.locomotivesRepository.update(id, { ...dto, locomotiveType: dto.locomotiveType === undefined ? locomotive.locomotiveType : dto.locomotiveType as LocomotiveType, status: dto.status === undefined ? locomotive.status : dto.status as LocomotiveStatus, currentYardId: dto.currentYardId === undefined ? locomotive.currentYardId : (dto.currentYardId ?? null), name: dto.name === undefined ? locomotive.name : dto.name?.trim() || null, powerKw: dto.powerKw === undefined ? locomotive.powerKw : dto.powerKw ?? null, tractionForceKn: dto.tractionForceKn === undefined ? locomotive.tractionForceKn : dto.tractionForceKn ?? null, maxSpeedKmh: dto.maxSpeedKmh === undefined ? locomotive.maxSpeedKmh : dto.maxSpeedKmh ?? null, }); if (!updated) { throw new NotFoundException(`Locomotive ${id} not found`); } return updated; } async decommission(id: string): Promise { await this.findById(id); const updated = await this.locomotivesRepository.update(id, { status: 'OUT_OF_SERVICE', }); if (!updated) { throw new NotFoundException(`Locomotive ${id} not found`); } return updated; } }