mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 00:52:50 +00:00
Merge pull request #212 from Tria-plc/freight_hotfix_2
fix data integrity issue
This commit is contained in:
@@ -44,7 +44,6 @@ import { EdrOrgSeeder } from "./seed/edr-org.seeder";
|
||||
import { DemoUsersSeeder } from "./seed/demo-users.seeder";
|
||||
import { FreightStaffUsersSeeder } from "./seed/freight-staff-users.seeder";
|
||||
import { PaymentModule } from "./modules/payment/payment.module";
|
||||
import { DemoBookingsSeeder } from "./seed/demo-bookings.seeder";
|
||||
import { PricingDataSeeder } from "./seed/pricing-data.seeder";
|
||||
import { FileUploadSettingsSeeder } from "./seed/file-upload-settings.seeder";
|
||||
import { FreightPermissionKeyMigrationSeeder } from "./seed/freight-permission-key-migration.seeder";
|
||||
@@ -56,6 +55,7 @@ import { ContainersModule } from './modules/container-management/containers.modu
|
||||
import { CargoesModule } from './modules/cargoes/cargoes.module';
|
||||
import { RoutesModule } from './modules/routes/routes.module';
|
||||
import { OverviewModule } from './modules/overview/overview.module';
|
||||
import { VehiclesModule } from './modules/vehicles/vehicles.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -113,12 +113,12 @@ import { OverviewModule } from './modules/overview/overview.module';
|
||||
CargoesModule,
|
||||
RoutesModule,
|
||||
OverviewModule,
|
||||
VehiclesModule,
|
||||
],
|
||||
providers: [
|
||||
EdrOrgSeeder,
|
||||
DemoUsersSeeder,
|
||||
FreightStaffUsersSeeder,
|
||||
DemoBookingsSeeder,
|
||||
PricingDataSeeder,
|
||||
FileUploadSettingsSeeder,
|
||||
FreightPermissionKeyMigrationSeeder,
|
||||
@@ -131,7 +131,6 @@ export class AppModule implements OnApplicationBootstrap {
|
||||
private readonly edrOrgSeeder: EdrOrgSeeder,
|
||||
private readonly demoUsersSeeder: DemoUsersSeeder,
|
||||
private readonly freightStaffUsersSeeder: FreightStaffUsersSeeder,
|
||||
private readonly demoBookingsSeeder: DemoBookingsSeeder,
|
||||
private readonly pricingDataSeeder: PricingDataSeeder,
|
||||
private readonly fileUploadSettingsSeeder: FileUploadSettingsSeeder,
|
||||
private readonly freightPermissionKeyMigrationSeeder: FreightPermissionKeyMigrationSeeder,
|
||||
@@ -144,7 +143,8 @@ export class AppModule implements OnApplicationBootstrap {
|
||||
await this.edrOrgSeeder.run();
|
||||
await this.demoUsersSeeder.run();
|
||||
await this.freightStaffUsersSeeder.run();
|
||||
await this.demoBookingsSeeder.run();
|
||||
// Temporarily skip demo bookings seeder to debug
|
||||
// await this.demoBookingsSeeder.run();
|
||||
await this.pricingDataSeeder.run();
|
||||
await this.fileUploadSettingsSeeder.run();
|
||||
// Idempotent demo data: ≥100 wagons/type, approval chains, 4 staff users.
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class CreateVehiclesTable1770000000000 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'vehicles' AND table_schema = 'freight') THEN
|
||||
CREATE TABLE freight.vehicles (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
plate_number VARCHAR NOT NULL UNIQUE,
|
||||
registration_number VARCHAR NOT NULL UNIQUE,
|
||||
vehicle_type VARCHAR NOT NULL,
|
||||
manufacturer VARCHAR NOT NULL,
|
||||
model VARCHAR NOT NULL,
|
||||
year INTEGER NOT NULL,
|
||||
fuel_type VARCHAR NOT NULL,
|
||||
capacity NUMERIC NOT NULL,
|
||||
status VARCHAR DEFAULT 'ACTIVE' NOT NULL,
|
||||
description TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
deleted_at TIMESTAMP NULL
|
||||
);
|
||||
|
||||
CREATE INDEX idx_vehicles_plate_number ON freight.vehicles(plate_number);
|
||||
CREATE INDEX idx_vehicles_registration_number ON freight.vehicles(registration_number);
|
||||
CREATE INDEX idx_vehicles_status ON freight.vehicles(status);
|
||||
CREATE INDEX idx_vehicles_vehicle_type ON freight.vehicles(vehicle_type);
|
||||
CREATE INDEX idx_vehicles_manufacturer ON freight.vehicles(manufacturer);
|
||||
END IF;
|
||||
END $$;
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS freight.vehicles CASCADE;`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { IsString, IsEnum, IsNumber, IsOptional } from 'class-validator';
|
||||
import { VehicleType, FuelType, VehicleStatus } from '../entities/vehicle.entity';
|
||||
|
||||
export class CreateVehicleDto {
|
||||
@IsString()
|
||||
plateNumber!: string;
|
||||
|
||||
@IsEnum(VehicleType)
|
||||
vehicleType!: VehicleType;
|
||||
|
||||
@IsString()
|
||||
manufacturer!: string;
|
||||
|
||||
@IsString()
|
||||
model!: string;
|
||||
|
||||
@IsNumber()
|
||||
year!: number;
|
||||
|
||||
@IsEnum(FuelType)
|
||||
fuelType!: FuelType;
|
||||
|
||||
@IsNumber()
|
||||
capacity!: number;
|
||||
|
||||
@IsEnum(VehicleStatus)
|
||||
status!: VehicleStatus;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from '@nestjs/mapped-types';
|
||||
import { CreateVehicleDto } from './create-vehicle.dto';
|
||||
|
||||
export class UpdateVehicleDto extends PartialType(CreateVehicleDto) {}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { Entity, Column, Index } from 'typeorm';
|
||||
import { BaseEntity } from '@edr/api-common';
|
||||
|
||||
export enum VehicleType {
|
||||
TRUCK = 'TRUCK',
|
||||
VAN = 'VAN',
|
||||
CAR = 'CAR',
|
||||
BUS = 'BUS',
|
||||
TRAILER = 'TRAILER',
|
||||
TANKER = 'TANKER',
|
||||
FLATBED = 'FLATBED',
|
||||
}
|
||||
|
||||
export enum FuelType {
|
||||
PETROL = 'PETROL',
|
||||
DIESEL = 'DIESEL',
|
||||
ELECTRIC = 'ELECTRIC',
|
||||
HYBRID = 'HYBRID',
|
||||
}
|
||||
|
||||
export enum VehicleStatus {
|
||||
ACTIVE = 'ACTIVE',
|
||||
MAINTENANCE = 'MAINTENANCE',
|
||||
RETIRED = 'RETIRED',
|
||||
OUT_OF_SERVICE = 'OUT_OF_SERVICE',
|
||||
}
|
||||
|
||||
@Entity({ name: 'vehicles', schema: 'freight' })
|
||||
@Index(['plateNumber'])
|
||||
@Index(['registrationNumber'])
|
||||
@Index(['status'])
|
||||
@Index(['vehicleType'])
|
||||
@Index(['manufacturer'])
|
||||
export class Vehicle extends BaseEntity {
|
||||
@Column({ name: 'plate_number', unique: true })
|
||||
plateNumber!: string;
|
||||
|
||||
@Column({ name: 'registration_number', unique: true })
|
||||
registrationNumber!: string;
|
||||
|
||||
@Column({ name: 'vehicle_type', type: 'varchar' })
|
||||
vehicleType!: VehicleType;
|
||||
|
||||
@Column()
|
||||
manufacturer!: string;
|
||||
|
||||
@Column()
|
||||
model!: string;
|
||||
|
||||
@Column()
|
||||
year!: number;
|
||||
|
||||
@Column({ name: 'fuel_type', type: 'varchar' })
|
||||
fuelType!: FuelType;
|
||||
|
||||
@Column()
|
||||
capacity!: number;
|
||||
|
||||
@Column({ name: 'status', type: 'varchar', default: VehicleStatus.ACTIVE })
|
||||
status!: VehicleStatus;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
description!: string | null;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Patch,
|
||||
Delete,
|
||||
Param,
|
||||
Body,
|
||||
Query,
|
||||
ParseUUIDPipe,
|
||||
} from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { FleetManage, FleetView } from '../../common/booking-guards';
|
||||
import { VehiclesService } from './vehicles.service';
|
||||
import { CreateVehicleDto } from './dto/create-vehicle.dto';
|
||||
import { UpdateVehicleDto } from './dto/update-vehicle.dto';
|
||||
|
||||
@ApiTags('vehicles')
|
||||
@ApiBearerAuth()
|
||||
@Controller('vehicles')
|
||||
@FleetView()
|
||||
export class VehiclesController {
|
||||
constructor(private readonly vehiclesService: VehiclesService) {}
|
||||
|
||||
@Post()
|
||||
@FleetManage()
|
||||
@ApiOperation({ summary: 'Create a new vehicle' })
|
||||
create(@Body() createVehicleDto: CreateVehicleDto) {
|
||||
return this.vehiclesService.create(createVehicleDto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Get all vehicles with filters' })
|
||||
findAll(
|
||||
@Query('search') search?: string,
|
||||
@Query('status') status?: string,
|
||||
@Query('page') page?: string,
|
||||
@Query('limit') limit?: string,
|
||||
@Query('sortBy') sortBy?: string,
|
||||
@Query('sortOrder') sortOrder?: 'ASC' | 'DESC',
|
||||
) {
|
||||
return this.vehiclesService.findAll({
|
||||
search,
|
||||
status: status as any,
|
||||
page: page ? parseInt(page) : undefined,
|
||||
limit: limit ? parseInt(limit) : undefined,
|
||||
sortBy,
|
||||
sortOrder,
|
||||
});
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Get vehicle by id' })
|
||||
findOne(@Param('id', ParseUUIDPipe) id: string) {
|
||||
return this.vehiclesService.findById(id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@FleetManage()
|
||||
@ApiOperation({ summary: 'Update a vehicle' })
|
||||
update(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@Body() updateVehicleDto: UpdateVehicleDto,
|
||||
) {
|
||||
return this.vehiclesService.update(id, updateVehicleDto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@FleetManage()
|
||||
@ApiOperation({ summary: 'Delete a vehicle' })
|
||||
remove(@Param('id', ParseUUIDPipe) id: string) {
|
||||
return this.vehiclesService.remove(id);
|
||||
}
|
||||
}
|
||||
13
apps/edr-freight-api/src/modules/vehicles/vehicles.module.ts
Normal file
13
apps/edr-freight-api/src/modules/vehicles/vehicles.module.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Vehicle } from './entities/vehicle.entity';
|
||||
import { VehiclesService } from './vehicles.service';
|
||||
import { VehiclesController } from './vehicles.controller';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Vehicle])],
|
||||
providers: [VehiclesService],
|
||||
controllers: [VehiclesController],
|
||||
exports: [VehiclesService],
|
||||
})
|
||||
export class VehiclesModule {}
|
||||
@@ -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 { Vehicle } from './entities/vehicle.entity';
|
||||
|
||||
@Injectable()
|
||||
export class VehiclesRepository extends BaseRepository<Vehicle> {
|
||||
constructor(
|
||||
@InjectRepository(Vehicle)
|
||||
repository: Repository<Vehicle>,
|
||||
) {
|
||||
super(repository);
|
||||
}
|
||||
}
|
||||
109
apps/edr-freight-api/src/modules/vehicles/vehicles.service.ts
Normal file
109
apps/edr-freight-api/src/modules/vehicles/vehicles.service.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { CreateVehicleDto } from './dto/create-vehicle.dto';
|
||||
import { UpdateVehicleDto } from './dto/update-vehicle.dto';
|
||||
import { Vehicle, VehicleStatus } from './entities/vehicle.entity';
|
||||
|
||||
@Injectable()
|
||||
export class VehiclesService {
|
||||
constructor(
|
||||
@InjectRepository(Vehicle)
|
||||
private readonly vehicleRepo: Repository<Vehicle>,
|
||||
) {}
|
||||
|
||||
async create(dto: CreateVehicleDto): Promise<Vehicle> {
|
||||
const existing = await this.vehicleRepo.findOne({
|
||||
where: { plateNumber: dto.plateNumber },
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
throw new ConflictException(
|
||||
`Vehicle with plate number ${dto.plateNumber} already exists`,
|
||||
);
|
||||
}
|
||||
|
||||
const registrationNumber = `REG-${dto.vehicleType}-${Date.now()}`;
|
||||
const vehicle = this.vehicleRepo.create({
|
||||
...dto,
|
||||
registrationNumber,
|
||||
});
|
||||
|
||||
return this.vehicleRepo.save(vehicle);
|
||||
}
|
||||
|
||||
async findAll(query: {
|
||||
search?: string;
|
||||
status?: VehicleStatus | string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
sortBy?: string;
|
||||
sortOrder?: 'ASC' | 'DESC';
|
||||
} = {}): Promise<{ data: Vehicle[]; total: number; page: number; limit: number }> {
|
||||
const page = query.page || 1;
|
||||
const limit = query.limit || 10;
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const where: any = {};
|
||||
if (query.status) where.status = query.status;
|
||||
|
||||
let qb = this.vehicleRepo.createQueryBuilder('v');
|
||||
|
||||
if (query.search) {
|
||||
qb = qb.where(
|
||||
'v.plateNumber ILIKE :search OR v.manufacturer ILIKE :search',
|
||||
{ search: `%${query.search}%` },
|
||||
);
|
||||
}
|
||||
|
||||
if (query.status) {
|
||||
qb = qb.andWhere('v.status = :status', { status: query.status });
|
||||
}
|
||||
|
||||
const sortBy = ['plateNumber', 'status', 'year', 'createdAt'].includes(
|
||||
query.sortBy ?? '',
|
||||
)
|
||||
? query.sortBy
|
||||
: 'createdAt';
|
||||
const sortOrder = (query.sortOrder ?? 'DESC').toUpperCase();
|
||||
|
||||
const [data, total] = await qb
|
||||
.orderBy(`v.${sortBy}`, sortOrder as 'ASC' | 'DESC')
|
||||
.skip(skip)
|
||||
.take(limit)
|
||||
.getManyAndCount();
|
||||
|
||||
return { data, total, page, limit };
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<Vehicle> {
|
||||
const vehicle = await this.vehicleRepo.findOne({ where: { id } });
|
||||
if (!vehicle) {
|
||||
throw new NotFoundException(`Vehicle ${id} not found`);
|
||||
}
|
||||
return vehicle;
|
||||
}
|
||||
|
||||
async update(id: string, dto: UpdateVehicleDto): Promise<Vehicle> {
|
||||
const vehicle = await this.findById(id);
|
||||
|
||||
if (dto.plateNumber && dto.plateNumber !== vehicle.plateNumber) {
|
||||
const existing = await this.vehicleRepo.findOne({
|
||||
where: { plateNumber: dto.plateNumber },
|
||||
});
|
||||
if (existing) {
|
||||
throw new ConflictException(
|
||||
`Vehicle with plate number ${dto.plateNumber} already exists`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(vehicle, dto);
|
||||
return this.vehicleRepo.save(vehicle);
|
||||
}
|
||||
|
||||
async remove(id: string): Promise<void> {
|
||||
await this.findById(id);
|
||||
await this.vehicleRepo.softDelete(id);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user