mantine added to Train, wagon, containers and cargoes

This commit is contained in:
hagiye
2026-06-08 09:29:34 +03:00
105 changed files with 6510 additions and 3248 deletions

View File

@@ -25,10 +25,10 @@ export const BOOKING_LIST_TABS: ReadonlyArray<{
key: 'approved_contract',
statuses: ['APPROVED', 'CONTRACT_READY', 'SIGNED_CUSTOMER', 'FULLY_EXECUTED'],
},
{ key: 'payment', statuses: ['FULLY_EXECUTED', 'PAID'] },
{ key: 'payment', statuses: ['FULLY_EXECUTED'] },
{
key: 'operations',
statuses: ['IN_TRANSIT', 'PENDING_CONSOLIDATION', 'CONSOLIDATED'],
statuses: ['IN_TRANSIT', 'PENDING_CONSOLIDATION', 'CONSOLIDATED','PAID'],
},
{ key: 'completed', statuses: ['COMPLETED'] },
{ key: 'closed', statuses: ['REJECTED', 'CANCELLED'] },

View File

@@ -34,8 +34,8 @@ export function computeNextStep(
};
case 'APPROVED':
return {
action: 'GENERATE_CONTRACT',
description: 'Generate the contract document',
action: 'CUSTOMER_SIGN',
description: 'Contract generated; customer must sign',
};
case 'CONTRACT_READY':
return {
@@ -49,8 +49,8 @@ export function computeNextStep(
};
case 'FULLY_EXECUTED':
return {
action: 'PAY',
description: 'Complete in-app payment',
action: 'AWAIT_PAYMENT',
description: 'Awaiting customer payment',
};
case 'PAID':
return {

View File

@@ -185,6 +185,11 @@ export class BookingTransitionService {
await this.bookingsRepository.update(bookingId, updates as never);
}
if (allDone) {
const generated = await this.contractService.generateContract(bookingId);
return this.bookingsService.findById(generated.id);
}
return this.bookingsService.findById(bookingId);
}

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;
}
}

View File

@@ -0,0 +1,28 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { ArrayMinSize, IsArray, IsBoolean, IsOptional, IsString, IsUUID, MaxLength, ValidateNested } from 'class-validator';
export class CreateRouteMilestoneDto {
@ApiProperty({ format: 'uuid' })
@IsUUID()
yardId!: string;
}
export class CreateRouteDto {
@ApiProperty()
@IsString()
@MaxLength(120)
name!: string;
@ApiProperty({ type: [CreateRouteMilestoneDto] })
@IsArray()
@ArrayMinSize(2)
@ValidateNested({ each: true })
@Type(() => CreateRouteMilestoneDto)
milestones!: CreateRouteMilestoneDto[];
@ApiPropertyOptional()
@IsOptional()
@IsBoolean()
isActive?: boolean;
}

View File

@@ -0,0 +1,16 @@
import { ApiPropertyOptional } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { IsBoolean, IsOptional, IsString } from 'class-validator';
export class FilterRoutesDto {
@ApiPropertyOptional()
@IsOptional()
@IsString()
search?: string;
@ApiPropertyOptional()
@IsOptional()
@Transform(({ value }) => value === 'true' || value === true)
@IsBoolean()
isActive?: boolean;
}

View File

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

View File

@@ -0,0 +1,26 @@
import { BaseEntity } from '@edr/api-common';
import { Column, Entity, Index, JoinColumn, ManyToOne } from 'typeorm';
import { Yard } from '../../rule-engine/entities/yard.entity';
import { Route } from './route.entity';
@Entity({ schema: 'freight', name: 'route_milestones' })
@Index(['routeId', 'sequenceNo'], { unique: true })
export class RouteMilestone extends BaseEntity {
@Column({ name: 'route_id', type: 'uuid' })
routeId!: string;
@ManyToOne(() => Route, (route) => route.milestones, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'route_id' })
route?: Route;
@Column({ name: 'yard_id', type: 'uuid' })
yardId!: string;
@ManyToOne(() => Yard)
@JoinColumn({ name: 'yard_id' })
yard?: Yard;
@Column({ name: 'sequence_no', type: 'int' })
sequenceNo!: number;
}

View File

@@ -0,0 +1,33 @@
import { BaseEntity } from '@edr/api-common';
import { Column, Entity, Index, JoinColumn, ManyToOne, OneToMany } from 'typeorm';
import { Yard } from '../../rule-engine/entities/yard.entity';
import { RouteMilestone } from './route-milestone.entity';
@Entity({ schema: 'freight', name: 'routes' })
@Index(['name'])
@Index(['isActive'])
export class Route extends BaseEntity {
@Column({ name: 'name', type: 'varchar', length: 120, unique: true })
name!: string;
@Column({ name: 'origin_yard_id', type: 'uuid' })
originYardId!: string;
@ManyToOne(() => Yard)
@JoinColumn({ name: 'origin_yard_id' })
originYard?: Yard;
@Column({ name: 'destination_yard_id', type: 'uuid' })
destinationYardId!: string;
@ManyToOne(() => Yard)
@JoinColumn({ name: 'destination_yard_id' })
destinationYard?: Yard;
@Column({ name: 'is_active', type: 'boolean', default: true })
isActive!: boolean;
@OneToMany(() => RouteMilestone, (milestone) => milestone.route, { cascade: false })
milestones?: RouteMilestone[];
}

