import { Injectable, NotFoundException, ConflictException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository, DataSource, FindOptionsOrder, FindOptionsWhere, ILike } from 'typeorm'; import { CreateWagonDto } from './dto/create-wagon.dto'; import { UpdateWagonDto } from './dto/update-wagon.dto'; import { AssignWagonToTrainDto } from './dto/assign-wagon-to-train.dto'; import { ReorderWagonsDto } from './dto/reorder-wagons.dto'; import { Wagon } from './entities/wagon.entity'; import { Train } from '../trains/entities/train.entity'; import { Yard } from '../rule-engine/entities/yard.entity'; @Injectable() export class WagonsService { constructor( @InjectRepository(Wagon) private readonly wagonRepo: Repository, @InjectRepository(Train) private readonly trainRepo: Repository, @InjectRepository(Yard) private readonly yardRepo: Repository, private readonly dataSource: DataSource, ) {} async create(dto: CreateWagonDto): Promise { const wagon = this.wagonRepo.create(dto); // Convert undefined to null for nullable fields if (dto.trainId === undefined) wagon.trainId = null; if (dto.sequenceNumber === undefined) wagon.sequenceNumber = null; wagon.status = await this.statusForLocation(dto.currentLocationYardId, dto.status); return this.wagonRepo.save(wagon); } async findAll(query: Record = {}): Promise { const where: FindOptionsWhere[] | FindOptionsWhere = []; const search = query.search?.trim(); const status = query.status?.trim(); const trainId = query.trainId?.trim(); const currentLocationYardId = query.currentLocationYardId?.trim(); if (search) { where.push({ wagonNumber: ILike(`%${search}%`), ...(status ? { status } : {}), ...(trainId ? { trainId } : {}), ...(currentLocationYardId ? { currentLocationYardId } : {}), }); } const sortBy = ['wagonNumber', 'tareWeight', 'maxPayloadWeight', 'status', 'sequenceNumber'].includes(query.sortBy ?? '') ? (query.sortBy as keyof Wagon) : 'wagonNumber'; const sortOrder = query.sortOrder?.toUpperCase() === 'DESC' ? 'DESC' : 'ASC'; return this.wagonRepo.find({ where: search ? where : { ...(status ? { status } : {}), ...(trainId ? { trainId } : {}), ...(currentLocationYardId ? { currentLocationYardId } : {}) }, relations: { currentLocationYard: true, wagonType: true }, order: { [sortBy]: sortOrder } as FindOptionsOrder, skip: query.page && query.limit ? (Number(query.page) - 1) * Number(query.limit) : undefined, take: query.limit ? Number(query.limit) : undefined, }); } async findById(id: string): Promise { const wagon = await this.wagonRepo.findOne({ where: { id }, relations: { currentLocationYard: true, wagonType: true } }); if (!wagon) throw new NotFoundException(`Wagon ${id} not found`); return wagon; } async update(id: string, dto: UpdateWagonDto): Promise { const wagon = await this.findById(id); Object.assign(wagon, dto); if (dto.currentLocationYardId !== undefined) { wagon.status = await this.statusForLocation(dto.currentLocationYardId, dto.status); } return this.wagonRepo.save(wagon); } async remove(id: string): Promise { const wagon = await this.findById(id); await this.wagonRepo.remove(wagon); } async assignToTrain(wagonId: string, dto: AssignWagonToTrainDto): Promise { const wagon = await this.findById(wagonId); if (wagon.status === 'ASSIGNED') { throw new ConflictException('Wagon already assigned to a train'); } const train = await this.trainRepo.findOne({ where: { id: dto.trainId } }); if (!train) throw new NotFoundException('Train not found'); let sequence: number | null = dto.sequenceNumber ?? null; if (sequence === null) { const maxSeq = await this.wagonRepo .createQueryBuilder('w') .select('MAX(w.sequenceNumber)', 'max') .where('w.trainId = :trainId', { trainId: train.id }) .getRawOne(); sequence = (maxSeq?.max ?? 0) + 1; } wagon.trainId = train.id; wagon.sequenceNumber = sequence; wagon.status = 'ASSIGNED'; return this.wagonRepo.save(wagon); } async unassignFromTrain(wagonId: string): Promise { const wagon = await this.findById(wagonId); wagon.trainId = null; wagon.sequenceNumber = null; wagon.status = await this.statusForLocation(wagon.currentLocationYardId, 'AVAILABLE'); return this.wagonRepo.save(wagon); } private async statusForLocation(yardId?: string | null, fallback = 'AVAILABLE') { if (!yardId) return fallback; const yard = await this.yardRepo.findOne({ where: { id: yardId } }); const country = yard?.country?.trim().toLowerCase(); if (country === 'ethiopia' || country === 'et') return 'EXPORT_READY'; if (country === 'djibouti' || country === 'djoubti' || country === 'dj') return 'IMPORT_READY'; return fallback; } async reorderWagons(_trainId: string, dto: ReorderWagonsDto): Promise { const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); try { for (let i = 0; i < dto.wagonIds.length; i++) { await queryRunner.manager.update(Wagon, dto.wagonIds[i], { sequenceNumber: i + 1 }); } await queryRunner.commitTransaction(); } catch (err) { await queryRunner.rollbackTransaction(); throw err; } finally { await queryRunner.release(); } } }