feat(freight): added routes and locomotives

This commit is contained in:
Michael Abebe
2026-06-06 16:23:48 +03:00
parent e067da29df
commit a9c2f2eb97
32 changed files with 1443 additions and 64 deletions

View File

@@ -0,0 +1,59 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { IsIn, IsNumber, IsOptional, IsString, MaxLength, Min } from 'class-validator';
import { LOCOMOTIVE_STATUSES, LOCOMOTIVE_TYPES } from '../entities/locomotive.entity';
export class CreateLocomotiveDto {
@ApiProperty({ example: 'LOCO-001' })
@IsString()
@MaxLength(32)
code!: string;
@ApiPropertyOptional()
@IsOptional()
@IsString()
@MaxLength(100)
name?: string;
@ApiProperty({ enum: LOCOMOTIVE_TYPES })
@IsIn([...LOCOMOTIVE_TYPES])
locomotiveType!: string;
@ApiProperty({ enum: LOCOMOTIVE_STATUSES })
@IsIn([...LOCOMOTIVE_STATUSES])
status!: string;
@ApiProperty({ example: 3500 })
@Transform(({ value }) => Number(value))
@IsNumber()
@Min(0)
maxPullWeightTons!: number;
@ApiProperty({ example: 760 })
@Transform(({ value }) => Number(value))
@IsNumber()
@Min(0)
maxTrainLengthMeters!: number;
@ApiPropertyOptional({ example: 4200 })
@IsOptional()
@Transform(({ value }) => (value === '' || value == null ? undefined : Number(value)))
@IsNumber()
@Min(0)
powerKw?: number;
@ApiPropertyOptional({ example: 300 })
@IsOptional()
@Transform(({ value }) => (value === '' || value == null ? undefined : Number(value)))
@IsNumber()
@Min(0)
tractionForceKn?: number;
@ApiPropertyOptional({ example: 120 })
@IsOptional()
@Transform(({ value }) => (value === '' || value == null ? undefined : Number(value)))
@IsNumber()
@Min(0)
maxSpeedKmh?: number;
}

View File

@@ -1,11 +1,16 @@
import { ApiPropertyOptional } from '@nestjs/swagger';
import { IsIn, IsOptional } from 'class-validator';
import { LOCOMOTIVE_STATUSES } from '../entities/locomotive.entity';
import { LOCOMOTIVE_STATUSES, LOCOMOTIVE_TYPES } from '../entities/locomotive.entity';
export class FilterLocomotivesDto {
@ApiPropertyOptional({ enum: LOCOMOTIVE_STATUSES })
@IsOptional()
@IsIn([...LOCOMOTIVE_STATUSES])
status?: string;
@ApiPropertyOptional({ enum: LOCOMOTIVE_TYPES })
@IsOptional()
@IsIn([...LOCOMOTIVE_TYPES])
locomotiveType?: string;
}

View File

@@ -0,0 +1,5 @@
import { PartialType } from '@nestjs/swagger';
import { CreateLocomotiveDto } from './create-locomotive.dto';
export class UpdateLocomotiveDto extends PartialType(CreateLocomotiveDto) {}

View File