View File

@@ -0,0 +1,13 @@
import { BaseRepository } from '@edr/api-common';
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { RouteMilestone } from './entities/route-milestone.entity';
@Injectable()
export class RouteMilestonesRepository extends BaseRepository<RouteMilestone> {
constructor(@InjectRepository(RouteMilestone) repository: Repository<RouteMilestone>) {
super(repository);
}
}

View File

@@ -0,0 +1,44 @@
import { Body, Controller, Delete, Get, Param, ParseUUIDPipe, Patch, Post, Query } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { CreateRouteDto } from './dto/create-route.dto';
import { FilterRoutesDto } from './dto/filter-routes.dto';
import { UpdateRouteDto } from './dto/update-route.dto';
import { RoutesService } from './routes.service';
@ApiTags('routes')
@ApiBearerAuth()
@Controller('routes')
export class RoutesController {
constructor(private readonly routesService: RoutesService) {}
@Get()
@ApiOperation({ summary: 'List routes' })
findAll(@Query() filter: FilterRoutesDto) {
return this.routesService.findAll(filter);
}
@Get(':id')
@ApiOperation({ summary: 'Get route by ID' })
findOne(@Param('id', ParseUUIDPipe) id: string) {
return this.routesService.findById(id);
}
@Post()
@ApiOperation({ summary: 'Create route' })
create(@Body() dto: CreateRouteDto) {
return this.routesService.create(dto);
}
@Patch(':id')
@ApiOperation({ summary: 'Update route' })
update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateRouteDto) {
return this.routesService.update(id, dto);
}
@Delete(':id')
@ApiOperation({ summary: 'Deactivate route' })
remove(@Param('id', ParseUUIDPipe) id: string) {
return this.routesService.deactivate(id);
}
}

View File

@@ -0,0 +1,18 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Yard } from '../rule-engine/entities/yard.entity';
import { RouteMilestone } from './entities/route-milestone.entity';
import { Route } from './entities/route.entity';
import { RouteMilestonesRepository } from './route-milestones.repository';
import { RoutesController } from './routes.controller';
import { RoutesRepository } from './routes.repository';
import { RoutesService } from './routes.service';
@Module({
imports: [TypeOrmModule.forFeature([Route, RouteMilestone, Yard])],
controllers: [RoutesController],
providers: [RoutesRepository, RouteMilestonesRepository, RoutesService],
exports: [RoutesRepository, RouteMilestonesRepository, RoutesService],
})
export class RoutesModule {}

View File

@@ -0,0 +1,13 @@
import { BaseRepository } from '@edr/api-common';
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Route } from './entities/route.entity';
@Injectable()
export class RoutesRepository extends BaseRepository<Route> {
constructor(@InjectRepository(Route) repository: Repository<Route>) {
super(repository);
}
}

View File

