complete rule engine and booking flow

This commit is contained in:
marshal
2026-05-30 11:52:50 +03:00
parent 7bbb95e9fd
commit badeeff345
15 changed files with 575 additions and 348 deletions

View File

@@ -56,6 +56,8 @@ export class BookingsRepository extends BaseRepository<Booking> {
.createQueryBuilder('booking')
.leftJoinAndSelect('booking.bookingContainers', 'bc')
.leftJoinAndSelect('bc.containerType', 'ct')
.leftJoinAndSelect('booking.customer', 'customer')
.leftJoinAndSelect('booking.train', 'train')
.leftJoinAndSelect('booking.serviceType', 'st')
.leftJoinAndSelect('booking.cargoType', 'cargo')
.leftJoinAndSelect('booking.originYard', 'oy')

View File

@@ -1,9 +1,11 @@
import { BaseEntity } from '@edr/api-common';
import { Column, Entity, JoinColumn, ManyToOne, OneToMany } from 'typeorm';
import { Customer } from '../../customers/entities/customer.entity';
import { CargoType } from '../../rule-engine/entities/cargo-type.entity';
import { ServiceType } from '../../rule-engine/entities/service-type.entity';
import { ShippingLine } from '../../rule-engine/entities/shipping-line.entity';
import { Yard } from '../../rule-engine/entities/yard.entity';
import { Train } from '../../trains/entities/train.entity';
import { FileRecord } from '../../files/entities/file.entity';
import { BookingApprovalStep } from './booking-approval-step.entity';
import { BookingCargoModifier } from './booking-cargo-modifier.entity';
@@ -36,9 +38,17 @@ export class Booking extends BaseEntity {
@Column({ name: 'customer_id', type: 'uuid' })
customerId!: string;
@ManyToOne(() => Customer)
@JoinColumn({ name: 'customer_id' })
customer?: Customer;
@Column({ name: 'train_id', type: 'uuid', nullable: true })
trainId?: string | null;
@ManyToOne(() => Train, { nullable: true })
@JoinColumn({ name: 'train_id' })
train?: Train | null;
@Column({ name: 'status', type: 'varchar', length: 40, default: 'DRAFT' })
status!: string;
@@ -57,6 +67,10 @@ export class Booking extends BaseEntity {
@Column({ name: 'previous_contract_id', type: 'uuid', nullable: true })
previousContractId?: string | null;
@ManyToOne(() => Booking, { nullable: true })
@JoinColumn({ name: 'previous_contract_id' })
previousContract?: Booking | null;
@Column({ name: 'service_type_id', type: 'uuid' })
serviceTypeId!: string;
@@ -164,6 +178,10 @@ export class Booking extends BaseEntity {
@Column({ name: 'consolidation_partner_id', type: 'uuid', nullable: true })
consolidationPartnerId?: string | null;
@ManyToOne(() => Booking, { nullable: true })
@JoinColumn({ name: 'consolidation_partner_id' })
consolidationPartner?: Booking | null;
@OneToMany(() => BookingContainer, (bc) => bc.booking)
bookingContainers?: BookingContainer[];

View File

@@ -44,7 +44,7 @@ export class CustomersRepository {
async findByName(name: string): Promise<Customer[]> {
return await this.repository
.createQueryBuilder("customer")
.where("customer.name ILIKE :name", { name: `%${name}%` })
.where("customer.companyName ILIKE :name", { name: `%${name}%` })
.getMany();
}

View File

@@ -43,7 +43,7 @@ export class ResponseCustomerDto {
this.contactPersonName = customer.contactPersonName;
this.contactPersonPhone = customer.contactPersonPhone;
this.tinNumber = customer.tinNumber;
this.vatNumber = customer.vatNumber;
this.vatNumber = customer.vatNumber ?? undefined;
this.fanNumber = customer.fanNumber;
this.generalManagerName = customer.generalManagerName;
this.generalManagerEmail = customer.generalManagerEmail;

View File

@@ -1,100 +1,87 @@
import {
Column,
Entity,
CreateDateColumn,
UpdateDateColumn,
Index,
BaseEntity,
PrimaryGeneratedColumn,
} from "typeorm";
import { BaseEntity } from '@edr/api-common';
import { Column, Entity, Index } from 'typeorm';
@Entity("customers")
@Entity({ schema: 'freight', name: 'customers' })
@Index(['email'])
@Index(['userId'])
@Index(['tinNumber'])
@Index(['fanNumber'])
export class Customer extends BaseEntity {
@PrimaryGeneratedColumn("uuid")
id!: string;
@Column({ type: "uuid" })
@Index()
@Column({ name: 'user_id', type: 'uuid' })
userId!: string;
@Column({ length: 100 })
@Index()
@Column({ name: 'first_name', type: 'varchar', length: 100 })
firstName!: string;
@Column({ length: 100 })
@Index()
@Column({ name: 'last_name', type: 'varchar', length: 100 })
lastName!: string;
@Column({ unique: true, length: 150 })
@Index()
@Column({ name: 'email', type: 'varchar', length: 150, unique: true })
email!: string;
@Column({ length: 20 })
@Column({ name: 'phone', type: 'varchar', length: 20 })
phone!: string;
@Column({ length: 200 })
@Index()
@Column({ name: 'company_name', type: 'varchar', length: 200 })
companyName!: string;
@Column({ length: 150 })
@Column({ name: 'company_email', type: 'varchar', length: 150 })
companyEmail!: string;
@Column({ length: 20 })
@Column({ name: 'company_phone', type: 'varchar', length: 20 })
companyPhone!: string;
@Column({ length: 100 })
@Column({ name: 'company_location', type: 'varchar', length: 100 })
companyLocation!: string;
@Column({ type: "text" })
@Column({ name: 'company_address', type: 'text' })
companyAddress!: string;
@Column({ length: 100 })
@Column({ name: 'customer_type', type: 'varchar', length: 32, nullable: true })
customerType?: string | null;
@Column({ name: 'status', type: 'varchar', length: 32, nullable: true })
status?: string | null;
@Column({ name: 'contact_person_name', type: 'varchar', length: 100 })
contactPersonName!: string;
@Column({ length: 20 })
@Column({ name: 'contact_person_phone', type: 'varchar', length: 20 })
contactPersonPhone!: string;
@Column({ length: 10, unique: true })
@Index()
@Column({ name: 'tin_number', type: 'varchar', length: 10, unique: true })
tinNumber!: string;
@Column({ length: 50, nullable: true })
vatNumber?: string;
@Column({ name: 'vat_number', type: 'varchar', length: 50, nullable: true })
vatNumber?: string | null;
@Column({ length: 16, unique: true })
@Index()
@Column({ name: 'fan_number', type: 'varchar', length: 16, unique: true })
fanNumber!: string;
@Column({ length: 100 })
@Column({ name: 'general_manager_name', type: 'varchar', length: 100 })
generalManagerName!: string;
@Column({ length: 150 })
@Column({ name: 'general_manager_email', type: 'varchar', length: 150 })
generalManagerEmail!: string;
@Column({ length: 20 })
@Column({ name: 'general_manager_phone', type: 'varchar', length: 20 })
generalManagerPhone!: string;
@Column({ length: 100, nullable: true })
poaName?: string;
@Column({ name: 'poa_name', type: 'varchar', length: 100, nullable: true })
poaName?: string | null;
@Column({ length: 20, nullable: true })
poaPhone?: string;
@Column({ name: 'poa_phone', type: 'varchar', length: 20, nullable: true })
poaPhone?: string | null;
@Column({ type: "text", nullable: true })
poaAddress?: string;
@Column({ name: 'poa_address', type: 'text', nullable: true })
poaAddress?: string | null;
@Column({ nullable: true, length: 150 })
poaEmail?: string;
@Column({ name: 'poa_email', type: 'varchar', length: 150, nullable: true })
poaEmail?: string | null;
@Column({ length: 100, nullable: true })
poaLocation?: string;
@Column({ name: 'poa_location', type: 'varchar', length: 100, nullable: true })
poaLocation?: string | null;
@Column({ type: "text", nullable: true })
notes?: string;
@CreateDateColumn()
createdAt!: Date;
@UpdateDateColumn()
updatedAt!: Date;
}
@Column({ name: 'notes', type: 'text', nullable: true })
notes?: string | null;
}

View File

@@ -1,57 +0,0 @@
import {
Body,
Controller,
Delete,
Get,
HttpCode,
HttpStatus,
Param,
ParseUUIDPipe,
Patch,
Post,
} from "@nestjs/common";
import { ApiOperation, ApiTags } from "@nestjs/swagger";
import { CustomersService } from "./customers.service";
import { CreateCustomerDto } from "./dto/create-customer.dto";
import { UpdateCustomerDto } from "./dto/update-customer.dto";
@ApiTags("customers")
@Controller("customers")
export class CustomersController {
constructor(private readonly customersService: CustomersService) {}
@Post()
@ApiOperation({ summary: "Create a new customer" })
create(@Body() dto: CreateCustomerDto) {
return this.customersService.create(dto);
}
@Get()
@ApiOperation({ summary: "List all customers" })
findAll() {
return this.customersService.findAll();
}
@Get(":id")
@ApiOperation({ summary: "Get a customer by ID" })
findOne(@Param("id", ParseUUIDPipe) id: string) {
return this.customersService.findById(id);
}
@Patch(":id")
@ApiOperation({ summary: "Update a customer" })
update(
@Param("id", ParseUUIDPipe) id: string,
@Body() dto: UpdateCustomerDto,
) {
return this.customersService.update(id, dto);
}
@Delete(":id")
@ApiOperation({ summary: "Soft-delete a customer" })
@HttpCode(HttpStatus.NO_CONTENT)
remove(@Param("id", ParseUUIDPipe) id: string) {
return this.customersService.remove(id);
}
}

View File

@@ -1,15 +0,0 @@
import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";
import { CustomersController } from "./customers.controller";
import { CustomersRepository } from "./customers.repository";
import { CustomersService } from "./customers.service";
import { Customer } from "./entities/customer.entity";
@Module({
imports: [TypeOrmModule.forFeature([Customer])],
controllers: [CustomersController],
providers: [CustomersService, CustomersRepository],
exports: [CustomersService],
})
export class CustomersModule {}

View File

@@ -1,21 +0,0 @@
import { BaseRepository } from "@edr/api-common";
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { Customer } from "./entities/customer.entity";
@Injectable()
export class CustomersRepository extends BaseRepository<Customer> {
constructor(
@InjectRepository(Customer)
repository: Repository<Customer>,
) {
super(repository);
}
/** Find a customer by their unique email. */
findByEmail(email: string): Promise<Customer | null> {
return this.repository.findOne({ where: { email } });
}
}

View File

@@ -1,61 +0,0 @@
import {
ConflictException,
Injectable,
NotFoundException,
} from "@nestjs/common";
import { CustomersRepository } from "./customers.repository";
import { CreateCustomerDto } from "./dto/create-customer.dto";
import { UpdateCustomerDto } from "./dto/update-customer.dto";
import { Customer } from "./entities/customer.entity";
@Injectable()
export class CustomersService {
constructor(private readonly customersRepository: CustomersRepository) {}
async create(dto: CreateCustomerDto): Promise<Customer> {
const existing = await this.customersRepository.findByEmail(dto.email);
if (existing) {
throw new ConflictException(
`Customer with email "${dto.email}" already exists`,
);
}
return this.customersRepository.create(dto);
}
findAll(): Promise<Customer[]> {
return this.customersRepository.findAll({ order: { name: "ASC" } });
}
async findById(id: string): Promise<Customer> {
const customer = await this.customersRepository.findById(id);
if (!customer) {
throw new NotFoundException(`Customer ${id} not found`);
}
return customer;
}
async update(id: string, dto: UpdateCustomerDto): Promise<Customer> {
await this.findById(id);
if (dto.email) {
const conflict = await this.customersRepository.findByEmail(dto.email);
if (conflict && conflict.id !== id) {
throw new ConflictException(
`Customer with email "${dto.email}" already exists`,
);
}
}
const updated = await this.customersRepository.update(id, dto);
if (!updated) {
throw new NotFoundException(`Customer ${id} not found`);
}
return updated;
}
async remove(id: string): Promise<void> {
await this.findById(id);
await this.customersRepository.softDelete(id);
}
}

View File

@@ -1,73 +0,0 @@
import {
IsEmail,
IsEnum,
IsOptional,
IsString,
MaxLength,
} from "class-validator";
export enum CustomerStatusDto {
Active = "Active",
Pending = "Pending",
Inactive = "Inactive",
}
export enum CustomerTypeDto {
Importer = "Importer",
Exporter = "Exporter",
Supplier = "Supplier",
}
export class CreateCustomerDto {
@IsString()
@MaxLength(256)
name!: string;
@IsEmail()
email!: string;
@IsString()
@MaxLength(32)
phone!: string;
@IsOptional()
@IsString()
@MaxLength(256)
company?: string;
@IsOptional()
@IsEnum(CustomerTypeDto)
customerType?: CustomerTypeDto;
@IsOptional()
@IsEnum(CustomerStatusDto)
status?: CustomerStatusDto;
@IsOptional()
@IsString()
@MaxLength(64)
tinNumber?: string;
@IsOptional()
@IsString()
@MaxLength(128)
city?: string;
@IsOptional()
@IsString()
@MaxLength(128)
country?: string;
@IsOptional()
@IsString()
address?: string;
@IsOptional()
@IsString()
@MaxLength(64)
taxId?: string;
@IsOptional()
@IsString()
notes?: string;
}

View File

@@ -1,5 +0,0 @@
import { PartialType } from "@nestjs/mapped-types";
import { CreateCustomerDto } from "./create-customer.dto";
export class UpdateCustomerDto extends PartialType(CreateCustomerDto) {}

View File

@@ -1,54 +0,0 @@
import { BaseEntity } from "@edr/api-common";
import { Column, Entity } from "typeorm";
export type CustomerStatus = "Active" | "Pending" | "Inactive";
export type CustomerType = "Importer" | "Exporter" | "Supplier";
@Entity({schema:"freight", name: "customers" })
export class Customer extends BaseEntity {
@Column({ name: "name", type: "varchar", length: 256 })
name!: string;
@Column({ name: "email", type: "varchar", length: 256, unique: true })
email!: string;
@Column({ name: "phone", type: "varchar", length: 32 })
phone!: string;
@Column({ name: "company", type: "varchar", length: 256, nullable: true })
company?: string | null;
@Column({
name: "customer_type",
type: "varchar",
length: 32,
default: "Importer",
})
customerType!: CustomerType;
@Column({
name: "status",
type: "varchar",
length: 32,
default: "Active",
})
status!: CustomerStatus;
@Column({ name: "tin_number", type: "varchar", length: 64, nullable: true })
tinNumber?: string | null;
@Column({ name: "city", type: "varchar", length: 128, nullable: true })
city?: string | null;
@Column({ name: "country", type: "varchar", length: 128, nullable: true })
country?: string | null;
@Column({ name: "address", type: "text", nullable: true })
address?: string | null;
@Column({ name: "tax_id", type: "varchar", length: 64, nullable: true })
taxId?: string | null;
@Column({ name: "notes", type: "text", nullable: true })
notes?: string | null;
}