@@ -7,10 +7,13 @@ export const LOCOMOTIVE_STATUSES = [
'AVAILABLE',
'ASSIGNED',
'MAINTENANCE',
'INACTIVE',
'OUT_OF_SERVICE',
] as const;
export const LOCOMOTIVE_TYPES = ['DIESEL', 'ELECTRIC'] as const;
export type LocomotiveStatus = (typeof LOCOMOTIVE_STATUSES)[number];
export type LocomotiveType = (typeof LOCOMOTIVE_TYPES)[number];
@Entity({ schema: 'freight', name: 'locomotives' })
@Index(['code'])
@@ -22,14 +25,26 @@ export class Locomotive extends BaseEntity {
@Column({ name: 'name', type: 'varchar', length: 100, nullable: true })
name?: string | null;
@Column({ name: 'locomotive_type', type: 'varchar', length: 20, default: 'DIESEL' })
locomotiveType!: LocomotiveType;
@Column({ name: 'max_pull_weight_tons', type: 'numeric', precision: 10, scale: 3 })
maxPullWeightTons!: number;
@Column({ name: 'max_train_length_meters', type: 'numeric', precision: 10, scale: 3, default: 760 })
maxTrainLengthMeters!: number;
@Column({ name: 'status', type: 'varchar', length: 20, default: 'AVAILABLE' })
status!: LocomotiveStatus;
@Column({ name: 'available_from', type: 'timestamptz', nullable: true })
availableFrom?: Date | null;
@Column({ name: 'power_kw', type: 'numeric', precision: 10, scale: 3, nullable: true })
powerKw?: number | null;
@Column({ name: 'traction_force_kn', type: 'numeric', precision: 10, scale: 3, nullable: true })
tractionForceKn?: number | null;
@Column({ name: 'max_speed_kmh', type: 'numeric', precision: 10, scale: 3, nullable: true })
maxSpeedKmh?: number | null;
@OneToMany(() => TrainSet, (trainSet) => trainSet.locomotive)
trainSets?: TrainSet[];

View File

@@ -1,7 +1,9 @@
import { Controller, Get, Query } from '@nestjs/common';
import { Body, Controller, Get, Param, ParseUUIDPipe, Patch, Post, Query } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { CreateLocomotiveDto } from './dto/create-locomotive.dto';
import { FilterLocomotivesDto } from './dto/filter-locomotives.dto';
import { UpdateLocomotiveDto } from './dto/update-locomotive.dto';
import { LocomotivesService } from './locomotives.service';
@ApiTags('locomotives')
@@ -15,4 +17,28 @@ export class LocomotivesController {
findAll(@Query() filter: FilterLocomotivesDto) {
return this.locomotivesService.findAll(filter);
}
@Get(':id')
@ApiOperation({ summary: 'Get a locomotive by ID' })
findOne(@Param('id', ParseUUIDPipe) id: string) {
return this.locomotivesService.findById(id);
}
@Post()
@ApiOperation({ summary: 'Create a locomotive' })
create(@Body() dto: CreateLocomotiveDto) {
return this.locomotivesService.create(dto);
}
@Patch(':id')
@ApiOperation({ summary: 'Update a locomotive' })
update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateLocomotiveDto) {
return this.locomotivesService.update(id, dto);
}
@Post(':id/decommission')
@ApiOperation({ summary: 'Decommission a locomotive' })
decommission(@Param('id', ParseUUIDPipe) id: string) {
return this.locomotivesService.decommission(id);
}
}

View File

@@ -1,7 +1,9 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { ConflictException, Injectable, NotFoundException } from '@nestjs/common';
import { CreateLocomotiveDto } from './dto/create-locomotive.dto';
import { FilterLocomotivesDto } from './dto/filter-locomotives.dto';
import { Locomotive, type LocomotiveStatus } from './entities/locomotive.entity';
import { UpdateLocomotiveDto } from './dto/update-locomotive.dto';
import { Locomotive, type LocomotiveStatus, type LocomotiveType } from './entities/locomotive.entity';
import { LocomotivesRepository } from './locomotives.repository';
@Injectable()
@@ -10,13 +12,36 @@ export class LocomotivesService {
findAll(filter: FilterLocomotivesDto): Promise<Locomotive[]> {
return this.locomotivesRepository.findAll({
where: filter.status
? { status: filter.status as LocomotiveStatus }
: undefined,
where: {
...(filter.status ? { status: filter.status as LocomotiveStatus } : {}),
...(filter.locomotiveType
? { locomotiveType: filter.locomotiveType as LocomotiveType }
: {}),
},
order: { code: 'ASC' },
});
}
async create(dto: CreateLocomotiveDto): Promise<Locomotive> {
const [existing] = await this.locomotivesRepository.findAll({ where: { code: dto.code } });
if (existing) {
throw new ConflictException(`Locomotive code ${dto.code} already exists`);
}
return this.locomotivesRepository.create({
code: dto.code,
name: dto.name?.trim() || null,
locomotiveType: dto.locomotiveType as LocomotiveType,
status: dto.status as LocomotiveStatus,
maxPullWeightTons: dto.maxPullWeightTons,
maxTrainLengthMeters: dto.maxTrainLengthMeters,
powerKw: dto.powerKw ?? null,
tractionForceKn: dto.tractionForceKn ?? null,
maxSpeedKmh: dto.maxSpeedKmh ?? null,
});
}
async findById(id: string): Promise<Locomotive> {
const locomotive = await this.locomotivesRepository.findById(id);
@@ -26,4 +51,48 @@ export class LocomotivesService {
return locomotive;
}
async update(id: string, dto: UpdateLocomotiveDto): Promise<Locomotive> {
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,
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<Locomotive> {
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;
}
}