@@ -0,0 +1,171 @@
import { BadRequestException, ConflictException, Injectable, NotFoundException } from '@nestjs/common';
import { DataSource, ILike } from 'typeorm';
import { Yard } from '../rule-engine/entities/yard.entity';
import { CreateRouteDto } from './dto/create-route.dto';
import { FilterRoutesDto } from './dto/filter-routes.dto';
import { UpdateRouteDto } from './dto/update-route.dto';
import { RouteMilestone } from './entities/route-milestone.entity';
import { Route } from './entities/route.entity';
import { RoutesRepository } from './routes.repository';
@Injectable()
export class RoutesService {
constructor(
private readonly dataSource: DataSource,
private readonly routesRepository: RoutesRepository,
) {}
findAll(filter: FilterRoutesDto): Promise<Route[]> {
return this.routesRepository.findAll({
where: {
...(filter.search ? { name: ILike(`%${filter.search.trim()}%`) } : {}),
...(filter.isActive !== undefined ? { isActive: filter.isActive } : {}),
},
relations: {
originYard: true,
destinationYard: true,
milestones: { yard: true },
},
order: {
name: 'ASC',
milestones: { sequenceNo: 'ASC' },
},
});
}
async findById(id: string): Promise<Route> {
const route = await this.dataSource.getRepository(Route).findOne({
where: { id },
relations: {
originYard: true,
destinationYard: true,
milestones: { yard: true },
},
order: { milestones: { sequenceNo: 'ASC' } },
});
if (!route) {
throw new NotFoundException(`Route ${id} not found`);
}
return route;
}
async create(dto: CreateRouteDto): Promise<Route> {
await this.validateRouteName(dto.name);
const validated = await this.validateMilestones(dto.milestones);
const route = await this.dataSource.transaction(async (manager) => {
const savedRoute = await manager.getRepository(Route).save(
manager.getRepository(Route).create({
name: dto.name.trim(),
originYardId: validated.originYardId,
destinationYardId: validated.destinationYardId,
isActive: dto.isActive ?? true,
}),
);
await manager.getRepository(RouteMilestone).save(
validated.milestones.map((milestone) =>
manager.getRepository(RouteMilestone).create({
routeId: savedRoute.id,
yardId: milestone.yardId,
sequenceNo: milestone.sequenceNo,
}),
),
);
return savedRoute;
});
return this.findById(route.id);
}
async update(id: string, dto: UpdateRouteDto): Promise<Route> {
const existing = await this.findById(id);
if (dto.name && dto.name.trim() !== existing.name) {
await this.validateRouteName(dto.name, id);
}
const milestoneInput = dto.milestones
? await this.validateMilestones(dto.milestones)
: null;
await this.dataSource.transaction(async (manager) => {
await manager.getRepository(Route).update(id, {
name: dto.name?.trim() ?? existing.name,
originYardId: milestoneInput?.originYardId ?? existing.originYardId,
destinationYardId: milestoneInput?.destinationYardId ?? existing.destinationYardId,
isActive: dto.isActive ?? existing.isActive,
});
if (milestoneInput) {
await manager.getRepository(RouteMilestone).delete({ routeId: id });
await manager.getRepository(RouteMilestone).save(
milestoneInput.milestones.map((milestone) =>
manager.getRepository(RouteMilestone).create({
routeId: id,
yardId: milestone.yardId,
sequenceNo: milestone.sequenceNo,
}),
),
);
}
});
return this.findById(id);
}
async deactivate(id: string): Promise<Route> {
await this.findById(id);
const updated = await this.routesRepository.update(id, { isActive: false });
if (!updated) {
throw new NotFoundException(`Route ${id} not found`);
}
return this.findById(id);
}
private async validateRouteName(name: string, routeId?: string) {
const trimmedName = name.trim();
const [existing] = await this.routesRepository.findAll({ where: { name: trimmedName } });
if (existing && existing.id !== routeId) {
throw new ConflictException(`Route name ${trimmedName} already exists`);
}
}
private async validateMilestones(milestones: Array<{ yardId: string }>) {
if (milestones.length < 2) {
throw new BadRequestException('A route requires at least two yards');
}
const normalized = milestones.map((milestone, index) => ({
yardId: milestone.yardId,
sequenceNo: index + 1,
}));
const uniqueYardIds = [...new Set(normalized.map((milestone) => milestone.yardId))];
const yards = await this.dataSource.getRepository(Yard).find({ where: uniqueYardIds.map((id) => ({ id })) });
const yardIds = new Set(yards.map((yard) => yard.id));
for (const milestone of normalized) {
if (!yardIds.has(milestone.yardId)) {
throw new BadRequestException(`Yard ${milestone.yardId} does not exist`);
}
}
if (normalized[0].yardId === normalized[normalized.length - 1].yardId) {
throw new BadRequestException('Origin and destination yards must be different');
}
return {
originYardId: normalized[0].yardId,
destinationYardId: normalized[normalized.length - 1].yardId,
milestones: normalized,
};
}
}

View File

