trains, wagons,containers and cargoes schema and API

This commit is contained in:
hagiye
2026-06-04 15:56:29 +03:00
parent 18015ceaff
commit 71ac89edc7
66 changed files with 2077 additions and 90 deletions

View File

@@ -0,0 +1,63 @@
import {
Body,
Controller,
Delete,
Get,
Param,
ParseUUIDPipe,
Patch,
Post,
} from '@nestjs/common';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { CreateContainerDto } from './dto/create-container.dto';
import { UpdateContainerDto } from './dto/update-container.dto';
import { AssignContainerToWagonDto } from './dto/assign-container-to-wagon.dto';
import { ContainersService } from './containers.service';
@ApiTags('containers')
@Controller('containers')
export class ContainersController {
constructor(private readonly containersService: ContainersService) {}
@Post()
@ApiOperation({ summary: 'Create a new container' })
create(@Body() dto: CreateContainerDto) {
return this.containersService.create(dto);
}
@Get()
@ApiOperation({ summary: 'List all containers' })
findAll() {
return this.containersService.findAll();
}
@Get(':id')
@ApiOperation({ summary: 'Get a container by ID' })
findOne(@Param('id', ParseUUIDPipe) id: string) {
return this.containersService.findById(id);
}
@Patch(':id')
@ApiOperation({ summary: 'Update a container' })
update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateContainerDto) {
return this.containersService.update(id, dto);
}
@Delete(':id')
@ApiOperation({ summary: 'Delete a container' })
remove(@Param('id', ParseUUIDPipe) id: string) {
return this.containersService.remove(id);
}
@Post(':id/assign-wagon')
@ApiOperation({ summary: 'Assign container to a wagon' })
assignToWagon(@Param('id', ParseUUIDPipe) id: string, @Body() dto: AssignContainerToWagonDto) {
return this.containersService.assignToWagon(id, dto);
}
@Post(':id/unassign-wagon')
@ApiOperation({ summary: 'Unassign container from wagon' })
unassignFromWagon(@Param('id', ParseUUIDPipe) id: string) {
return this.containersService.unassignFromWagon(id);
}
}

View File

@@ -0,0 +1,14 @@
// apps/edr-freight-api/src/modules/container-management/containers.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Container } from './entities/container.entity';
import { Wagon } from '../wagons/entities/wagon.entity';
import { ContainersController } from './containers.controller';
import { ContainersService } from './containers.service';
@Module({
imports: [TypeOrmModule.forFeature([Container, Wagon])], // ✅ add Wagon
controllers: [ContainersController],
providers: [ContainersService],
})
export class ContainersModule {}

View File

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

View File

@@ -0,0 +1,86 @@
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CreateContainerDto } from './dto/create-container.dto';
import { UpdateContainerDto } from './dto/update-container.dto';
import { AssignContainerToWagonDto } from './dto/assign-container-to-wagon.dto';
import { Container } from './entities/container.entity';
//import { ContainersRepository } from './containers.repository';
import { WagonsRepository } from '../wagons/wagons.repository';
@Injectable()
export class ContainersService {
constructor(
@InjectRepository(Container)
private readonly containerRepo: Repository<Container>,
private readonly wagonsRepository: WagonsRepository,
) {}
async create(dto: CreateContainerDto): Promise<Container> {
const container = this.containerRepo.create(dto);
// Convert undefined to null for optional fields
if (dto.wagonId === undefined) container.wagonId = null;
if (dto.position === undefined) container.position = null;
return this.containerRepo.save(container);
}
async findAll(): Promise<Container[]> {
return this.containerRepo.find({ order: { containerNumber: 'ASC' } });
}
async findById(id: string): Promise<Container> {
const container = await this.containerRepo.findOne({ where: { id } });
if (!container) throw new NotFoundException(`Container ${id} not found`);
return container;
}
async update(id: string, dto: UpdateContainerDto): Promise<Container> {
const container = await this.findById(id);
Object.assign(container, dto);
// Convert undefined to null for nullable fields
if (dto.wagonId === undefined) container.wagonId = null;
if (dto.position === undefined) container.position = null;
return this.containerRepo.save(container);
}
async remove(id: string): Promise<void> {
const container = await this.findById(id);
await this.containerRepo.remove(container);
}
async assignToWagon(containerId: string, dto: AssignContainerToWagonDto): Promise<Container> {
const container = await this.findById(containerId);
if (container.status === 'LOADED') {
throw new ConflictException('Cannot reassign a loaded container');
}
const wagon = await this.wagonsRepository.findById(dto.wagonId);
if (!wagon) throw new NotFoundException('Wagon not found');
let position: number | null = dto.position ?? null; // convert undefined to null
if (position === null) {
const maxPos = await this.containerRepo
.createQueryBuilder('c')
.select('MAX(c.position)', 'max')
.where('c.wagonId = :wagonId', { wagonId: wagon.id })
.getRawOne();
position = (maxPos?.max ?? 0) + 1;
}
container.wagonId = wagon.id;
container.position = position; // now position is number | null, safe
container.status = 'AVAILABLE';
return this.containerRepo.save(container);
}
async unassignFromWagon(containerId: string): Promise<Container> {
const container = await this.findById(containerId);
if (container.status === 'LOADED') {
throw new ConflictException('Cannot unassign a loaded container');
}
container.wagonId = null;
container.position = null;
container.status = 'AVAILABLE';
return this.containerRepo.save(container);
}
}

View File