@@ -2,6 +2,7 @@ import { BaseEntity } from '@edr/api-common';
import { Column, Entity, Index, JoinColumn, ManyToOne, OneToMany, OneToOne } from 'typeorm';
import { Yard } from '../../rule-engine/entities/yard.entity';
import { Route } from '../../routes/entities/route.entity';
import { TrainSet } from '../../train-sets/entities/train-set.entity';
import { TrainScheduleBooking } from './train-schedule-booking.entity';
@@ -26,6 +27,13 @@ export class TrainSchedule extends BaseEntity {
@JoinColumn({ name: 'train_set_id' })
trainSet?: TrainSet;
@Column({ name: 'route_id', type: 'uuid', nullable: true })
routeId?: string | null;
@ManyToOne(() => Route)
@JoinColumn({ name: 'route_id' })
route?: Route | null;
@Column({ name: 'origin_station_id', type: 'uuid' })
originStationId!: string;

View File

@@ -1,9 +1,15 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsUUID } from 'class-validator';
import { IsDateString, IsUUID } from 'class-validator';
import { PreviewContainerTrainScheduleDto } from './preview-container-train-schedule.dto';
export class CreateContainerTrainScheduleDto {
@ApiProperty({ format: 'uuid' })
@IsUUID()
routeId!: string;
@ApiProperty({ example: '2026-06-20T08:00:00.000Z' })
@IsDateString()
scheduleDate!: string;
export class CreateContainerTrainScheduleDto extends PreviewContainerTrainScheduleDto {
@ApiProperty({ format: 'uuid' })
@IsUUID()
locomotiveId!: string;

View File

@@ -17,6 +17,7 @@ const locomotive = {
id: 'loc-1',
code: 'LOC-001',
maxPullWeightTons: 3500,
maxTrainLengthMeters: 760,
status: 'AVAILABLE',
};
@@ -202,47 +203,12 @@ describe('TrainSchedulingService', () => {
});
it('creates a schedule transactionally when validation passes', async () => {
const bookings = [makeBooking('b1', 'BKG-CONT-001', 140, 2, '40FT')];
const validation = {
valid: true,
violations: [],
bookings,
wagonType: nw5,
summary: {
totalBookings: 1,
totalWeightTons: 140,
wagonType: 'NW5',
wagonsNeeded: 2,
totalLengthMeters: 28,
},
wagonPlan: [
{
sequenceNo: 1,
capacityTons: 70,
lengthMeters: 14,
assignedWeightTons: 70,
allocations: [
{
bookingId: 'b1',
bookingReference: 'BKG-CONT-001',
allocatedWeightTons: 70,
},
],
},
{
sequenceNo: 2,
capacityTons: 70,
lengthMeters: 14,
assignedWeightTons: 70,
allocations: [
{
bookingId: 'b1',
bookingReference: 'BKG-CONT-001',
allocatedWeightTons: 70,
},
],
},
],
const route = {
id: 'route-1',
name: 'Djibouti to Addis',
originYardId: 'yard-origin',
destinationYardId: 'yard-destination',
isActive: true,
};
const lockedLocomotiveRepo = {
@@ -253,23 +219,6 @@ describe('TrainSchedulingService', () => {
create: jest.fn().mockImplementation((value) => value),
save: jest.fn().mockResolvedValue({ id: 'schedule-1' }),
};
const trainScheduleBookingRepo = {
count: jest.fn().mockResolvedValue(0),
create: jest.fn().mockImplementation((value) => value),
save: jest.fn().mockResolvedValue(undefined),
};
const trainSetWagonRepo = {
create: jest.fn().mockImplementation((value) => value),
save: jest.fn().mockResolvedValue(undefined),
find: jest.fn().mockResolvedValue([
{ id: 'wagon-1', sequenceNo: 1 },
{ id: 'wagon-2', sequenceNo: 2 },
]),
};
const wagonAllocRepo = {
create: jest.fn().mockImplementation((value) => value),
save: jest.fn().mockResolvedValue(undefined),
};
const trainSetRepo = {
create: jest.fn().mockImplementation((value) => value),
save: jest.fn().mockResolvedValue({ id: 'train-set-1' }),
@@ -281,12 +230,6 @@ describe('TrainSchedulingService', () => {
return lockedLocomotiveRepo;
case 'TrainSchedule':
return trainScheduleRepo;
case 'TrainScheduleBooking':
return trainScheduleBookingRepo;
case 'TrainSetWagon':
return trainSetWagonRepo;
case 'WagonBookingAllocation':
return wagonAllocRepo;
case 'TrainSet':
return trainSetRepo;
default:
@@ -295,70 +238,60 @@ describe('TrainSchedulingService', () => {
}),
};
jest.spyOn(service, 'validateContainerBookingsForScheduling').mockResolvedValue(validation as never);
jest.spyOn(service, 'selectOrValidateLocomotive').mockResolvedValue(locomotive as never);
dataSource.getRepository.mockImplementation((entity: { name?: string }) => {
if (entity?.name === 'Route') {
return { findOne: jest.fn().mockResolvedValue(route) };
}
throw new Error(`Unexpected repository ${entity?.name}`);
});
jest.spyOn(service, 'getContainerTrainScheduleById').mockResolvedValue({ id: 'schedule-1' } as never);
dataSource.transaction.mockImplementation(async (callback: (tx: typeof manager) => Promise<string>) =>
callback(manager),
);
const result = await service.createContainerTrainSchedule({
bookingIds: ['b1'],
routeId: 'route-1',
scheduleDate: '2026-06-20T08:00:00.000Z',
originStationId: 'yard-origin',
destinationStationId: 'yard-destination',
locomotiveId: 'loc-1',
});
expect(trainSetRepo.save).toHaveBeenCalled();
expect(trainScheduleRepo.save).toHaveBeenCalled();
expect(trainSetWagonRepo.save).toHaveBeenCalled();
expect(wagonAllocRepo.save).toHaveBeenCalled();
expect(lockedLocomotiveRepo.update).toHaveBeenCalledWith('loc-1', { status: 'ASSIGNED' });
expect(result).toEqual({ id: 'schedule-1' });
});
it('rejects create when the locked locomotive is no longer available', async () => {
const validation = {
valid: true,
violations: [],
bookings: [makeBooking('b1', 'BKG-CONT-001', 70, 1, '40FT')],
wagonType: nw5,
summary: {
totalBookings: 1,
totalWeightTons: 70,
wagonType: 'NW5',
wagonsNeeded: 1,
totalLengthMeters: 14,
},
wagonPlan: [
{
sequenceNo: 1,
capacityTons: 70,
lengthMeters: 14,
assignedWeightTons: 70,
allocations: [],
},
],
};
const manager = {
getRepository: jest.fn(() => ({
findOne: jest.fn().mockResolvedValue({ ...locomotive, status: 'ASSIGNED' }),
})),
};
jest.spyOn(service, 'validateContainerBookingsForScheduling').mockResolvedValue(validation as never);
jest.spyOn(service, 'selectOrValidateLocomotive').mockResolvedValue(locomotive as never);
dataSource.getRepository.mockImplementation((entity: { name?: string }) => {
if (entity?.name === 'Route') {
return {
findOne: jest.fn().mockResolvedValue({
id: 'route-1',
name: 'Djibouti to Addis',
originYardId: 'yard-origin',
destinationYardId: 'yard-destination',
isActive: true,
}),
};
}
throw new Error(`Unexpected repository ${entity?.name}`);
});
dataSource.transaction.mockImplementation(async (callback: (tx: typeof manager) => Promise<string>) =>
callback(manager),
);
await expect(
service.createContainerTrainSchedule({
bookingIds: ['b1'],
routeId: 'route-1',
scheduleDate: '2026-06-20T08:00:00.000Z',
originStationId: 'yard-origin',
destinationStationId: 'yard-destination',
locomotiveId: 'loc-1',
}),
).rejects.toBeInstanceOf(ConflictException);

View File

@@ -15,9 +15,9 @@ import {
import { LocomotivesRepository } from "../locomotives/locomotives.repository";
import { TrainSetWagon } from "../train-sets/entities/train-set-wagon.entity";
import { TrainSet } from "../train-sets/entities/train-set.entity";
import { Route } from "../routes/entities/route.entity";
import { TrainScheduleBooking } from "../train-schedules/entities/train-schedule-booking.entity";
import { TrainSchedule } from "../train-schedules/entities/train-schedule.entity";
import { WagonBookingAllocation } from "../train-schedules/entities/wagon-booking-allocation.entity";
import { WagonType } from "../wagon-types/entities/wagon-type.entity";
import { WagonTypesRepository } from "../wagon-types/wagon-types.repository";
import { CreateContainerTrainScheduleDto } from "./dto/create-container-train-schedule.dto";
@@ -179,18 +179,12 @@ export class TrainSchedulingService {
}
async createContainerTrainSchedule(dto: CreateContainerTrainScheduleDto) {
const validation = await this.validateContainerBookingsForScheduling(dto);
if (!validation.valid) {
throw new BadRequestException({
message: "train_schedule_invalid",
violations: validation.violations,
});
}
const route = await this.getActiveRoute(dto.routeId);
const locomotive = await this.selectOrValidateLocomotive(
dto.locomotiveId,
validation.summary.totalWeightTons,
0,
0,
);
const createdSchedule = await this.dataSource.transaction(
@@ -211,90 +205,24 @@ export class TrainSchedulingService {
);
}
if (
Number(lockedLocomotive.maxPullWeightTons) <
validation.summary.totalWeightTons
) {
throw new BadRequestException(
`Locomotive ${lockedLocomotive.code} cannot pull ${validation.summary.totalWeightTons}T`,
);
}
const existingScheduleCount = await manager
.getRepository(TrainScheduleBooking)
.count({
where: {
bookingId: In(validation.bookings.map((booking) => booking.id)),
},
});
if (existingScheduleCount > 0) {
throw new BadRequestException(
"One or more bookings are already scheduled",
);
}
const trainSet = await this.buildTrainSet(
const trainSet = await this.buildEmptyTrainSet(
manager,
lockedLocomotive,
validation.wagonType,
validation.summary.totalWeightTons,
validation.summary.totalLengthMeters,
validation.wagonPlan,
);
const schedule = manager.getRepository(TrainSchedule).create({
trainSetId: trainSet.id,
originStationId: dto.originStationId,
destinationStationId: dto.destinationStationId,
routeId: route.id,
originStationId: route.originYardId,
destinationStationId: route.destinationYardId,
scheduledDepartureDate: new Date(dto.scheduleDate),
status: "SCHEDULED",
status: "DRAFT",
});
const savedSchedule = await manager
.getRepository(TrainSchedule)
.save(schedule);
const scheduleBookings = validation.bookings.map((booking) =>
manager.getRepository(TrainScheduleBooking).create({
trainScheduleId: savedSchedule.id,
bookingId: booking.id,
}),
);
await manager
.getRepository(TrainScheduleBooking)
.save(scheduleBookings);
const savedWagons = await manager.getRepository(TrainSetWagon).find({
where: { trainSetId: trainSet.id },
order: { sequenceNo: "ASC" },
});
const wagonBySequence = new Map(
savedWagons.map((wagon) => [wagon.sequenceNo, wagon]),
);
const allocationRows = validation.wagonPlan.flatMap((wagonPlan) => {
const wagon = wagonBySequence.get(wagonPlan.sequenceNo);
if (!wagon) {
throw new BadRequestException(
`Missing wagon sequence ${wagonPlan.sequenceNo}`,
);
}
return wagonPlan.allocations.map((allocation) =>
manager.getRepository(WagonBookingAllocation).create({
trainSetWagonId: wagon.id,
bookingId: allocation.bookingId,
allocatedWeightTons: allocation.allocatedWeightTons,
}),
);
});
await manager
.getRepository(WagonBookingAllocation)
.save(allocationRows);
await locomotiveRepository.update(lockedLocomotive.id, {
status: "ASSIGNED",
});
@@ -461,10 +389,14 @@ export class TrainSchedulingService {
where: { status: "AVAILABLE" },
});
const canPull = capableLocomotives.some(
(locomotive) => Number(locomotive.maxPullWeightTons) >= totalWeightTons,
(locomotive) =>
Number(locomotive.maxPullWeightTons) >= totalWeightTons &&
Number(locomotive.maxTrainLengthMeters) >= totalLengthMeters,
);
if (!canPull) {
violations.push("No available locomotive can pull the total weight");
violations.push(
'No available locomotive can support the total train weight and length',
);
}
}
@@ -513,6 +445,7 @@ export class TrainSchedulingService {
async selectOrValidateLocomotive(
locomotiveId: string,
totalWeightTons: number,
totalLengthMeters: number,
) {
const locomotive = await this.locomotivesRepository.findById(locomotiveId);
@@ -532,6 +465,12 @@ export class TrainSchedulingService {
);
}
if (Number(locomotive.maxTrainLengthMeters) < totalLengthMeters) {
throw new BadRequestException(
`Locomotive ${locomotive.code} cannot support ${totalLengthMeters}m`,
);
}
return locomotive;
}
@@ -568,6 +507,21 @@ export class TrainSchedulingService {
return savedTrainSet;
}
async buildEmptyTrainSet(
manager: EntityManager,
locomotive: Locomotive,
) {
const trainSet = manager.getRepository(TrainSet).create({
locomotiveId: locomotive.id,
totalWeightTons: 0,
totalLengthMeters: 0,
wagonCount: 0,
status: 'DRAFT',
});
return manager.getRepository(TrainSet).save(trainSet);
}
allocateBookingsToWagons(
bookings: Booking[],
baseWagonPlan: WagonPlanRecord[],
@@ -627,6 +581,7 @@ export class TrainSchedulingService {
const schedules = await this.dataSource.getRepository(TrainSchedule).find({
relations: {
trainSet: { locomotive: true },
route: true,
originStation: true,
destinationStation: true,
scheduleBookings: true,
@@ -637,6 +592,7 @@ export class TrainSchedulingService {
return schedules.map((schedule) => ({
id: schedule.id,
scheduleDate: schedule.scheduledDepartureDate,
routeName: schedule.route?.name ?? null,
origin:
schedule.originStation?.label ?? schedule.originStation?.code ?? null,
destination:
@@ -668,6 +624,7 @@ export class TrainSchedulingService {
.findOne({
where: { id },
relations: {
route: true,
trainSet: {
locomotive: true,
wagons: { wagonType: true, allocations: { booking: true } },
@@ -687,6 +644,12 @@ export class TrainSchedulingService {
return {
id: schedule.id,
status: schedule.status,
route: schedule.route
? {
id: schedule.route.id,
name: schedule.route.name,
}
: null,
scheduledDepartureDate: schedule.scheduledDepartureDate,
scheduledArrivalDate: schedule.scheduledArrivalDate,
originStation: schedule.originStation,
@@ -711,6 +674,9 @@ export class TrainSchedulingService {
maxPullWeightTons: this.roundTons(
Number(schedule.trainSet.locomotive.maxPullWeightTons),
),
maxTrainLengthMeters: this.roundTons(
Number(schedule.trainSet.locomotive.maxTrainLengthMeters),
),
}
: null,
wagons: [...(schedule.trainSet.wagons ?? [])]
@@ -806,6 +772,22 @@ export class TrainSchedulingService {
});
}
private async getActiveRoute(routeId: string) {
const route = await this.dataSource.getRepository(Route).findOne({
where: { id: routeId },
});
if (!route) {
throw new NotFoundException(`Route ${routeId} not found`);
}
if (!route.isActive) {
throw new BadRequestException(`Route ${route.name} is inactive`);
}
return route;
}
private toUtcDateKey(value: Date | string) {
const date = value instanceof Date ? value : new Date(value);
return date.toISOString().slice(0, 10);

View File

@@ -11,6 +11,7 @@ import {
Min,
} from 'class-validator';
<<<<<<< HEAD
const toNumber = ({ value }: { value: unknown }) =>
value === '' || value == null ? value : Number(value);
@@ -37,10 +38,28 @@ export class CreateWagonTypeDto {
code!: string;
@ApiProperty({ maxLength: 100, example: 'Flat wagon' })
=======
const parseLoadTypes = (value: unknown): string[] => {
if (Array.isArray(value)) {
return value.map((item) => String(item).trim()).filter(Boolean);
}
if (typeof value === 'string') {
return value
.split(',')
.map((item) => item.trim())
.filter(Boolean);
}
return [];
};
export class CreateWagonTypeDto {
@ApiProperty({ description: 'Display name, e.g. "Flat Wagon"', maxLength: 100 })
>>>>>>> b9cfce70fe17b5066ae5320cfcc595bf3c253467
@IsString()
@MaxLength(100)
name!: string;
<<<<<<< HEAD
@ApiProperty({ example: 60 })
@Transform(toNumber)
@IsNumber()
@@ -65,11 +84,44 @@ export class CreateWagonTypeDto {
@Transform(toStringArray)
@IsArray()
@IsString({ each: true })
=======
@ApiProperty({ description: 'Maximum payload capacity in metric tons' })
@IsNumber()
@Min(0.001)
@Transform(({ value }) => Number(value))
capacityTons!: number;
@ApiProperty({ description: 'Wagon length in meters' })
@IsNumber()
@Min(0.001)
@Transform(({ value }) => Number(value))
lengthMeters!: number;
@ApiPropertyOptional({ description: 'Maximum wagons of this type per train' })
@IsOptional()
@IsInt()
@Min(1)
@Transform(({ value }) => (value === '' || value === null || value === undefined ? undefined : Number(value)))
maxWagonsPerTrain?: number;
@ApiPropertyOptional({
description: 'Supported load types, e.g. CONTAINER,BULK',
type: [String],
default: [],
})
@IsOptional()
@IsArray()
@IsString({ each: true })
@Transform(({ value }) => parseLoadTypes(value))
>>>>>>> b9cfce70fe17b5066ae5320cfcc595bf3c253467
supportedLoadTypes?: string[];
@ApiPropertyOptional({ default: true })
@IsOptional()
<<<<<<< HEAD
@Transform(toBoolean)
=======
>>>>>>> b9cfce70fe17b5066ae5320cfcc595bf3c253467
@IsBoolean()
isActive?: boolean;
}

View File

@@ -11,17 +11,29 @@ import {
Post,
Query,
} from '@nestjs/common';
<<<<<<< HEAD
import { ApiTags, ApiOperation } from '@nestjs/swagger';
import { CreateWagonTypeDto } from './dto/create-wagon-type.dto';
import { UpdateWagonTypeDto } from './dto/update-wagon-type.dto';
import { WagonTypesService } from './wagon-types.service';
import { WagonType } from './entities/wagon-type.entity';
=======
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
>>>>>>> b9cfce70fe17b5066ae5320cfcc595bf3c253467
@ApiTags('Wagon Types')
import { RuleEngineManage, RuleEngineView } from '../../common/rule-engine-guards';
import { CreateWagonTypeDto } from './dto/create-wagon-type.dto';
import { UpdateWagonTypeDto } from './dto/update-wagon-type.dto';
import { WagonTypesService } from './wagon-types.service';
@ApiTags('wagon-types')
@Controller('wagon-types')
@ApiBearerAuth()
export class WagonTypesController {
constructor(private readonly wagonTypesService: WagonTypesService) {}
<<<<<<< HEAD
@Post()
@ApiOperation({ summary: 'Create a wagon type' })
async create(@Body() dto: CreateWagonTypeDto): Promise<WagonType> {
@@ -46,13 +58,51 @@ export class WagonTypesController {
@Param('id', ParseUUIDPipe) id: string,
@Body() dto: UpdateWagonTypeDto,
): Promise<WagonType> {
=======
@Get()
@RuleEngineView('wagon-types')
@ApiOperation({ summary: 'List wagon types' })
findAll(@Query() query: Record<string, string>) {
return this.wagonTypesService.findAll({
isActive: query['isActive'] !== undefined ? query['isActive'] === 'true' : undefined,
page: query['page'] ? parseInt(query['page'], 10) : undefined,
pageSize: query['pageSize'] ? parseInt(query['pageSize'], 10) : undefined,
});
}
@Get(':id')
@RuleEngineView('wagon-types')
@ApiOperation({ summary: 'Get a wagon type by ID' })
findOne(@Param('id', ParseUUIDPipe) id: string) {
return this.wagonTypesService.findById(id);
}
@Post()
@RuleEngineManage('wagon-types')
@ApiOperation({ summary: 'Create a wagon type' })
create(@Body() dto: CreateWagonTypeDto) {
return this.wagonTypesService.create(dto);
}
@Patch(':id')
@RuleEngineManage('wagon-types')
@ApiOperation({ summary: 'Update a wagon type' })
update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateWagonTypeDto) {
>>>>>>> b9cfce70fe17b5066ae5320cfcc595bf3c253467
return this.wagonTypesService.update(id, dto);
}
@Delete(':id')
<<<<<<< HEAD
@HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({ summary: 'Deactivate a wagon type' })
async remove(@Param('id', ParseUUIDPipe) id: string): Promise<void> {
=======
@RuleEngineManage('wagon-types')
@HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({ summary: 'Soft-delete a wagon type' })
remove(@Param('id', ParseUUIDPipe) id: string) {
>>>>>>> b9cfce70fe17b5066ae5320cfcc595bf3c253467
return this.wagonTypesService.remove(id);
}
}

View File

@@ -13,4 +13,8 @@ export class WagonTypesRepository extends BaseRepository<WagonType> {
) {
super(repository);
}
findByCode(code: string): Promise<WagonType | null> {
return this.repository.findOne({ where: { code } });
}
}

View File

@@ -1,6 +1,17 @@
<<<<<<< HEAD
import { ConflictException, Injectable, NotFoundException } from '@nestjs/common';
import { FindOptionsOrder } from 'typeorm';
=======
import {
ConflictException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { generateCode } from '../../common/utils/generate-code.util';
>>>>>>> b9cfce70fe17b5066ae5320cfcc595bf3c253467
import { CreateWagonTypeDto } from './dto/create-wagon-type.dto';
import { UpdateWagonTypeDto } from './dto/update-wagon-type.dto';
import { WagonType } from './entities/wagon-type.entity';
@@ -10,6 +21,7 @@ import { WagonTypesRepository } from './wagon-types.repository';
export class WagonTypesService {
constructor(private readonly wagonTypesRepository: WagonTypesRepository) {}
<<<<<<< HEAD
async create(dto: CreateWagonTypeDto): Promise<WagonType> {
const code = dto.code.trim().toUpperCase();
const existing = await this.wagonTypesRepository.findAll({ where: { code } });
@@ -23,7 +35,47 @@ export class WagonTypesService {
name: dto.name.trim(),
supportedLoadTypes: dto.supportedLoadTypes ?? [],
isActive: dto.isActive ?? true,
=======
async findAll(filter: {
isActive?: boolean;
page?: number;
pageSize?: number;
} = {}): Promise<{
data: WagonType[];
meta: { total: number; page: number; pageSize: number; totalPages: number };
}> {
const page = filter.page ?? 1;
const pageSize = filter.pageSize ?? 20;
const where: Record<string, unknown> = {};
if (filter.isActive !== undefined) {
where.isActive = filter.isActive;
}
const [data, total] = await this.wagonTypesRepository.findAndCount({
where,
order: { code: 'ASC' },
skip: (page - 1) * pageSize,
take: pageSize,
>>>>>>> b9cfce70fe17b5066ae5320cfcc595bf3c253467
});
return {
data,
meta: {
total,
page,
pageSize,
totalPages: Math.max(1, Math.ceil(total / pageSize)),
},
};
}
async findById(id: string): Promise<WagonType> {
const wagonType = await this.wagonTypesRepository.findById(id);
if (!wagonType) {
throw new NotFoundException(`Wagon type ${id} not found`);
}
return wagonType;
}
async findAll(query: Record<string, string | undefined> = {}): Promise<WagonType[]> {
@@ -57,15 +109,14 @@ export class WagonTypesService {
}
async findByCode(code: string): Promise<WagonType> {
const [wagonType] = await this.wagonTypesRepository.findAll({ where: { code } });
const wagonType = await this.wagonTypesRepository.findByCode(code);
if (!wagonType) {
throw new NotFoundException(`Wagon type ${code} not found`);
}
return wagonType;
}
<<<<<<< HEAD
async update(id: string, dto: UpdateWagonTypeDto): Promise<WagonType> {
const wagonType = await this.findById(id);
const nextCode = dto.code?.trim().toUpperCase();
@@ -87,11 +138,43 @@ export class WagonTypesService {
throw new NotFoundException(`Wagon type ${id} not found`);
}
=======
async create(dto: CreateWagonTypeDto): Promise<WagonType> {
const code = generateCode(dto.name);
const existing = await this.wagonTypesRepository.findByCode(code);
if (existing) {
throw new ConflictException(
`Wagon type with name "${dto.name}" conflicts with existing code "${code}"`,
);
}
return this.wagonTypesRepository.create({
code,
name: dto.name,
capacityTons: dto.capacityTons,
lengthMeters: dto.lengthMeters,
maxWagonsPerTrain: dto.maxWagonsPerTrain ?? null,
supportedLoadTypes: dto.supportedLoadTypes ?? [],
isActive: dto.isActive ?? true,
});
}
async update(id: string, dto: UpdateWagonTypeDto): Promise<WagonType> {
await this.findById(id);
const updated = await this.wagonTypesRepository.update(id, dto);
if (!updated) {
throw new NotFoundException(`Wagon type ${id} not found`);
}
>>>>>>> b9cfce70fe17b5066ae5320cfcc595bf3c253467
return updated;
}
async remove(id: string): Promise<void> {
await this.findById(id);
<<<<<<< HEAD
await this.wagonTypesRepository.update(id, { isActive: false });
=======
await this.wagonTypesRepository.softDelete(id);
>>>>>>> b9cfce70fe17b5066ae5320cfcc595bf3c253467
}
}