@@ -0,0 +1,85 @@
// apps/edr-freight-api/src/modules/container-management/containers.service.ts
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CreateContainerDto } from './dto/create-container.dto';
import { UpdateContainerDto } from './dto/update-container.dto';
import { AssignContainerToWagonDto } from './dto/assign-container-to-wagon.dto';
import { Container } from './entities/container.entity';
import { Wagon } from '../wagons/entities/wagon.entity';
@Injectable()
export class ContainersService {
constructor(
@InjectRepository(Container)
private readonly containerRepo: Repository<Container>,
@InjectRepository(Wagon)
private readonly wagonRepo: Repository<Wagon>, // ✅ use raw repository
) {}
async create(dto: CreateContainerDto): Promise<Container> {
const container = this.containerRepo.create(dto);
if (dto.wagonId === undefined) container.wagonId = null;
if (dto.position === undefined) container.position = null;
return this.containerRepo.save(container);
}
async findAll(): Promise<Container[]> {
return this.containerRepo.find({ order: { containerNumber: 'ASC' } });
}
async findById(id: string): Promise<Container> {
const container = await this.containerRepo.findOne({ where: { id } });
if (!container) throw new NotFoundException(`Container ${id} not found`);
return container;
}
async update(id: string, dto: UpdateContainerDto): Promise<Container> {
const container = await this.findById(id);
Object.assign(container, dto);
if (dto.wagonId === undefined) container.wagonId = null;
if (dto.position === undefined) container.position = null;
return this.containerRepo.save(container);
}
async remove(id: string): Promise<void> {
const container = await this.findById(id);
await this.containerRepo.remove(container);
}
async assignToWagon(containerId: string, dto: AssignContainerToWagonDto): Promise<Container> {
const container = await this.findById(containerId);
if (container.status === 'LOADED') {
throw new ConflictException('Cannot reassign a loaded container');
}
const wagon = await this.wagonRepo.findOne({ where: { id: dto.wagonId } });
if (!wagon) throw new NotFoundException(`Wagon ${dto.wagonId} not found`);
let position: number | null = dto.position ?? null;
if (position === null) {
const maxPos = await this.containerRepo
.createQueryBuilder('c')
.select('MAX(c.position)', 'max')
.where('c.wagonId = :wagonId', { wagonId: wagon.id })
.getRawOne();
position = (maxPos?.max ?? 0) + 1;
}
container.wagonId = wagon.id;
container.position = position;
container.status = 'AVAILABLE';
return this.containerRepo.save(container);
}
async unassignFromWagon(containerId: string): Promise<Container> {
const container = await this.findById(containerId);
if (container.status === 'LOADED') {
throw new ConflictException('Cannot unassign a loaded container');
}
container.wagonId = null;
container.position = null;
container.status = 'AVAILABLE';
return this.containerRepo.save(container);
}
}

View File

@@ -0,0 +1,11 @@
import { IsUUID, IsOptional, IsInt, Min } from 'class-validator';
export class AssignContainerToWagonDto {
@IsUUID()
wagonId!: string;
@IsOptional()
@IsInt()
@Min(1)
position?: number;
}

View File

@@ -0,0 +1,34 @@
import { IsString, IsUUID, IsOptional, IsInt, Min, IsNumber, IsIn } from 'class-validator';
export class CreateContainerDto {
@IsString()
containerNumber!: string;
@IsUUID()
containerTypeId!: string;
@IsOptional()
@IsUUID()
wagonId?: string;
@IsOptional()
@IsInt()
@Min(1)
position?: number;
@IsNumber()
@Min(0)
tareWeight!: number;
@IsNumber()
@Min(0)
maxGrossWeight!: number;
@IsOptional()
@IsString()
sealNumber?: string;
@IsOptional()
@IsIn(['AVAILABLE', 'LOADED', 'IN_TRANSIT', 'MAINTENANCE', 'DAMAGED'])
status?: string;
}

View File

@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { CreateContainerDto } from './create-container.dto';
export class UpdateContainerDto extends PartialType(CreateContainerDto) {}

View File

@@ -0,0 +1,45 @@
// apps/edr-freight-api/src/modules/container-management/entities/container.entity.ts
import { Entity, Column, ManyToOne, OneToMany, JoinColumn } from 'typeorm';
import { BaseEntity } from '@edr/api-common';
import { Wagon } from '../../wagons/entities/wagon.entity';
import { Cargo } from '../../cargoes/entities/cargoes.entity';
@Entity({ name: 'containers', schema: 'freight' })
export class Container extends BaseEntity {
@Column({ unique: true, name: 'container_number' })
containerNumber!: string;
@Column({ name: 'container_type_id', type: 'uuid' })
containerTypeId!: string;
@Column({ name: 'wagon_id', type: 'uuid', nullable: true })
wagonId!: string | null;
@Column({ type: 'int', nullable: true })
position!: number | null; // position on the wagon (1..N)
@Column({ name: 'tare_weight', type: 'decimal', precision: 10, scale: 2 })
tareWeight!: number;
@Column({ name: 'max_gross_weight', type: 'decimal', precision: 10, scale: 2 })
maxGrossWeight!: number;
@Column({
name: 'seal_number',
type: 'varchar',
nullable: true,
})
sealNumber!: string | null;
@Column({ type: 'varchar', default: 'AVAILABLE' })
status!: string; // AVAILABLE, LOADED, IN_TRANSIT, MAINTENANCE, DAMAGED
// Relationship to Wagon
@ManyToOne(() => Wagon, (wagon) => wagon.containers, { onDelete: 'SET NULL' })
@JoinColumn({ name: 'wagon_id' })
wagon!: Wagon | null;
// Relationship to Cargo
@OneToMany(() => Cargo, (cargo) => cargo.container)
cargoes!: Cargo[];
}